diff --git a/.buildinfo b/.buildinfo
new file mode 100644
index 000000000..cd2eb2088
--- /dev/null
+++ b/.buildinfo
@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: 4eafda0cabff8f8cb7f5b7faaebe1c24
+tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 000000000..e69de29bb
diff --git a/_autosummary/reV.SAM.SAM.RevPySam.html b/_autosummary/reV.SAM.SAM.RevPySam.html
new file mode 100644
index 000000000..37630a701
--- /dev/null
+++ b/_autosummary/reV.SAM.SAM.RevPySam.html
@@ -0,0 +1,896 @@
+
+
+
+
+
Base class for reV-SAM simulations (generation and econ).
+
Initialize a SAM object.
+
+
Parameters:
+
+
meta (pd.DataFrame | pd.Series | None) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone. Can be None for econ runs.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
Get the SAM resource iterator object (single year, single file).
+
+
Parameters:
+
+
res_file (str) – Single resource file (with full path) to retrieve.
+
project_points (reV.config.ProjectPoints) – reV Project Points instance used to retrieve resource data at a
+specific set of sites.
+
module (str) – SAM module name or reV technology to force interpretation
+of the resource file type.
+Example: module set to ‘pvwatts’ or ‘tcsmolten’ means that this
+expects a SolarResource file. If ‘nsrdb’ is in the res_file name,
+the NSRDB handler will be used.
+
output_request (list | tuple, optional) – Outputs to retrieve from SAM, by default (‘cf_mean’, )
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
res (reV.resource.SAMResource) – Resource iterator object to pass to SAM.
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
inputs (dict) – Dictionary of SAM system input parameters.
Execute SAM LCOE simulations based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
year (int | str | None) – reV generation year to calculate econ for. Looks for cf_mean_{year}
+or cf_profile_{year}. None will default to a non-year-specific cf
+dataset (cf_mean, cf_profile).
+
output_request (list | tuple | str) – Output(s) to retrieve from SAM.
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Execute SAM SingleOwner simulations based on reV points control.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
year (int | str | None) – reV generation year to calculate econ for. Looks for cf_mean_{year}
+or cf_profile_{year}. None will default to a non-year-specific cf
+dataset (cf_mean, cf_profile).
+
output_request (list | tuple | str) – Output(s) to retrieve from SAM.
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
Base class for running sam generation with a weather file on disk.
+
Initialize a SAM generation object.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Generate the weather file and set the path as an input.
+
Some PySAM models require a data file, not raw data. This method
+generates the weather data, writes it to a file on disk, and
+then sets the file as an input to the generation module. The
+function
+run_gen_and_econ()
+deletes the file on disk after a run is complete.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Time series resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the
+required variables to run the respective SAM simulation.
+Remapping will be done to convert typical NSRDB/WTK names
+into SAM names (e.g. DNI -> dn and wind_speed -> windspeed).
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude,
+elevation, and timezone.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
Photovoltaic (PV) generation with either pvwatts of detailed pv.
+
Initialize a SAM solar object.
+
See the PySAM Pvwattsv8 (or older
+version model) or Pvsamv1 documentation for
+the configuration keys required in the sam_sys_inputs config for the
+respective models. Some notable keys include the following to enable a
+lifetime simulation (non-exhaustive):
+
+
+
system_use_lifetime_output : Integer flag indicating whether
+or not to run a full lifetime model (0 for off, 1 for on). If
+running a lifetime model, the resource file will be repeated
+for the number of years specified as the lifetime of the
+plant and a performance degradation term will be used to
+simulate reduced performance over time.
+
analysis_period : Integer representing the number of years
+to include in the lifetime of the model generator. Required if
+system_use_lifetime_output is set to 1.
+
dc_degradation : List of percentage values representing the
+annual DC degradation of capacity factors. Maybe a single value
+that will be compound each year or a vector of yearly rates.
+Required if system_use_lifetime_output is set to 1.
+
+
+
You may also include the following reV-specific keys:
+
+
+
reV_outages : Specification for reV-scheduled
+stochastic outage losses. For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute NSRDB input data, time_index_step=1 yields
+the full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be an
+integer multiple of 8760, or the execution will
+fail.
+
+
+
clearsky : Boolean flag value indicating wether
+computation should use clearsky resource data to compute
+generation data.
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
+
:raises ValueError : If lat/lon outside of -90 to 90 and -180 to 180,: respectively.
Check if tilt is specified as latitude and set tilt=lat, az=180 or 0
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+If for a pv simulation the “tilt” parameter was originally not
+present or set to ‘lat’ or MetaKeyName.LATITUDE, the tilt will be
+set to the absolute value of the latitude found in meta and the
+azimuth will be 180 if lat>0, 0 if lat<0.
Get hourly capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV CF is calculated as AC power / DC nameplate.
Get hourly AC capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV AC capacity factor is the AC power production / the AC
+nameplate. AC nameplate = DC nameplate / ILR
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV AC CF is calculated as AC power / AC nameplate.
Get AC inverter power generation profile (local timezone) in kW.
+This is an alias of the “ac” SAM output variable if PySAM version>=3.
+See self.outputs attribute for collected output data in UTC.
+
+
Returns:
+
output (np.ndarray) – 1D array of AC inverter power generation in kW.
+Datatype is float32 and array length is 8760*time_interval.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
See the PySAM Windpower
+documentation for the configuration keys required in the
+sam_sys_inputs config. You may also include the following
+reV-specific keys:
+
+
+
reV_power_curve_losses : A dictionary that can be used
+to initialize
+PowerCurveLossesInput.
+For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute input data, time_index_step=1 yields the
+full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be
+an integer multiple of 8760, or the execution
+will fail.
+
+
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Adjust power curve in SAM config file to account for losses.
+
This function reads the information in the
+reV_power_curve_losses key of the sam_sys_inputs
+dictionary and computes a new power curve that accounts for the
+loss percentage specified from that input. If no power curve
+loss info is specified in sam_sys_inputs, the power curve
+will not be adjusted.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
As of 12/20/2022, the resource potential input in SAM is only used
+to calculate the number of well replacements during the lifetime of
+a geothermal plant. It was decided that reV would not model well
+replacements. Therefore, reV sets the resource potential to match
+(or be just above) the gross potential so that SAM does not throw
+any errors.
+
Also as of 12/20/2022, the SAM GETEM module requires a weather file,
+but does not actually require any weather data to run. Therefore,
+reV currently generates an empty weather file to pass to SAM. This
+behavior can be easily updated in the future should the SAM GETEM
+module start using weather data.
+
See the PySAM Geothermal documentation
+for the configuration keys required in the sam_sys_inputs config.
+Some notable keys include (non-exhaustive):
+
+
+
resource_type : Integer flag representing either
+Hydrothermal (0) or EGS (1) resource. Only values of 0 or 1
+allowed.
+
resource_potential : Total resource potential at location
+(in MW).
+
+
Important
+
reV automatically sets the resource
+potential to match the gross potential (see documentation
+above), so this key should be left out of the config (it
+will be overridden in any case).
+
+
+
resource_temp : Temperature of resource (in C).
+
+
Important
+
This value is set by reV based on the
+user’s geothermal resource data input. To override this
+behavior, users may specify their own resource_temp
+value (either a single value for all sites in the SAM
+geothermal config or a site-dependent value in the project
+points CSV). In this case, the resource temperature from
+the input data will be ignored completely, and the
+temperature at each location will be determined solely from
+this input.
+
+
+
resource_depth : Depth to geothermal resource (in m).
+
analysis_type : Integer flag representing the plant
+configuration. If the nameplate input is to be used to
+specify the plant capacity, then this flag should be set to 0
+(this is the default reV assumption). Otherwise, if the
+num_wells input is to be used to specify the plant site,
+then this flag should be set to 1. Only values of 0 or 1
+allowed.
+
nameplate : Geothermal plant size (in kW). Only affects
+the output if analysis_type=0.
+
+
Important
+
Unlike wind or solar, reV geothermal
+dynamically sets the size of a geothermal plant. In
+particular, the plant capacity is set to match the resource
+potential (obtained from the input data) for each site. For
+this to work, users must leave out the nameplate
+key from the SAM config.
+
Alternatively, users may specify their own nameplate
+capacity value (either a single value for all sites in the
+SAM geothermal config or a site-dependent value in the
+project points CSV). In this case, the resource potential
+from the input data will be ignored completely, and the
+capacity at each location will be determined solely from
+this input.
+
+
+
num_wells : Number of wells at each plant. This value is
+used to determined plant capacity if analysis_type=1.
+Otherwise this input has no effect.
+
num_wells_getem : Number of wells assumed at each plant
+for power block calculations. Only affects power block outputs
+if analysis_type=0 (otherwise the num_wells input is
+used in power block calculations).
+
+
Note
+
reV does not currently adjust this value based
+on the resource input (as it probably should). If any
+power block outputs are required in the future, there may
+need to be extra development to set this value based on
+the dynamically calculated plant size.
+
+
+
conversion_type : Integer flag representing the conversion
+plant type. Either Binary (0) or Flash (1). Only values of 0
+or 1 allowed.
+
geotherm.cost.inj_prod_well_ratio : Fraction representing
+the injection to production well ratio (0-1). SAM GUI defaults
+to 0.5 for this value, but it is recommended to set this to
+the GETEM default of 0.75.
+
+
+
You may also include the following reV-specific keys:
+
+
+
num_confirmation_wells : Number of confirmation wells that
+can also be used as production wells. This number is used to
+determined to total number of wells required at each plant,
+and therefore the total drilling costs. This value defaults to
+2 (to match the SAM GUI as of 8/1/2023). However, the default
+value can lead to negative costs if the plant size is small
+(e.g. only 1 production well is needed, so the costs equal
+-1 * drill_cost_per_well). This is a limitation of the
+SAM calculations (as of 8/1/2023), and it is therefore useful
+to set num_confirmation_wells=0 when performing reV
+runs for small plant sizes.
+
capital_cost_per_kw : Capital cost values in $/kW. If
+this value is specified in the config, reV calculates and
+overrides the total capital_cost value based on the
+geothermal plant size (capacity) at each location.
+
fixed_operating_cost : Fixed operating cost values in
+$/kW. If this value is specified in the config, reV calculates
+and overrides the total fixed_operating_cost value based
+on the geothermal plant size (capacity) at each location.
+
drill_cost_per_well : Drilling cost per well, in $. If
+this value is specified in the config, reV calculates the
+total drilling costs based on the number of wells that need to
+be drilled at each location. The drilling costs are added to
+the total capital_cost at each location.
+
reV_outages : Specification for reV-scheduled
+stochastic outage losses. For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute NSRDB input data, time_index_step=1 yields
+the full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
+
Initialize a SAM generation object.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Generate the weather file and set the path as an input.
+
The Geothermal PySAM model requires a data file, not raw data.
+This method generates the weather data, writes it to a file on
+disk, and then sets the file as an input to the Geothermal
+generation module. The function
+run_gen_and_econ()
+deletes the file on disk after a run is complete.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Time series resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the
+required variables to run the respective SAM simulation.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude,
+elevation, and timezone.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
Process heat linear Fresnel direct steam generation
+
Initialize a SAM generation object.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Generate the weather file and set the path as an input.
+
Some PySAM models require a data file, not raw data. This method
+generates the weather data, writes it to a file on disk, and
+then sets the file as an input to the generation module. The
+function
+run_gen_and_econ()
+deletes the file on disk after a run is complete.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Time series resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the
+required variables to run the respective SAM simulation.
+Remapping will be done to convert typical NSRDB/WTK names
+into SAM names (e.g. DNI -> dn and wind_speed -> windspeed).
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude,
+elevation, and timezone.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
resource (pd.DataFrame) – Timeseries resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
See the PySAM Pvwattsv8 (or older
+version model) or Pvsamv1 documentation for
+the configuration keys required in the sam_sys_inputs config for the
+respective models. Some notable keys include the following to enable a
+lifetime simulation (non-exhaustive):
+
+
+
system_use_lifetime_output : Integer flag indicating whether
+or not to run a full lifetime model (0 for off, 1 for on). If
+running a lifetime model, the resource file will be repeated
+for the number of years specified as the lifetime of the
+plant and a performance degradation term will be used to
+simulate reduced performance over time.
+
analysis_period : Integer representing the number of years
+to include in the lifetime of the model generator. Required if
+system_use_lifetime_output is set to 1.
+
dc_degradation : List of percentage values representing the
+annual DC degradation of capacity factors. Maybe a single value
+that will be compound each year or a vector of yearly rates.
+Required if system_use_lifetime_output is set to 1.
+
+
+
You may also include the following reV-specific keys:
+
+
+
reV_outages : Specification for reV-scheduled
+stochastic outage losses. For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute NSRDB input data, time_index_step=1 yields
+the full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be an
+integer multiple of 8760, or the execution will
+fail.
+
+
+
clearsky : Boolean flag value indicating wether
+computation should use clearsky resource data to compute
+generation data.
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
Get hourly capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV CF is calculated as AC power / DC nameplate.
Get hourly AC capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV AC capacity factor is the AC power production / the AC
+nameplate. AC nameplate = DC nameplate / ILR
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV AC CF is calculated as AC power / AC nameplate.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Get AC inverter power generation profile (local timezone) in kW.
+This is an alias of the “ac” SAM output variable if PySAM version>=3.
+See self.outputs attribute for collected output data in UTC.
+
+
Returns:
+
output (np.ndarray) – 1D array of AC inverter power generation in kW.
+Datatype is float32 and array length is 8760*time_interval.
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check if tilt is specified as latitude and set tilt=lat, az=180 or 0
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+If for a pv simulation the “tilt” parameter was originally not
+present or set to ‘lat’ or MetaKeyName.LATITUDE, the tilt will be
+set to the absolute value of the latitude found in meta and the
+azimuth will be 180 if lat>0, 0 if lat<0.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
+
:raises ValueError : If lat/lon outside of -90 to 90 and -180 to 180,: respectively.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
See the PySAM Pvwattsv8 (or older
+version model) or Pvsamv1 documentation for
+the configuration keys required in the sam_sys_inputs config for the
+respective models. Some notable keys include the following to enable a
+lifetime simulation (non-exhaustive):
+
+
+
system_use_lifetime_output : Integer flag indicating whether
+or not to run a full lifetime model (0 for off, 1 for on). If
+running a lifetime model, the resource file will be repeated
+for the number of years specified as the lifetime of the
+plant and a performance degradation term will be used to
+simulate reduced performance over time.
+
analysis_period : Integer representing the number of years
+to include in the lifetime of the model generator. Required if
+system_use_lifetime_output is set to 1.
+
dc_degradation : List of percentage values representing the
+annual DC degradation of capacity factors. Maybe a single value
+that will be compound each year or a vector of yearly rates.
+Required if system_use_lifetime_output is set to 1.
+
+
+
You may also include the following reV-specific keys:
+
+
+
reV_outages : Specification for reV-scheduled
+stochastic outage losses. For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute NSRDB input data, time_index_step=1 yields
+the full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be an
+integer multiple of 8760, or the execution will
+fail.
+
+
+
clearsky : Boolean flag value indicating wether
+computation should use clearsky resource data to compute
+generation data.
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
Get hourly capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV CF is calculated as AC power / DC nameplate.
Get hourly AC capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV AC capacity factor is the AC power production / the AC
+nameplate. AC nameplate = DC nameplate / ILR
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV AC CF is calculated as AC power / AC nameplate.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Get AC inverter power generation profile (local timezone) in kW.
+This is an alias of the “ac” SAM output variable if PySAM version>=3.
+See self.outputs attribute for collected output data in UTC.
+
+
Returns:
+
output (np.ndarray) – 1D array of AC inverter power generation in kW.
+Datatype is float32 and array length is 8760*time_interval.
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check if tilt is specified as latitude and set tilt=lat, az=180 or 0
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+If for a pv simulation the “tilt” parameter was originally not
+present or set to ‘lat’ or MetaKeyName.LATITUDE, the tilt will be
+set to the absolute value of the latitude found in meta and the
+azimuth will be 180 if lat>0, 0 if lat<0.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
+
:raises ValueError : If lat/lon outside of -90 to 90 and -180 to 180,: respectively.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
See the PySAM Pvwattsv8 (or older
+version model) or Pvsamv1 documentation for
+the configuration keys required in the sam_sys_inputs config for the
+respective models. Some notable keys include the following to enable a
+lifetime simulation (non-exhaustive):
+
+
+
system_use_lifetime_output : Integer flag indicating whether
+or not to run a full lifetime model (0 for off, 1 for on). If
+running a lifetime model, the resource file will be repeated
+for the number of years specified as the lifetime of the
+plant and a performance degradation term will be used to
+simulate reduced performance over time.
+
analysis_period : Integer representing the number of years
+to include in the lifetime of the model generator. Required if
+system_use_lifetime_output is set to 1.
+
dc_degradation : List of percentage values representing the
+annual DC degradation of capacity factors. Maybe a single value
+that will be compound each year or a vector of yearly rates.
+Required if system_use_lifetime_output is set to 1.
+
+
+
You may also include the following reV-specific keys:
+
+
+
reV_outages : Specification for reV-scheduled
+stochastic outage losses. For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute NSRDB input data, time_index_step=1 yields
+the full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be an
+integer multiple of 8760, or the execution will
+fail.
+
+
+
clearsky : Boolean flag value indicating wether
+computation should use clearsky resource data to compute
+generation data.
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
Get hourly capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV CF is calculated as AC power / DC nameplate.
Get hourly AC capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV AC capacity factor is the AC power production / the AC
+nameplate. AC nameplate = DC nameplate / ILR
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV AC CF is calculated as AC power / AC nameplate.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Get AC inverter power generation profile (local timezone) in kW.
+This is an alias of the “ac” SAM output variable if PySAM version>=3.
+See self.outputs attribute for collected output data in UTC.
+
+
Returns:
+
output (np.ndarray) – 1D array of AC inverter power generation in kW.
+Datatype is float32 and array length is 8760*time_interval.
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check if tilt is specified as latitude and set tilt=lat, az=180 or 0
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+If for a pv simulation the “tilt” parameter was originally not
+present or set to ‘lat’ or MetaKeyName.LATITUDE, the tilt will be
+set to the absolute value of the latitude found in meta and the
+azimuth will be 180 if lat>0, 0 if lat<0.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
+
:raises ValueError : If lat/lon outside of -90 to 90 and -180 to 180,: respectively.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
See the PySAM Pvwattsv8 (or older
+version model) or Pvsamv1 documentation for
+the configuration keys required in the sam_sys_inputs config for the
+respective models. Some notable keys include the following to enable a
+lifetime simulation (non-exhaustive):
+
+
+
system_use_lifetime_output : Integer flag indicating whether
+or not to run a full lifetime model (0 for off, 1 for on). If
+running a lifetime model, the resource file will be repeated
+for the number of years specified as the lifetime of the
+plant and a performance degradation term will be used to
+simulate reduced performance over time.
+
analysis_period : Integer representing the number of years
+to include in the lifetime of the model generator. Required if
+system_use_lifetime_output is set to 1.
+
dc_degradation : List of percentage values representing the
+annual DC degradation of capacity factors. Maybe a single value
+that will be compound each year or a vector of yearly rates.
+Required if system_use_lifetime_output is set to 1.
+
+
+
You may also include the following reV-specific keys:
+
+
+
reV_outages : Specification for reV-scheduled
+stochastic outage losses. For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute NSRDB input data, time_index_step=1 yields
+the full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be an
+integer multiple of 8760, or the execution will
+fail.
+
+
+
clearsky : Boolean flag value indicating wether
+computation should use clearsky resource data to compute
+generation data.
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
Get hourly capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV CF is calculated as AC power / DC nameplate.
Get hourly AC capacity factor (frac) profile in local timezone.
+See self.outputs attribute for collected output data in UTC.
+
NOTE: PV AC capacity factor is the AC power production / the AC
+nameplate. AC nameplate = DC nameplate / ILR
+
+
Returns:
+
cf_profile (np.ndarray) – 1D numpy array of capacity factor profile.
+Datatype is float32 and array length is 8760*time_interval.
+PV AC CF is calculated as AC power / AC nameplate.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Get AC inverter power generation profile (local timezone) in kW.
+This is an alias of the “ac” SAM output variable if PySAM version>=3.
+See self.outputs attribute for collected output data in UTC.
+
+
Returns:
+
output (np.ndarray) – 1D array of AC inverter power generation in kW.
+Datatype is float32 and array length is 8760*time_interval.
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check if tilt is specified as latitude and set tilt=lat, az=180 or 0
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+If for a pv simulation the “tilt” parameter was originally not
+present or set to ‘lat’ or MetaKeyName.LATITUDE, the tilt will be
+set to the absolute value of the latitude found in meta and the
+azimuth will be 180 if lat>0, 0 if lat<0.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
+
:raises ValueError : If lat/lon outside of -90 to 90 and -180 to 180,: respectively.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Generate the weather file and set the path as an input.
+
Some PySAM models require a data file, not raw data. This method
+generates the weather data, writes it to a file on disk, and
+then sets the file as an input to the generation module. The
+function
+run_gen_and_econ()
+deletes the file on disk after a run is complete.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Time series resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the
+required variables to run the respective SAM simulation.
+Remapping will be done to convert typical NSRDB/WTK names
+into SAM names (e.g. DNI -> dn and wind_speed -> windspeed).
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude,
+elevation, and timezone.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
Concentrated Solar Power (CSP) generation with tower molten salt
+
Initialize a SAM generation object.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Generate the weather file and set the path as an input.
+
Some PySAM models require a data file, not raw data. This method
+generates the weather data, writes it to a file on disk, and
+then sets the file as an input to the generation module. The
+function
+run_gen_and_econ()
+deletes the file on disk after a run is complete.
+
+
Parameters:
+
+
resource (pd.DataFrame) – Time series resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the
+required variables to run the respective SAM simulation.
+Remapping will be done to convert typical NSRDB/WTK names
+into SAM names (e.g. DNI -> dn and wind_speed -> windspeed).
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude,
+elevation, and timezone.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
See the PySAM Windpower
+documentation for the configuration keys required in the
+sam_sys_inputs config. You may also include the following
+reV-specific keys:
+
+
+
reV_power_curve_losses : A dictionary that can be used
+to initialize
+PowerCurveLossesInput.
+For example:
reV_outages_seed : Integer value used to seed the RNG
+used to compute stochastic outage losses.
+
time_index_step : Integer representing the step size
+used to sample the time_index in the resource data.
+This can be used to reduce temporal resolution (i.e. for
+30 minute input data, time_index_step=1 yields the
+full 30 minute time series as output, while
+time_index_step=2 yields hourly output, and so forth).
+
+
Note
+
The reduced data shape (i.e. after applying a
+step size of time_index_step) must still be
+an integer multiple of 8760, or the execution
+will fail.
+
+
+
+
+
+
Parameters:
+
+
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
+
meta (pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
Adjust power curve in SAM config file to account for losses.
+
This function reads the information in the
+reV_power_curve_losses key of the sam_sys_inputs
+dictionary and computes a new power curve that accounts for the
+loss percentage specified from that input. If no power curve
+loss info is specified in sam_sys_inputs, the power curve
+will not be adjusted.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
WindPower analysis with wind speed/direction joint probabilty
+distrubtion input
+
Initialize a SAM generation object for windpower with a
+speed/direction joint probability distribution.
+
+
Parameters:
+
+
ws_edges (np.ndarray) – 1D array of windspeed (m/s) values that set the bin edges for the
+wind probability distribution. Same len as wind_dist.shape[0] + 1
+
wd_edges (np.ndarray) – 1D array of winddirections (deg) values that set the bin edges
+for the wind probability dist. Same len as wind_dist.shape[1] + 1
+
wind_dist (np.ndarray) – 2D array probability distribution of (windspeed, winddirection).
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
ws_edges (np.ndarray) – 1D array of windspeed (m/s) values that set the bin edges for the
+wind probability distribution. Same len as wind_dist.shape[0] + 1
+
wd_edges (np.ndarray) – 1D array of winddirections (deg) values that set the bin edges
+for the wind probability dist. Same len as wind_dist.shape[1] + 1
+
wind_dist (np.ndarray) – 2D array probability distribution of (windspeed, winddirection).
Adjust power curve in SAM config file to account for losses.
+
This function reads the information in the
+reV_power_curve_losses key of the sam_sys_inputs
+dictionary and computes a new power curve that accounts for the
+loss percentage specified from that input. If no power curve
+loss info is specified in sam_sys_inputs, the power curve
+will not be adjusted.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
+
+
+
+
See also
+
+
Outage
Single outage specification.
+
+
+
+
Notes
+
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
resource (pd.DataFrame) – Timeseries solar or wind resource data for a single location with a
+pandas DatetimeIndex. There must be columns for all the required
+variables to run the respective SAM simulation. Remapping will be
+done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+dn and wind_speed -> windspeed)
Execute SAM generation based on a reV points control instance.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
res_file (str) – Resource file with full path.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
lr_res_file (str | None) – Optional low resolution resource file that will be dynamically
+mapped+interpolated to the nominal-resolution res_file. This
+needs to be of the same format as resource_file, e.g. they both
+need to be handled by the same rex Resource handler such as
+WindResource
+
output_request (list | tuple) – Outputs to retrieve from SAM.
+
drop_leap (bool) – Drops February 29th from the resource data. If False, December
+31st is dropped from leap years.
+
gid_map (None | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This enables the user to input unique
+generation gids in the project points that map to non-unique
+resource gids. This can be None or a pre-extracted dict.
+
nn_map (np.ndarray) – Optional 1D array of nearest neighbor mappings associated with the
+res_file to lr_res_file spatial mapping. For details on this
+argument, see the rex.MultiResolutionResource docstring.
+
bias_correct (None | pd.DataFrame) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
Check timezone+elevation input and use json config
+timezone+elevation if not in resource meta.
+
+
Parameters:
+
+
sam_sys_inputs (dict) – Site-agnostic SAM system model inputs arguments.
+
site_sys_inputs (dict) – Optional set of site-specific SAM system inputs to complement the
+site-agnostic inputs.
+
meta (pd.DataFrame | pd.Series) – Meta data corresponding to the resource input for the single
+location. Should include values for latitude, longitude, elevation,
+and timezone.
+
+
+
Returns:
+
meta (pd.DataFrame | pd.Series) – Dataframe or series for a single site. Will include “timezone”
+and “elevation” from the sam and site system inputs if found.
Execute SAM SingleOwner simulations based on reV points control.
+
+
Parameters:
+
+
points_control (config.PointsControl) – PointsControl instance containing project points site and SAM
+config info.
+
site_df (pd.DataFrame) – Dataframe of site-specific input variables. Row index corresponds
+to site number/gid (via df.loc not df.iloc), column labels are the
+variable keys that will be passed forward as SAM parameters.
+
output_request (list | tuple | str) – Output(s) to retrieve from SAM.
+
kwargs (dict) – Not used but maintained for polymorphic calls with other
+SAM econ reV_run() methods (lcoe and single owner).
+Breaks pylint error W0613: unused argument.
+
+
+
Returns:
+
out (dict) – Nested dictionaries where the top level key is the site index,
+the second level key is the variable name, second level value is
+the output variable value.
This object is intended to facilitate the use of pre-loaded data for
+running BespokeWindPlants on systems with slow parallel
+reads to a single HDF5 file.
+
Initialize BespokeMultiPlantData
+
+
Parameters:
+
+
res_fpath (str | list) – Unix shell style path (potentially containing wildcard (*)
+patterns) to a single or multi-file resource file set(s).
+Can also be an explicit list of resource file paths, which
+themselves can contain wildcards. This input must be
+readable by
+rex.multi_year_resource.MultiYearWindResource.
+
sc_gid_to_hh (dict) – Dictionary mapping SC GID values to hub-heights. Data for
+each SC GID will be pulled for the corresponding hub-height
+given in this dictionary.
+
sc_gid_to_res_gid (dict) – Dictionary mapping SC GID values to an iterable oif resource
+GID values. Resource GID values should correspond to GID
+values in the HDF5 file, so any GID map must be applied
+before initializing :class`BespokeMultiPlantData`.
+
pre_load_humidity (optional, default=False) – Option to pre-load relative humidity data (useful for icing
+runs). If False, relative humidities are not loaded.
Framework for analyzing and optimizing a wind plant layout specific to
+the local wind resource and exclusions for a single reV supply curve point.
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
res (str | Resource) – Filepath to .h5 wind resource file or pre-initialized Resource
+handler
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
sam_sys_inputs (dict) – SAM windpower compute module system inputs not including the
+wind resource data.
+
objective_function (str) – The objective function of the optimization as a string, should
+return the objective to be minimized during layout optimization.
+Variables available are:
+
+
+
n_turbines: the number of turbines
+
system_capacity: wind plant capacity
+
aep: annual energy production
+
avg_sl_dist_to_center_m: Average straight-line
+distance to the supply curve point center from all
+turbine locations (in m). Useful for computing plant
+BOS costs.
+
avg_sl_dist_to_medoid_m: Average straight-line
+distance to the medoid of all turbine locations
+(in m). Useful for computing plant BOS costs.
+
nn_conn_dist_m: Total BOS connection distance
+using nearest-neighbor connections. This variable is
+only available for the
+balance_of_system_cost_function equation.
+
fixed_charge_rate: user input fixed_charge_rate if
+included as part of the sam system config.
+
capital_cost: plant capital cost as evaluated
+by capital_cost_function
+
fixed_operating_cost: plant fixed annual operating
+cost as evaluated by fixed_operating_cost_function
+
variable_operating_cost: plant variable annual
+operating cost as evaluated by
+variable_operating_cost_function
+
balance_of_system_cost: plant balance of system
+cost as evaluated by balance_of_system_cost_function
+
self.wind_plant: the SAM wind plant object,
+through which all SAM variables can be accessed
+
+
+
+
capital_cost_function (str) – The plant capital cost function as a string, must return the total
+capital cost in $. Has access to the same variables as the
+objective_function.
+
fixed_operating_cost_function (str) – The plant annual fixed operating cost function as a string, must
+return the fixed operating cost in $/year. Has access to the same
+variables as the objective_function.
+
variable_operating_cost_function (str) – The plant annual variable operating cost function as a string, must
+return the variable operating cost in $/kWh. Has access to the same
+variables as the objective_function. You can set this to “0”
+to effectively ignore variable operating costs.
+
balance_of_system_cost_function (str) – The plant balance-of-system cost function as a string, must
+return the variable operating cost in $. Has access to the
+same variables as the objective_function. You can set this
+to “0” to effectively ignore balance-of-system costs.
+
balance_of_system_cost_function (str) – The plant balance-of-system cost function as a string, must
+return the variable operating cost in $. Has access to the same
+variables as the objective_function.
+
min_spacing (float | int | str) – Minimum spacing between turbines in meters. Can also be a string
+like “5x” (default) which is interpreted as 5 times the turbine
+rotor diameter.
+
wake_loss_multiplier (float, optional) – A multiplier used to scale the annual energy lost due to
+wake losses.
+.. WARNING:: This multiplier will ONLY be applied during the
+optimization process and will NOT be come through in output
+values such as the hourly profiles,
+aep, any of the cost functions, or even the output objective.
+
ga_kwargs (dict | None) – Dictionary of keyword arguments to pass to GA initialization.
+If None, default initialization values are used.
+See GeneticAlgorithm for
+a description of the allowed keyword arguments.
+
output_request (list | tuple) – Outputs requested from the SAM windpower simulation after the
+bespoke plant layout optimization. Can also request resource means
+like ws_mean, windspeed_mean, temperature_mean, pressure_mean.
+
ws_bins (tuple) – 3-entry tuple with (start, stop, step) for the windspeed binning of
+the wind joint probability distribution. The stop value is
+inclusive, so ws_bins=(0, 20, 5) would result in four bins with bin
+edges (0, 5, 10, 15, 20).
+
wd_bins (tuple) – 3-entry tuple with (start, stop, step) for the winddirection
+binning of the wind joint probability distribution. The stop value
+is inclusive, so ws_bins=(0, 360, 90) would result in four bins
+with bin edges (0, 90, 180, 270, 360).
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
data_layers (None | dict) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
eos_mult_baseline_cap_mw (int | float, optional) – Baseline plant capacity (MW) used to calculate economies of
+scale (EOS) multiplier from the capital_cost_function. EOS
+multiplier is calculated as the $-per-kW of the wind plant
+divided by the $-per-kW of a plant with this baseline
+capacity. By default, 200 (MW), which aligns the baseline
+with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
+
prior_meta (pd.DataFrame | None) – Optional meta dataframe belonging to a prior run. This will only
+run the timeseries power generation step and assume that all of the
+wind plant layouts are fixed given the prior run. The meta data
+needs columns “capacity”, “turbine_x_coords”, and
+“turbine_y_coords”.
+
gid_map (None | str | dict) – Mapping of unique integer generation gids (keys) to single integer
+resource gids (values). This can be None, a pre-extracted dict, or
+a filepath to json or csv. If this is a csv, it must have the
+columns “gid” (which matches the techmap) and “gid_map” (gids to
+extract from the resource input). This is useful if you’re running
+forecasted resource data (e.g., ECMWF) to complement historical
+meteorology (e.g., WTK).
+
bias_correct (str | pd.DataFrame, optional) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
pre_loaded_data (BespokeSinglePlantData, optional) – A pre-loaded BespokeSinglePlantData object, or
+None. Can be useful to speed up execution on file
+systems with slow parallel reads.
+
close (bool) – Flag to close object file handlers on exit.
Get the reV compliant wind resource dataframe representing the aggregated and included wind resource in the current reV supply curve point at the turbine hub height.
Bias correct windspeed data if the bias_correct input was
+provided.
+
+
Parameters:
+
+
ws (np.ndarray) – Windspeed data in shape (time, space)
+
dset (str) – Resource dataset name e.g., “windspeed_100m”, “temperature_100m”,
+“pressure_100m”, or something similar
+
h5_gids (list | np.ndarray) – Array of integer gids (spatial indices) from the source h5 file.
+This is used to get the correct bias correction parameters from
+bias_correct table based on its gid column
+
+
+
Returns:
+
ws (np.ndarray) – Bias corrected windspeed data in same shape as input
Get the SAM windpower system inputs. If the wind plant has not yet
+been optimized, this returns the initial SAM config. If the wind plant
+has been optimized using the wind_plant_pd object, this returns the
+final optimized SAM plant config.
Get the reV compliant wind resource dataframe representing the
+aggregated and included wind resource in the current reV supply curve
+point at the turbine hub height. Includes a DatetimeIndex and columns
+for temperature, pressure, windspeed, and winddirection.
Get the wind joint probability distribution and corresonding bin
+edges
+
+
Returns:
+
+
wind_dist (np.ndarray) – 2D array probability distribution of (windspeed, winddirection)
+normalized so the sum of all values = 1.
+
ws_edges (np.ndarray) – 1D array of windspeed (m/s) values that set the bin edges for the
+wind probability distribution. Same len as wind_dist.shape[0] + 1
+
wd_edges (np.ndarray) – 1D array of winddirections (deg) values that set the bin edges
+for the wind probability dist. Same len as wind_dist.shape[1] + 1
Get a namespace of arguments for calculating LCOE based on the
+bespoke optimized wind plant capacity
+
+
Returns:
+
lcoe_kwargs (dict) – kwargs for the SAM lcoe model. These are based on the original
+sam_sys_inputs, normalized to the original system_capacity, and
+updated based on the bespoke optimized system_capacity, includes
+fixed_charge_rate, system_capacity (kW), capital_cost ($),
+fixed_operating_cos ($), variable_operating_cost ($/kWh),
+balance_of_system_cost ($). Data source priority: outputs,
+plant_optimizer, original_sam_sys_inputs, meta
Run the wind plant multi-year timeseries analysis and export output
+requests to outputs property.
+
+
Returns:
+
outputs (dict) – Output dictionary for the full BespokeSinglePlant object. The
+multi-year timeseries data is also exported to the
+BespokeSinglePlant.outputs property.
Run the wind plant layout optimization and export outputs
+to outputs property.
+
+
Returns:
+
outputs (dict) – Output dictionary for the full BespokeSinglePlant object. The
+layout optimization output data is also exported to the
+BespokeSinglePlant.outputs property.
This object is intended to facilitate the use of pre-loaded data for
+running BespokeSinglePlant on systems with slow parallel
+reads to a single HDF5 file.
+
Initialize BespokeSinglePlantData
+
+
Parameters:
+
+
data_inds (1D np.array) – Array of res GIDs. This array should be the same length as
+the second dimension of wind_dirs, wind_speeds, temps,
+and pressures. The GID value of data_inds[0] should
+correspond to the wind_dirs[:, 0] data, etc.
+
wind_dirs (2D np.array) – Array of wind directions. Dimensions should be correspond to
+[time, location]. See documentation for data_inds for
+required spatial mapping of GID values.
+
wind_speeds (2D np.array) – Array of wind speeds. Dimensions should be correspond to
+[time, location]. See documentation for data_inds for
+required spatial mapping of GID values.
+
temps (2D np.array) – Array oftemperatures. Dimensions should be correspond to
+[time, location]. See documentation for data_inds for
+required spatial mapping of GID values.
+
pressures (2D np.array) – Array of pressures. Dimensions should be correspond to
+pressures, respectively. Dimensions should be correspond to
+[time, location]. See documentation for data_inds for
+required spatial mapping of GID values.
+
time_index (1D np.array) – Time index array corresponding to the temporal dimension of
+the 2D data. Will be exposed directly to user.
+
relative_humidities (2D np.array, optional) – Array of relative humidities. Dimensions should be
+correspond to [time, location]. See documentation for
+data_inds for required spatial mapping of GID values.
+If None, relative_humidities cannot be queried.
Much like generation, reV bespoke analysis runs SAM
+simulations by piping in renewable energy resource data (usually
+from the WTK), loading the SAM config, and then executing the
+PySAM.Windpower.Windpower compute module.
+However, unlike reV generation, bespoke analysis is
+performed on the supply-curve grid resolution, and the plant
+layout is optimized for every supply-curve point based on an
+optimization objective specified by the user. See the NREL
+publication on the bespoke methodology for more information.
excl_fpath (str | list | tuple) – Filepath to exclusions data HDF5 file. The exclusions HDF5
+file should contain the layers specified in excl_dict
+and data_layers. These layers may also be spread out
+across multiple HDF5 files, in which case this input should
+be a list or tuple of filepaths pointing to the files
+containing the layers. Note that each data layer must be
+uniquely defined (i.e.only appear once and in a single
+input file).
+
res_fpath (str) – Unix shell style path to wind resource HDF5 file in NREL WTK
+format. Can also be a path including a wildcard input like
+/h5_dir/prefix*suffix to run bespoke on multiple years
+of resource data. Can also be an explicit list of resource
+HDF5 file paths, which themselves can contain wildcards. If
+multiple files are specified in this way, they must have the
+same coordinates but can have different time indices (i.e.
+different years). This input must be readable by
+rex.multi_year_resource.MultiYearWindResource
+(i.e. the resource data conform to the
+rex data format). This
+means the data file(s) must contain a 1D time_index
+dataset indicating the UTC time of observation, a 1D
+meta dataset represented by a DataFrame with
+site-specific columns, and 2D resource datasets that match
+the dimensions of (time_index, meta). The time index must
+start at 00:00 of January 1st of the year under
+consideration, and its shape must be a multiple of 8760.
+
tm_dset (str) – Dataset name in the excl_fpath file containing the
+techmap (exclusions-to-resource mapping data). This data
+layer links the supply curve GID’s to the generation GID’s
+that are used to evaluate the performance metrics of each
+wind plant. By default, the generation GID’s are assumed to
+match the resource GID’s, but this mapping can be customized
+via the gid_map input (see the documentation for gid_map
+for more details).
+
+
Important
+
This dataset uniquely couples the (typically
+high-resolution) exclusion layers to the (typically
+lower-resolution) resource data. Therefore, a separate
+techmap must be used for every unique combination of
+resource and exclusion coordinates.
+
+
+
objective_function (str) – The objective function of the optimization written out as a
+string. This expression should compute the objective to be
+minimized during layout optimization. Variables available
+for computation are:
+
+
+
n_turbines: the number of turbines
+
system_capacity: wind plant capacity
+
aep: annual energy production
+
avg_sl_dist_to_center_m: Average straight-line
+distance to the supply curve point center from all
+turbine locations (in m). Useful for computing plant
+BOS costs.
+
avg_sl_dist_to_medoid_m: Average straight-line
+distance to the medoid of all turbine locations
+(in m). Useful for computing plant BOS costs.
+
nn_conn_dist_m: Total BOS connection distance
+using nearest-neighbor connections. This variable is
+only available for the
+balance_of_system_cost_function equation.
+
fixed_charge_rate: user input fixed_charge_rate if
+included as part of the sam system config.
+
capital_cost: plant capital cost as evaluated
+by capital_cost_function
+
fixed_operating_cost: plant fixed annual operating
+cost as evaluated by fixed_operating_cost_function
+
variable_operating_cost: plant variable annual
+operating cost as evaluated by
+variable_operating_cost_function
+
balance_of_system_cost: plant balance of system
+cost as evaluated by balance_of_system_cost_function
+
self.wind_plant: the SAM wind plant object,
+through which all SAM variables can be accessed
+
+
+
+
capital_cost_function (str) – The plant capital cost function written out as a string.
+This expression must return the total plant capital cost in
+$. This expression has access to the same variables as the
+objective_function argument above.
+
fixed_operating_cost_function (str) – The plant annual fixed operating cost function written out
+as a string. This expression must return the fixed operating
+cost in $/year. This expression has access to the same
+variables as the objective_function argument above.
+
variable_operating_cost_function (str) – The plant annual variable operating cost function written
+out as a string. This expression must return the variable
+operating cost in $/kWh. This expression has access to the
+same variables as the objective_function argument above.
+You can set this to “0” to effectively ignore variable
+operating costs.
+
balance_of_system_cost_function (str) – The plant balance-of-system cost function as a string, must
+return the variable operating cost in $. Has access to the
+same variables as the objective_function. You can set this
+to “0” to effectively ignore balance-of-system costs.
+
project_points (int | list | tuple | str | dict | pd.DataFrame | slice) – Input specifying which sites to process. A single integer
+representing the supply curve GID of a site may be specified
+to evaluate reV at a supply curve point. A list or tuple
+of integers (or slice) representing the supply curve GIDs of
+multiple sites can be specified to evaluate reV at
+multiple specific locations. A string pointing to a project
+points CSV file may also be specified. Typically, the CSV
+contains the following columns:
+
+
+
gid: Integer specifying the supply curve GID of
+each site.
+
config: Key in the sam_files input dictionary
+(see below) corresponding to the SAM configuration to
+use for each particular site. This value can also be
+None (or left out completely) if you specify only
+a single SAM configuration file as the sam_files
+input.
+
+
+
The CSV file may also contain site-specific inputs by
+including a column named after a config keyword (e.g. a
+column called capital_cost may be included to specify a
+site-specific capital cost value for each location). Columns
+that do not correspond to a config key may also be included,
+but they will be ignored. The CSV file input can also have
+these extra, optional columns:
+
+
+
capital_cost_multiplier
+
fixed_operating_cost_multiplier
+
variable_operating_cost_multiplier
+
balance_of_system_cost_multiplier
+
+
+
These particular inputs are treated as multipliers to be
+applied to the respective cost curves
+(capital_cost_function, fixed_operating_cost_function,
+variable_operating_cost_function, and
+balance_of_system_cost_function) both during and
+after the optimization. A DataFrame following the same
+guidelines as the CSV input (or a dictionary that can be
+used to initialize such a DataFrame) may be used for this
+input as well. If you would like to obtain all available
+reV supply curve points to run, you can use the
+reV.supply_curve.extent.SupplyCurveExtent class
+like so:
+
importpandasaspd
+fromreV.supply_curve.extentimportSupplyCurveExtent
+
+excl_fpath="..."
+resolution=...
+withSupplyCurveExtent(excl_fpath,resolution)assc:
+ points=sc.valid_sc_points(tm_dset).tolist()
+ points=pd.DataFrame({"gid":points})
+ points["config"]="default"# or a list of config choices
+
+# Use the points directly or save them to csv for CLI usage
+points.to_csv("project_points.csv",index=False)
+
+
+
+
sam_files (dict | str) – A dictionary mapping SAM input configuration ID(s) to SAM
+configuration(s). Keys are the SAM config ID(s) which
+correspond to the config column in the project points
+CSV. Values for each key are either a path to a
+corresponding SAM config file or a full dictionary
+of SAM config inputs. For example:
This input can also be a string pointing to a single SAM
+config file. In this case, the config column of the
+CSV points input should be set to None or left out
+completely. See the documentation for the reV SAM class
+(e.g. reV.SAM.generation.WindPower,
+reV.SAM.generation.PvWattsv8,
+reV.SAM.generation.Geothermal, etc.) for
+info on the allowed and/or required SAM config file inputs.
+
+
min_spacing (float | int | str, optional) – Minimum spacing between turbines (in meters). This input can
+also be a string like “5x”, which is interpreted as 5 times
+the turbine rotor diameter. By default, "5x".
+
wake_loss_multiplier (float, optional) – A multiplier used to scale the annual energy lost due to
+wake losses.
+
+
Warning
+
This multiplier will ONLY be applied during the
+optimization process and will NOT come through in output
+values such as the hourly profiles, aep, any of the cost
+functions, or even the output objective.
+
+
By default, 1.
+
+
ga_kwargs (dict, optional) – Dictionary of keyword arguments to pass to GA
+initialization. If None, default initialization values
+are used. See
+GeneticAlgorithm for
+a description of the allowed keyword arguments.
+By default, None.
+
output_request (list | tuple, optional) – Outputs requested from the SAM windpower simulation after
+the bespoke plant layout optimization. Can be any of the
+parameters in the “Outputs” group of the PySAM module
+PySAM.Windpower.Windpower.Outputs, PySAM module.
+This list can also include a select number of SAM
+config/resource parameters to include in the output:
+any key in any of the
+output attribute JSON files
+may be requested. Time-series profiles requested via this
+input are output in UTC. This input can also be used to
+request resource means like "ws_mean",
+"windspeed_mean", "temperature_mean", and
+"pressure_mean". By default,
+('system_capacity','cf_mean').
+
ws_bins (tuple, optional) – A 3-entry tuple with (start,stop,step) for the
+windspeed binning of the wind joint probability
+distribution. The stop value is inclusive, so
+ws_bins=(0,20,5) would result in four bins with bin
+edges (0, 5, 10, 15, 20). By default, (0.0,20.0,5.0).
+
wd_bins (tuple, optional) – A 3-entry tuple with (start,stop,step) for the wind
+direction binning of the wind joint probability
+distribution. The stop value is inclusive, so
+wd_bins=(0,360,90) would result in four bins with bin
+edges (0, 90, 180, 270, 360).
+By default, (0.0,360.0,45.0).
+
excl_dict (dict, optional) – Dictionary of exclusion keyword arguments of the format
+{layer_dset_name:{kwarg:value}}, where
+layer_dset_name is a dataset in the exclusion h5 file
+and the kwarg:value pair is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+For example:
Note that all the keys given in this dictionary should be
+datasets of the excl_fpath file. If None or empty
+dictionary, no exclusions are applied. By default, None.
+
+
area_filter_kernel ({“queen”, “rook”}, optional) – Contiguous area filter method to use on final exclusions
+mask. The filters are defined as:
These filters define how neighboring pixels are “connected”.
+Once pixels in the final exclusion layer are connected, the
+area of each resulting cluster is computed and compared
+against the min_area input. Any cluster with an area
+less than min_area is excluded from the final mask.
+This argument has no effect if min_area is None.
+By default, "queen".
+
+
min_area (float, optional) – Minimum area (in km2) required to keep an isolated
+cluster of (included) land within the resulting exclusions
+mask. Any clusters of land with areas less than this value
+will be marked as exclusions. See the documentation for
+area_filter_kernel for an explanation of how the area of
+each land cluster is computed. If None, no area
+filtering is performed. By default, None.
+
resolution (int, optional) – Supply Curve resolution. This value defines how many pixels
+are in a single side of a supply curve cell. For example,
+a value of 64 would generate a supply curve where the
+side of each supply curve cell is 64x64 exclusion
+pixels. By default, 64.
+
excl_area (float, optional) – Area of a single exclusion mask pixel (in km2).
+If None, this value will be inferred from the profile
+transform attribute in excl_fpath. By default, None.
+
data_layers (dict, optional) –
+
Dictionary of aggregation data layers of the format:
The "output_layer_name" is the column name under which
+the aggregated data will appear in the meta DataFrame of the
+output file. The "output_layer_name" does not have to
+match the dset input value. The latter should match
+the layer name in the HDF5 from which the data to aggregate
+should be pulled. The method should be one of
+{"mode","mean","min","max","sum","category"},
+describing how the high-resolution data should be aggregated
+for each supply curve point. fpath is an optional key
+that can point to an HDF5 file containing the layer data. If
+left out, the data is assumed to exist in the file(s)
+specified by the excl_fpath input. If None, no data
+layer aggregation is performed. By default, None.
+
+
pre_extract_inclusions (bool, optional) – Optional flag to pre-extract/compute the inclusion mask from
+the excl_dict input. It is typically faster to compute
+the inclusion mask on the fly with parallel workers.
+By default, False.
+
eos_mult_baseline_cap_mw (int | float, optional) – Baseline plant capacity (MW) used to calculate economies of
+scale (EOS) multiplier from the capital_cost_function. EOS
+multiplier is calculated as the $-per-kW of the wind plant
+divided by the $-per-kW of a plant with this baseline
+capacity. By default, 200 (MW), which aligns the baseline
+with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
+
prior_run (str, optional) – Optional filepath to a bespoke output HDF5 file belonging to
+a prior run. If specified, this module will only run the
+timeseries power generation step and assume that all of the
+wind plant layouts are fixed from the prior run. The meta
+data of this file must contain the following columns
+(automatically satisfied if the HDF5 file was generated by
+reV bespoke):
+
+
+
capacity : Capacity of the plant, in MW.
+
turbine_x_coords: A string representation of a
+python list containing the X coordinates (in m; origin
+of cell at bottom left) of the turbines within the
+plant (supply curve cell).
+
turbine_y_coords : A string representation of a
+python list containing the Y coordinates (in m; origin
+of cell at bottom left) of the turbines within the
+plant (supply curve cell).
+
+
+
If None, no previous run data is considered.
+By default, None
+
+
gid_map (str | dict, optional) – Mapping of unique integer generation gids (keys) to single
+integer resource gids (values). This enables unique
+generation gids in the project points to map to non-unique
+resource gids, which can be useful when evaluating multiple
+resource datasets in reV (e.g., forecasted ECMWF
+resource data to complement historical WTK meteorology).
+This input can be a pre-extracted dictionary or a path to a
+JSON or CSV file. If this input points to a CSV file, the
+file must have the columns gid (which matches the
+project points) and gid_map (gids to extract from the
+resource input). If None, the GID values in the project
+points are assumed to match the resource GID values.
+By default, None.
+
bias_correct (str | pd.DataFrame, optional) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
+
+
pre_load_data (bool, optional) – Option to pre-load resource data. This step can be
+time-consuming up front, but it drastically reduces the
+number of parallel reads to the res_fpath HDF5 file(s),
+and can have a significant overall speedup on systems with
+slow parallel I/O capabilities. Pre-loaded data can use a
+significant amount of RAM, so be sure to split execution
+across many nodes (e.g. 100 nodes, 36 workers each for
+CONUS) or request large amounts of memory for a smaller
+number of nodes. By default, False.
Run the bespoke wind plant optimization in serial or parallel.
+
+
Parameters:
+
+
out_fpath (str, optional) – Path to output file. If None, no output file will
+be written. If the filepath is specified but the module name
+(bespoke) is not included, the module name will get added to
+the output file name. By default, None.
+
max_workers (int, optional) – Number of local workers to run on. If None, uses all
+available cores (typically 36). By default, None.
+
+
+
Returns:
+
str | None – Path to output HDF5 file, or None if results were not
+written to disk.
a simple genetic algorithm used to select bespoke turbine locations
+
+
Parameters:
+
+
bits (array of ints) – The number of bits assigned to each of the design variables.
+The number of discretizations for each design variables will be
+2^n where n is the number of bits assigned to that variable.
+
bounds (array of tuples) – The bounds for each design variable. This parameter looks like:
+np.array([(lower, upper), (lower, upper)…])
+
variable_type (array of strings (‘int’ or ‘float’)) – The type of each design variable (int or float).
+
objective_function (function handle for the objective that is to be) – minimized. Should take a single variable as an input which is a
+list/array of the design variables.
+
max_generation (int, optional) – The maximum number of generations that will be run in the genetic
+algorithm.
+
population_size (int, optional) – The population size in the genetic algorithm.
+
crossover_rate (float, optional) – The probability of crossover for a single bit during the crossover
+phase of the genetic algorithm.
+
mutation_rate (float, optional) – The probability of a single bit mutating during the mutation phase
+of the genetic algorithm.
+
tol (float, optional) – The absolute tolerance to determine convergence.
+
convergence_iters (int, optional) – The number of generations to determine convergence.
+
max_time (float) – The maximum time (in seconds) to run the genetic algorithm.
Framework to maximize plant capacity in a provided wind plant area.
+
+
Parameters:
+
+
min_spacing (float) – The minimum allowed spacing between wind turbines.
+
safe_polygons (Polygon | MultiPolygon) – The “safe” area(s) where turbines can be placed without
+violating boundary, setback, exclusion, or other constraints.
Fast packing algorithm that maximizes plant capacity in a
+provided wind plant area. Sets the the optimal locations to
+self.turbine_x and self.turbine_y
Framework for optimizing turbine locations for site specific
+exclusions, wind resources, and objective
+
+
Parameters:
+
+
wind_plant (WindPowerPD) – wind plant object to analyze wind plant performance. This
+object should have everything in the plant defined, such
+that only the turbine coordinates and plant capacity need to
+be defined during the optimization.
+
objective_function (str) – The objective function of the optimization as a string,
+should return the objective to be minimized during layout
+optimization. Variables available are:
+
+
+
n_turbines: the number of turbines
+
system_capacity: wind plant capacity
+
aep: annual energy production
+
avg_sl_dist_to_center_m: Average straight-line
+distance to the supply curve point center from all
+turbine locations (in m). Useful for computing plant
+BOS costs.
+
avg_sl_dist_to_medoid_m: Average straight-line
+distance to the medoid of all turbine locations
+(in m). Useful for computing plant BOS costs.
+
nn_conn_dist_m: Total BOS connection distance
+using nearest-neighbor connections. This variable is
+only available for the
+balance_of_system_cost_function equation.
+
fixed_charge_rate: user input fixed_charge_rate if
+included as part of the sam system config.
+
capital_cost: plant capital cost as evaluated
+by capital_cost_function
+
fixed_operating_cost: plant fixed annual operating
+cost as evaluated by fixed_operating_cost_function
+
variable_operating_cost: plant variable annual
+operating cost as evaluated by
+variable_operating_cost_function
+
balance_of_system_cost: plant balance of system
+cost as evaluated by balance_of_system_cost_function
+
self.wind_plant: the SAM wind plant object,
+through which all SAM variables can be accessed
+
+
+
+
capital_cost_function (str) – The plant capital cost function as a string, must return the
+total capital cost in $. Has access to the same variables as
+the objective_function.
+
fixed_operating_cost_function (str) – The plant annual fixed operating cost function as a string,
+must return the fixed operating cost in $/year. Has access
+to the same variables as the objective_function.
+
variable_operating_cost_function (str) – The plant annual variable operating cost function as a
+string, must return the variable operating cost in $/kWh.
+Has access to the same variables as the objective_function.
+You can set this to “0” to effectively ignore variable
+operating costs.
+
balance_of_system_cost_function (str) – The plant balance-of-system cost function as a string, must
+return the variable operating cost in $. Has access to the
+same variables as the objective_function. You can set this
+to “0” to effectively ignore balance-of-system costs.
+
include_mask (np.ndarray) – Supply curve point 2D inclusion mask where included pixels
+are set to 1 and excluded pixels are set to 0.
+
pixel_side_length (int) – Side length (m) of a single pixel of the include_mask.
+
min_spacing (float) – The minimum spacing between turbines (in meters).
+
wake_loss_multiplier (float, optional) – A multiplier used to scale the annual energy lost due to
+wake losses. IMPORTANT: This multiplier will ONLY be
+applied during the optimization process and will NOT be
+come through in output values such as aep, any of the cost
+functions, or even the output objective.
Run the turbine packing algorithm (maximizing plant capacity) to define potential turbine locations that will be used as design variables in the gentic algorithm.
Run the turbine packing algorithm (maximizing plant capacity) to
+define potential turbine locations that will be used as design
+variables in the gentic algorithm.
Capital cost function ($ per kW) evaluated for a given capacity.
+
The capacity will be adjusted to be an exact multiple of the
+turbine rating in order to yield an integer number of
+turbines.
+
+
Parameters:
+
capacity_mw (float) – The desired capacity (MW) to sample the cost curve at. Note
+as mentioned above, the capacity will be adjusted to be an
+exact multiple of the turbine rating in order to yield an
+integer number of turbines. For best results, set this
+value to be an integer multiple of the turbine rating.
+
+
Returns:
+
capital_cost (float) – Capital cost ($ per kW) for the (adjusted) plant capacity.
Decorator that returns None until PlaceTurbines is optimized.
+
Meant for exclusive use in PlaceTurbines and its subclasses.
+PlaceTurbines is considered optimized when its
+optimized_design_variables attribute is not None.
+
+
Parameters:
+
func (callable) – A callable function that should return None until
+PlaceTurbines is optimized.
+
+
Returns:
+
callable – New function that returns None until PlaceTurbines is
+optimized.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
analysis_years (list) – List of years to analyze. If this is a single year run, this return
+value is a single entry list. If no analysis_years are specified,
+the code will look anticipate a year in the input files.
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
curtailment_parameters (str | dict) – Configuration json file (with path) containing curtailment
+information. Could also be a pre-extracted curtailment config
+dictionary (the contents of the curtailment json).
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
Get the date range tuple (start, end) over which curtailment is
+possible (inclusive, exclusive) (“MMDD”, “MMDD”). This overrides the
+months input.
+
+
Returns:
+
date_range (tuple) – Two-entry tuple of the starting date (inclusive) and ending date
+(exclusive) over which curtailment is possible. Input format is a
+zero-padded string: “MMDD”.
Get the precip rate (mm/hour) under which curtailment is possible.
+
+
Returns:
+
precipitation (float | NoneType) – Precipitation rate under which curtailment is possible. This is
+compared to the WTK resource dataset “precipitationrate_0m” in
+mm/hour. Defaults to None.
equation (str) – A python equation based on other curtailment variables (wind_speed,
+temperature, precipitation_rate, solar_zenith_angle) that returns
+a True or False output to signal curtailment.
Get the probability that curtailment is in-effect if all other
+screening criteria are met.
+
+
Returns:
+
probability (float) – Fractional probability that curtailment is in-effect if all other
+screening criteria are met. Defaults to 1 (curtailment is always
+in effect if all other criteria are met).
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
_split_range (list) – Two-entry list that indicates the starting and finishing
+(inclusive, exclusive, respectively) indices of a split instance
+of the PointsControl object. This is set in the iterator dunder
+methods of PointsControl.
points (int | slice | list | tuple | str | pd.DataFrame | dict) – Slice specifying project points, string pointing to a project
+points csv, or a dataframe containing the effective csv contents.
+Can also be a single integer site value.
+
sam_configs (dict | str | SAMConfig) – SAM input configuration ID(s) and file path(s). Keys are the SAM
+config ID(s) which map to the config column in the project points
+CSV. Values are either a JSON SAM config file or dictionary of SAM
+config inputs. Can also be a single config file path or a
+pre loaded SAMConfig object.
+
tech (str, optional) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam)
+The string should be lower-cased with spaces and _ removed,
+by default None
+
res_file (str | NoneType) – Optional resource file to find maximum length of project points if
+points slice stop is None.
+
curtailment (NoneType | dict | str | config.curtailment.Curtailment) – Inputs for curtailment parameters. If not None, curtailment inputs
+are expected. Can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config json file with path (str)
+
Instance of curtailment config object
+(config.curtailment.Curtailment)
dict – Multi-level dictionary containing multiple SAM input config files.
+The top level key is the SAM config ID, top level value is the SAM
+config file path
Get the SAM configuration inputs dictionary property.
+
+
Returns:
+
dict – Multi-level dictionary containing multiple SAM input
+configurations. The top level key is the SAM config ID, top level
+value is the SAM config. Each SAM config is a dictionary with keys
+equal to input names, values equal to the actual inputs.
Get a list of unique input keys from all SAM technology configs.
+
+
Returns:
+
all_sam_input_keys (list) – List of unique strings where each string is a input key for the
+SAM technology configs. For example, “gcr” or “losses” for PVWatts
+or “wind_turbine_hub_ht” for windpower.
sites_as_slice (list | slice) – Sites slice belonging to this instance of ProjectPoints.
+The type is slice if possible. Will be a list only if sites are
+non-sequential.
_tech (str) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam)
+The string should be lower-cased with spaces and _ removed.
Get the depths (m) corresponding to the site list.
+
+
Returns:
+
_d (list | NoneType) – Resource depths (m) corresponding to each site, taken from
+the sam config for each site. This is None if the technology
+is not geothermal.
_curtailment (NoneType | reV.config.curtailment.Curtailment) – None if no curtailment, reV curtailment config object if
+curtailment is being assessed.
Join new df2 to the _df attribute using the _df’s gid as pkey.
+
This can be used to add site-specific data to the project_points,
+taking advantage of the points_control iterator/split functions such
+that only the relevant site data is passed to the analysis functions.
+
+
Parameters:
+
+
df2 (pd.DataFrame) – Dataframe to be joined to the self._df attribute (this instance
+of project points dataframe). This likely contains
+site-specific inputs that are to be passed to parallel workers.
+
key (str) – Primary key of df2 to be joined to the _df attribute (this
+instance of the project points dataframe). Primary key
+of the self._df attribute is fixed as the gid column.
Return split instance of a ProjectPoints instance w/ site subset.
+
+
Parameters:
+
+
i0 (int) – Starting INDEX (not resource gid) (inclusive) of the site property
+attribute to include in the split instance. This is not necessarily
+the same as the starting site number, for instance if ProjectPoints
+is sites 20:100, i0=0 i1=10 will result in sites 20:30.
+
i1 (int) – Ending INDEX (not resource gid) (exclusive) of the site property
+attribute to include in the split instance. This is not necessarily
+the same as the final site number, for instance if ProjectPoints is
+sites 20:100, i0=0 i1=10 will result in sites 20:30.
+
project_points (ProjectPoints) – Instance of project points to split.
+
+
+
Returns:
+
sub (ProjectPoints) – New instance of ProjectPoints with a subset of the following
+attributes: sites, project points df, and the self dictionary data
+struct.
Generate ProjectPoints for gids nearest to given latitude longitudes
+
+
Parameters:
+
+
lat_lons (str | tuple | list | ndarray) – Pair or pairs of latitude longitude coordinates
+
res_file (str) – Resource file, needed to fine nearest neighbors
+
sam_configs (dict | str | SAMConfig) – SAM input configuration ID(s) and file path(s). Keys are the SAM
+config ID(s) which map to the config column in the project points
+CSV. Values are either a JSON SAM config file or dictionary of SAM
+config inputs. Can also be a single config file path or a
+pre loaded SAMConfig object.
+
tech (str, optional) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam)
+The string should be lower-cased with spaces and _ removed,
+by default None
+
curtailment (NoneType | dict | str | config.curtailment.Curtailment) – Inputs for curtailment parameters. If not None, curtailment inputs
+are expected. Can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config json file with path (str)
+
Instance of curtailment config object
+(config.curtailment.Curtailment)
+
+
+
+
+
+
Returns:
+
pp (ProjectPoints) – Initialized ProjectPoints object for points nearest to given
+lat_lons
Generate ProjectPoints for gids nearest to given latitude longitudes
+
+
Parameters:
+
+
regions (dict) – Dictionary of regions to extract points for in the form:
+{‘region’: ‘region_column’}
+
res_file (str) – Resource file, needed to fine nearest neighbors
+
sam_configs (dict | str | SAMConfig) – SAM input configuration ID(s) and file path(s). Keys are the SAM
+config ID(s) which map to the config column in the project points
+CSV. Values are either a JSON SAM config file or dictionary of SAM
+config inputs. Can also be a single config file path or a
+pre loaded SAMConfig object.
+
tech (str, optional) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam)
+The string should be lower-cased with spaces and _ removed,
+by default None
+
curtailment (NoneType | dict | str | config.curtailment.Curtailment) – Inputs for curtailment parameters. If not None, curtailment inputs
+are expected. Can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config json file with path (str)
+
Instance of curtailment config object
+(config.curtailment.Curtailment)
+
+
+
+
+
+
Returns:
+
pp (ProjectPoints) – Initialized ProjectPoints object for points nearest to given
+lat_lons
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]
Get a boolean for whether solar resource requires clearsky irrad.
+
+
Returns:
+
clearsky (bool) – Flag set in the SAM config input with key “clearsky” for solar
+analysis to process generation for clearsky irradiance.
+Defaults to False (normal all-sky irradiance).
Get a boolean for whether bifacial solar analysis is being run.
+
+
Returns:
+
bifacial (bool) – Flag set in the SAM config input with key “bifaciality” for solar
+analysis to analyze bifacial PV panels. Will require albedo input.
+Defaults to False (no bifacial panels is default).
dict | None – Option for NSRDB resource downscaling to higher temporal
+resolution. The config expects a str entry in the Pandas
+frequency format, e.g. ‘5min’ or a dict of downscaling kwargs
+such as {‘frequency’: ‘5min’, ‘variability_kwargs’:
+{‘var_frac’: 0.05, ‘distribution’: ‘uniform’}}.
+A str entry will be converted to a kwarg dict for the output
+of this property e.g. ‘5min’ -> {‘frequency’: ‘5min’}
Resolve a file path represented by the input string.
+
This function resolves the input string if it resembles a path.
+Specifically, the string will be resolved if it starts with
+“./” or “..”, or it if it contains either “./” or
+“..” somewhere in the string body. Otherwise, the string
+is returned unchanged, so this function is safe to call on any
+string, even ones that do not resemble a path.
+
This method delegates the “resolving” logic to
+pathlib.Path.resolve(). This means the path is made
+absolute, symlinks are resolved, and “..” components are
+eliminated. If the path input starts with “./” or
+“..”, it is assumed to be w.r.t the config directory, not
+the run directory.
If E is present and has a .keys() method, then does: for k in E: D[k] = E[k]
+If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v
+In either case, this is followed by: for k in F: D[k] = F[k]
reV econ analysis runs SAM econ calculations, typically to
+compute LCOE (using PySAM.Lcoefcr.Lcoefcr), though
+PySAM.Singleowner.Singleowner or
+PySAM.Windbos.Windbos calculations can also be
+performed simply by requesting outputs from those computation
+modules. See the keys of
+Econ.OPTIONS for all
+available econ outputs. Econ computations rely on an input a
+generation (i.e. capacity factor) profile. You can request
+reV to run the analysis for one or more “sites”, which
+correspond to the meta indices in the generation data.
+
+
Parameters:
+
+
project_points (int | list | tuple | str | dict | pd.DataFrame | slice) – Input specifying which sites to process. A single integer
+representing the GID of a site may be specified to evaluate
+reV at a single location. A list or tuple of integers
+(or slice) representing the GIDs of multiple sites can be
+specified to evaluate reV at multiple specific locations.
+A string pointing to a project points CSV file may also be
+specified. Typically, the CSV contains the following
+columns:
+
+
+
gid: Integer specifying the generation GID of each
+site.
+
config: Key in the sam_files input dictionary
+(see below) corresponding to the SAM configuration to
+use for each particular site. This value can also be
+None (or left out completely) if you specify only
+a single SAM configuration file as the sam_files
+input.
+
capital_cost_multiplier: This is an optional
+multiplier input that, if included, will be used to
+regionally scale the capital_cost input in the SAM
+config. If you include this column in your CSV, you
+do not need to specify capital_cost, unless you
+would like that value to vary regionally and
+independently of the multiplier (i.e. the multiplier
+will still be applied on top of the capital_cost
+input).
+
+
+
The CSV file may also contain other site-specific inputs by
+including a column named after a config keyword (e.g. a
+column called wind_turbine_rotor_diameter may be
+included to specify a site-specific turbine diameter for
+each location). Columns that do not correspond to a config
+key may also be included, but they will be ignored. A
+DataFrame following the same guidelines as the CSV input
+(or a dictionary that can be used to initialize such a
+DataFrame) may be used for this input as well.
+
+
sam_files (dict | str) – A dictionary mapping SAM input configuration ID(s) to SAM
+configuration(s). Keys are the SAM config ID(s) which
+correspond to the config column in the project points
+CSV. Values for each key are either a path to a
+corresponding SAM config file or a full dictionary
+of SAM config inputs. For example:
This input can also be a string pointing to a single SAM
+config file. In this case, the config column of the
+CSV points input should be set to None or left out
+completely. See the documentation for the reV SAM class
+(e.g. reV.SAM.generation.WindPower,
+reV.SAM.generation.PvWattsv8,
+reV.SAM.generation.Geothermal, etc.) for
+documentation on the allowed and/or required SAM config file
+inputs.
+
+
cf_file (str) – Path to reV output generation file containing a capacity
+factor output.
+
+
Note
+
If executing reV from the command line, this
+path can contain brackets {} that will be filled in
+by the analysis_years input. Alternatively, this input
+can be set to "PIPELINE" to parse the output of the
+previous step (reV generation) and use it as input to
+this call. However, note that duplicate executions of
+reV generation within the pipeline may invalidate this
+parsing, meaning the cf_file input will have to be
+specified manually.
+
+
+
site_data (str | pd.DataFrame, optional) – Site-specific input data for SAM calculation. If this input
+is a string, it should be a path that points to a CSV file.
+Otherwise, this input should be a DataFrame with
+pre-extracted site data. Rows in this table should match
+the input sites via a gid column. The rest of the
+columns should match configuration input keys that will take
+site-specific values. Note that some or all site-specific
+inputs can be specified via the project_points input
+table instead. If None, no site-specific data is
+considered. By default, None.
+
output_request (list | tuple, optional) – List of output variables requested from SAM. Can be any
+of the parameters in the “Outputs” group of the PySAM module
+(e.g. PySAM.Windpower.Windpower.Outputs,
+PySAM.Pvwattsv8.Pvwattsv8.Outputs,
+PySAM.Geothermal.Geothermal.Outputs, etc.) being
+executed. This list can also include a select number of SAM
+config/resource parameters to include in the output:
+any key in any of the
+output attribute JSON files
+may be requested. Time-series profiles requested via this
+input are output in UTC. By default, ('lcoe_fcr',).
+
sites_per_worker (int, optional) – Number of sites to run in series on a worker. None
+defaults to the resource file chunk size.
+By default, None.
+
memory_utilization_limit (float, optional) – Memory utilization limit (fractional). Must be a value
+between 0 and 1. This input sets how many site results will
+be stored in-memory at any given time before flushing to
+disk. By default, 0.4.
+
append (bool) – Option to append econ datasets to source cf_file.
+By default, False.
points (slice | list | str | reV.config.project_points.PointsControl) – Slice specifying project points, or string pointing to a project
+points csv, or a fully instantiated PointsControl object.
+
points_range (list | None) – Optional two-entry list specifying the index range of the sites to
+analyze. To be taken from the reV.config.PointsControl.split_range
+property.
+
sam_configs (dict | str | SAMConfig) – SAM input configuration ID(s) and file path(s). Keys are the SAM
+config ID(s) which map to the config column in the project points
+CSV. Values are either a JSON SAM config file or dictionary of SAM
+config inputs. Can also be a single config file path or a
+pre loaded SAMConfig object.
Flush the output data in self.out attribute to disk in .h5 format.
+
The data to be flushed is accessed from the instance attribute
+“self.out”. The disk target is based on the instance attributes
+“self._out_fpath”. Data is not flushed if _fpath is None or if .out is
+empty.
Get the nominal sites per worker (x-chunk size) for a given file.
+
This is based on the concept that it is most efficient for one core to
+perform one read on one chunk of resource data, such that chunks will
+not have to be read into memory twice and no sites will be read
+redundantly.
+
+
Parameters:
+
+
res_file (str) – Filepath to single resource file, multi-h5 directory,
+or /h5_dir/prefix*suffix
+
default (int) – Sites to be analyzed on a single core if the chunk size cannot be
+determined from res_file.
+
+
+
Returns:
+
sites_per_worker (int) – Nominal sites to be analyzed per worker. This is set to the x-axis
+chunk size for windspeed and dni datasets for the WTK and NSRDB
+data, respectively.
Execute a parallel reV econ run with smart data flushing.
+
+
Parameters:
+
+
out_fpath (str, optional) – Path to output file. If this class was initialized with
+append=True, this input has no effect. If None, no
+output file will be written. If the filepath is specified
+but the module name (econ) and/or resource data year is not
+included, the module name and/or resource data year will get
+added to the output file name. By default, None.
+
max_workers (int, optional) – Number of local workers to run on. By default, 1.
+
timeout (int, optional) – Number of seconds to wait for parallel run iteration to
+complete before returning zeros. By default, 1800
+seconds.
+
pool_size (int, optional) – Number of futures to submit to a single process pool for
+parallel futures. If None, the pool size is set to
+os.cpu_count()*2. By default, None.
+
+
+
Returns:
+
str | None – Path to output HDF5 file, or None if results were not
+written to disk.
site_gid (int) – Resource-native site index (gid).
+
out_index (bool) – Option to get output index (if true) which is the column index in
+the current in-memory output array, or (if false) the global site
+index from the project points site list.
+
+
+
Returns:
+
index (int) – Global site index if out_index=False, otherwise column index in
+the current in-memory output array.
tech (str) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam, econ)
+The string should be lower-cased with spaces and _ removed.
eqn (str) – LCOE scaling equation to implement “economies of scale”.
+Equation must be in python string format and return a scalar
+value to multiply the capital cost by. Independent variables in
+the equation should match the keys in the data input arg. This
+equation may use numpy functions with the package prefix “np”.
+
data (dict | pd.DataFrame) – Namespace of econ data to use to calculate economies of scale. Keys
+in dict or column labels in dataframe should match the Independent
+variables in the eqn input. Should also include variables required
+to calculate LCOE.
Get a list of variable names that the EconomiesOfScale equation
+uses as input.
+
+
Returns:
+
vars (list) – List of strings representing variable names that were parsed from
+the equation string. This will return an empty list if the equation
+has no variables.
Base class for reV gen and econ classes to run SAM simulations.
+
+
Parameters:
+
+
points_control (reV.config.project_points.PointsControl) – Project points control instance for site and SAM config spec.
+
output_request (list | tuple) – Output variables requested from SAM.
+
site_data (str | pd.DataFrame | None) – Site-specific input data for SAM calculation. String should be a
+filepath that points to a csv, DataFrame is pre-extracted data.
+Rows match sites, columns are input keys. Need a “gid” column.
+Input as None if no site-specific data.
+
drop_leap (bool) – Drop leap day instead of final day of year during leap years.
+
memory_utilization_limit (float) – Memory utilization limit (fractional). This sets how many site
+results will be stored in-memory at any given time before flushing
+to disk.
+
scale_outputs (bool) – Flag to scale outputs in-place immediately upon Gen returning data.
Get resource meta for all sites in project points.
+
+
Returns:
+
meta (pd.DataFrame) – Meta data df for sites in project points. Column names are meta
+data variables, rows are different sites. The row index
+does not indicate the site number if the project points are
+non-sequential or do not start from 0, so a SiteDataField.GID
+column is added.
tech (str) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam, econ)
+The string should be lower-cased with spaces and _ removed.
points (int | slice | list | str | pandas.DataFrame | PointsControl) – Single site integer,
+or slice or list specifying project points,
+or string pointing to a project points csv,
+or a pre-loaded project points DataFrame,
+or a fully instantiated PointsControl object.
+
points_range (list | None) – Optional two-entry list specifying the index range of the sites to
+analyze. To be taken from the reV.config.PointsControl.split_range
+property.
+
sam_configs (dict | str | SAMConfig) – SAM input configuration ID(s) and file path(s). Keys are the SAM
+config ID(s) which map to the config column in the project points
+CSV. Values are either a JSON SAM config file or dictionary of SAM
+config inputs. Can also be a single config file path or a
+pre loaded SAMConfig object.
+
tech (str) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam)
+The string should be lower-cased with spaces and _ removed.
+
sites_per_worker (int) – Number of sites to run in series on a worker. None defaults to the
+resource file chunk size.
+
res_file (str) – Filepath to single resource file, multi-h5 directory,
+or /h5_dir/prefix*suffix
+
curtailment (NoneType | dict | str | config.curtailment.Curtailment) – Inputs for curtailment parameters. If not None, curtailment inputs
+are expected. Can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config json file with path (str)
+
Instance of curtailment config object
+(config.curtailment.Curtailment)
+
+
+
+
+
+
Returns:
+
pc (reV.config.project_points.PointsControl) – PointsControl object instance.
Get the nominal sites per worker (x-chunk size) for a given file.
+
This is based on the concept that it is most efficient for one core to
+perform one read on one chunk of resource data, such that chunks will
+not have to be read into memory twice and no sites will be read
+redundantly.
+
+
Parameters:
+
+
res_file (str) – Filepath to single resource file, multi-h5 directory,
+or /h5_dir/prefix*suffix
+
default (int) – Sites to be analyzed on a single core if the chunk size cannot be
+determined from res_file.
+
+
+
Returns:
+
sites_per_worker (int) – Nominal sites to be analyzed per worker. This is set to the x-axis
+chunk size for windspeed and dni datasets for the WTK and NSRDB
+data, respectively.
site_gid (int) – Resource-native site index (gid).
+
out_index (bool) – Option to get output index (if true) which is the column index in
+the current in-memory output array, or (if false) the global site
+index from the project points site list.
+
+
+
Returns:
+
index (int) – Global site index if out_index=False, otherwise column index in
+the current in-memory output array.
Flush the output data in self.out attribute to disk in .h5 format.
+
The data to be flushed is accessed from the instance attribute
+“self.out”. The disk target is based on the instance attributes
+“self._out_fpath”. Data is not flushed if _fpath is None or if .out is
+empty.
reV generation analysis runs SAM simulations by piping in
+renewable energy resource data (usually from the NSRDB or WTK),
+loading the SAM config, and then executing the PySAM compute
+module for a given technology. See the documentation for the
+reV SAM class (e.g. reV.SAM.generation.WindPower,
+reV.SAM.generation.PvWattsv8,
+reV.SAM.generation.Geothermal, etc.) for info on the
+allowed and/or required SAM config file inputs. If economic
+parameters are supplied in the SAM config, then you can bundle a
+“follow-on” econ calculation by just adding the desired econ
+output keys to the output_request. You can request reV to
+run the analysis for one or more “sites”, which correspond to
+the meta indices in the resource data (also commonly called the
+gid's).
+
Examples
+
The following is an example of the most simple way to run reV
+generation. Note that the TESTDATADIR refers to the local cloned
+repository and will need to be replaced with a valid path if you
+installed reV via a simple pip install.
technology (str) – String indicating which SAM technology to analyze. Must be
+one of the keys of
+OPTIONS. The string
+should be lower-cased with spaces and underscores removed.
+
project_points (int | list | tuple | str | dict | pd.DataFrame | slice) – Input specifying which sites to process. A single integer
+representing the generation GID of a site may be specified
+to evaluate reV at a single location. A list or tuple of
+integers (or slice) representing the generation GIDs of
+multiple sites can be specified to evaluate reV at multiple
+specific locations. A string pointing to a project points
+CSV file may also be specified. Typically, the CSV contains
+the following columns:
+
+
+
gid: Integer specifying the generation GID of each
+site.
+
config: Key in the sam_files input dictionary
+(see below) corresponding to the SAM configuration to
+use for each particular site. This value can also be
+None (or left out completely) if you specify only
+a single SAM configuration file as the sam_files
+input.
+
capital_cost_multiplier: This is an optional
+multiplier input that, if included, will be used to
+regionally scale the capital_cost input in the SAM
+config. If you include this column in your CSV, you
+do not need to specify capital_cost, unless you
+would like that value to vary regionally and
+independently of the multiplier (i.e. the multiplier
+will still be applied on top of the capital_cost
+input).
+
+
+
The CSV file may also contain other site-specific inputs by
+including a column named after a config keyword (e.g. a
+column called wind_turbine_rotor_diameter may be
+included to specify a site-specific turbine diameter for
+each location). Columns that do not correspond to a config
+key may also be included, but they will be ignored. A
+DataFrame following the same guidelines as the CSV input
+(or a dictionary that can be used to initialize such a
+DataFrame) may be used for this input as well.
+
+
Note
+
By default, the generation GID of each site is
+assumed to match the resource GID to be evaluated for that
+site. However, unique generation GID’s can be mapped to
+non-unique resource GID’s via the gid_map input (see the
+documentation for gid_map for more details).
+
+
+
sam_files (dict | str) – A dictionary mapping SAM input configuration ID(s) to SAM
+configuration(s). Keys are the SAM config ID(s) which
+correspond to the config column in the project points
+CSV. Values for each key are either a path to a
+corresponding SAM config file or a full dictionary
+of SAM config inputs. For example:
This input can also be a string pointing to a single SAM
+config file. In this case, the config column of the
+CSV points input should be set to None or left out
+completely. See the documentation for the reV SAM class
+(e.g. reV.SAM.generation.WindPower,
+reV.SAM.generation.PvWattsv8,
+reV.SAM.generation.Geothermal, etc.) for
+info on the allowed and/or required SAM config file inputs.
+
+
resource_file (str) – Filepath to resource data. This input can be path to a
+single resource HDF5 file or a path including a wildcard
+input like /h5_dir/prefix*suffix (i.e. if your datasets
+for a single year are spread out over multiple files). In
+all cases, the resource data must be readable by
+rex.resource.Resource
+or rex.multi_file_resource.MultiFileResource.
+(i.e. the resource data conform to the
+rex data format). This
+means the data file(s) must contain a 1D time_index
+dataset indicating the UTC time of observation, a 1D
+meta dataset represented by a DataFrame with
+site-specific columns, and 2D resource datasets that match
+the dimensions of (time_index, meta). The time index
+must start at 00:00 of January 1st of the year under
+consideration, and its shape must be a multiple of 8760.
+
+
Note
+
If executing reV from the command line, this
+input string can contain brackets {} that will be
+filled in by the analysis_years input. Alternatively,
+this input can be a list of explicit files to process. In
+this case, the length of the list must match the length of
+the analysis_years input exactly, and the path are
+assumed to align with the analysis_years (i.e. the first
+path corresponds to the first analysis year, the second
+path corresponds to the second analysis year, and so on).
+
+
+
Important
+
If you are using custom resource data (i.e.
+not NSRDB/WTK/Sup3rCC, etc.), ensure the following:
The meta DataFrame is organized such that every
+row is a pixel and at least the columns
+latitude, longitude, timezone, and
+elevation are given for each location.
+
The time index and associated temporal data is in
+UTC.
+
The latitude is between -90 and 90 and longitude is
+between -180 and 180.
+
For solar data, ensure the DNI/DHI are not zero. You
+can calculate one of these these inputs from the
+other using the relationship
+
+\[GHI = DNI * cos(SZA) + DHI\]
+
+
+
+
+
+
low_res_resource_file (str, optional) – Optional low resolution resource file that will be
+dynamically mapped+interpolated to the nominal-resolution
+resource_file. This needs to be of the same format as
+resource_file - both files need to be handled by the
+same rexResource handler (e.g. WindResource). All
+of the requirements from the resource_file apply to this
+input as well. If None, no dynamic mapping to higher
+resolutions is performed. By default, None.
+
output_request (list | tuple, optional) – List of output variables requested from SAM. Can be any
+of the parameters in the “Outputs” group of the PySAM module
+(e.g. PySAM.Windpower.Windpower.Outputs,
+PySAM.Pvwattsv8.Pvwattsv8.Outputs,
+PySAM.Geothermal.Geothermal.Outputs, etc.) being
+executed. This list can also include a select number of SAM
+config/resource parameters to include in the output:
+any key in any of the
+output attribute JSON files
+may be requested. If cf_mean is not included in this
+list, it will automatically be added. Time-series profiles
+requested via this input are output in UTC.
+
+
Note
+
If you are performing reV solar runs using
+PVWatts and would like reV to include AC capacity
+values in your aggregation/supply curves, then you must
+include the "dc_ac_ratio" time series as an output in
+output_request when running reV generation. The AC
+capacity outputs will automatically be added during the
+aggregation/supply curve step if the "dc_ac_ratio"
+dataset is detected in the generation file.
+
+
By default, ('cf_mean',).
+
+
site_data (str | pd.DataFrame, optional) – Site-specific input data for SAM calculation. If this input
+is a string, it should be a path that points to a CSV file.
+Otherwise, this input should be a DataFrame with
+pre-extracted site data. Rows in this table should match
+the input sites via a gid column. The rest of the
+columns should match configuration input keys that will take
+site-specific values. Note that some or all site-specific
+inputs can be specified via the project_points input
+table instead. If None, no site-specific data is
+considered.
+
+
Note
+
This input is often used to provide site-based
+regional capital cost multipliers. reV does not
+ingest multipliers directly; instead, this file is
+expected to have a capital_cost column that gives the
+multiplier-adjusted capital cost value for each location.
+Therefore, you must re-create this input file every
+time you change your base capital cost assumption.
+
+
By default, None.
+
+
curtailment (dict | str, optional) –
+
Inputs for curtailment parameters, which can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config file with path (str)
+
+
+
The allowed key-value input pairs in the curtailment
+configuration are documented as properties of the
+reV.config.curtailment.Curtailment class. If
+None, no curtailment is modeled. By default, None.
+
+
gid_map (dict | str, optional) – Mapping of unique integer generation gids (keys) to single
+integer resource gids (values). This enables unique
+generation gids in the project points to map to non-unique
+resource gids, which can be useful when evaluating multiple
+resource datasets in reV (e.g., forecasted ECMWF
+resource data to complement historical WTK meteorology).
+This input can be a pre-extracted dictionary or a path to a
+JSON or CSV file. If this input points to a CSV file, the
+file must have the columns gid (which matches the
+project points) and gid_map (gids to extract from the
+resource input). If None, the GID values in the project
+points are assumed to match the resource GID values.
+By default, None.
+
drop_leap (bool, optional) – Drop leap day instead of final day of year when handling
+leap years. By default, False.
+
sites_per_worker (int, optional) – Number of sites to run in series on a worker. None
+defaults to the resource file chunk size.
+By default, None.
+
memory_utilization_limit (float, optional) – Memory utilization limit (fractional). Must be a value
+between 0 and 1. This input sets how many site results will
+be stored in-memory at any given time before flushing to
+disk. By default, 0.4.
+
scale_outputs (bool, optional) – Flag to scale outputs in-place immediately upon Gen
+returning data. By default, True.
+
write_mapped_gids (bool, optional) – Option to write mapped gids to output meta instead of
+resource gids. By default, False.
+
bias_correct (str | pd.DataFrame, optional) – Optional DataFrame or CSV filepath to a wind or solar
+resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless
+of the optional gid_map input. Only windspeedor
+GHI + DNI + DHI are corrected, depending on the
+technology (wind for the former, PV or CSP for the latter). See the
+functions in the rex.bias_correction module for available
+inputs for method. Any additional kwargs required for the
+requested method can be input as additional columns in the
+bias_correct table e.g., for linear bias correction functions
+you can include scalar and adder inputs as columns in the
+bias_correct table on a site-by-site basis. If None, no
+corrections are applied. By default, None.
Get resource meta for all sites in project points.
+
+
Returns:
+
meta (pd.DataFrame) – Meta data df for sites in project points. Column names are meta
+data variables, rows are different sites. The row index
+does not indicate the site number if the project points are
+non-sequential or do not start from 0, so a SiteDataField.GID
+column is added.
Flush the output data in self.out attribute to disk in .h5 format.
+
The data to be flushed is accessed from the instance attribute
+“self.out”. The disk target is based on the instance attributes
+“self._out_fpath”. Data is not flushed if _fpath is None or if .out is
+empty.
points (int | slice | list | str | pandas.DataFrame | PointsControl) – Single site integer,
+or slice or list specifying project points,
+or string pointing to a project points csv,
+or a pre-loaded project points DataFrame,
+or a fully instantiated PointsControl object.
+
points_range (list | None) – Optional two-entry list specifying the index range of the sites to
+analyze. To be taken from the reV.config.PointsControl.split_range
+property.
+
sam_configs (dict | str | SAMConfig) – SAM input configuration ID(s) and file path(s). Keys are the SAM
+config ID(s) which map to the config column in the project points
+CSV. Values are either a JSON SAM config file or dictionary of SAM
+config inputs. Can also be a single config file path or a
+pre loaded SAMConfig object.
+
tech (str) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam)
+The string should be lower-cased with spaces and _ removed.
+
sites_per_worker (int) – Number of sites to run in series on a worker. None defaults to the
+resource file chunk size.
+
res_file (str) – Filepath to single resource file, multi-h5 directory,
+or /h5_dir/prefix*suffix
+
curtailment (NoneType | dict | str | config.curtailment.Curtailment) – Inputs for curtailment parameters. If not None, curtailment inputs
+are expected. Can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config json file with path (str)
+
Instance of curtailment config object
+(config.curtailment.Curtailment)
+
+
+
+
+
+
Returns:
+
pc (reV.config.project_points.PointsControl) – PointsControl object instance.
Get the nominal sites per worker (x-chunk size) for a given file.
+
This is based on the concept that it is most efficient for one core to
+perform one read on one chunk of resource data, such that chunks will
+not have to be read into memory twice and no sites will be read
+redundantly.
+
+
Parameters:
+
+
res_file (str) – Filepath to single resource file, multi-h5 directory,
+or /h5_dir/prefix*suffix
+
default (int) – Sites to be analyzed on a single core if the chunk size cannot be
+determined from res_file.
+
+
+
Returns:
+
sites_per_worker (int) – Nominal sites to be analyzed per worker. This is set to the x-axis
+chunk size for windspeed and dni datasets for the WTK and NSRDB
+data, respectively.
Execute a parallel reV generation run with smart data flushing.
+
+
Parameters:
+
+
out_fpath (str, optional) – Path to output file. If None, no output file will
+be written. If the filepath is specified but the module name
+(generation) and/or resource data year is not included, the
+module name and/or resource data year will get added to the
+output file name. By default, None.
+
max_workers (int, optional) – Number of local workers to run on. If None, or if
+running from the command line and omitting this argument
+from your config file completely, this input is set to
+os.cpu_count(). Otherwise, the default is 1.
+
timeout (int, optional) – Number of seconds to wait for parallel run iteration to
+complete before returning zeros. By default, 1800
+seconds.
+
pool_size (int, optional) – Number of futures to submit to a single process pool for
+parallel futures. If None, the pool size is set to
+os.cpu_count()*2. By default, None.
+
+
+
Returns:
+
str | None – Path to output HDF5 file, or None if results were not
+written to disk.
site_gid (int) – Resource-native site index (gid).
+
out_index (bool) – Option to get output index (if true) which is the column index in
+the current in-memory output array, or (if false) the global site
+index from the project points site list.
+
+
+
Returns:
+
index (int) – Global site index if out_index=False, otherwise column index in
+the current in-memory output array.
tech (str) – SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+solarwaterheat, troughphysicalheat, lineardirectsteam, econ)
+The string should be lower-cased with spaces and _ removed.
Get an array of 1D index values for the flattened h5 excl extent.
+
+
Returns:
+
iarr (np.ndarray) – Uint array with same shape as exclusion extent, representing the 1D
+index values if the geotiff extent was flattened
+(with default flatten order ‘C’)
Class to handle multiple years of data and:
+- collect datasets from multiple years
+- compute multi-year means
+- compute multi-year standard deviations
+- compute multi-year coefficient of variations
+
+
Parameters:
+
+
h5_file (str) – Path to .h5 resource file
+
group (str) – Group to collect datasets into
+
unscale (bool) – Boolean flag to automatically unscale variables on extraction
+
mode (str) – Mode to instantiate h5py.File instance
+
str_decode (bool) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read.
Parse a source_files pattern that can be either an explicit list of source files or a unix-style /filepath/pattern*.h5 and either way return a list of explicit filepaths.
Parse a source_files pattern that can be either an explicit list of
+source files or a unix-style /filepath/pattern*.h5 and either way
+return a list of explicit filepaths.
+
+
Parameters:
+
source_files (list | str) – List of .h5 files to collect datasets from. This can also be a
+unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+however all resulting files must be .h5 otherwise an exception will
+be raised. NOTE: .h5 file names must indicate the year the data
+pertains to
source_files (list | str) – List of .h5 files to collect datasets from. This can also be a
+unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+however all resulting files must be .h5 otherwise an exception will
+be raised. NOTE: .h5 file names must indicate the year the data
+pertains to
+
dset (str) – Dataset to collect
+
profiles (bool) – Boolean flag to indicate if profiles are being collected
+If True also collect time_index
+
pass_through (bool) – Flag to just pass through dataset without name modifications
+(no differences between years, no means or stdevs)
Check dataset in source files to see if it is a profile.
+
+
Parameters:
+
+
source_files (list | str) – List of .h5 files to collect datasets from. This can also be a
+unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+however all resulting files must be .h5 otherwise an exception will
+be raised. NOTE: .h5 file names must indicate the year the data
+pertains to
+
dset (str) – Dataset to collect
+
+
+
Returns:
+
is_profile (bool) – True if profile, False if not.
Pass through a dataset that is identical in all source files to a
+dataset of the same name in the output multi-year file.
+
+
Parameters:
+
+
my_file (str) – Path to multi-year .h5 file
+
source_files (list | str) – List of .h5 files to collect datasets from. This can also be a
+unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+however all resulting files must be .h5 otherwise an exception will
+be raised. NOTE: .h5 file names must indicate the year the data
+pertains to
+
dset (str) – Dataset to pass through (will also be the name of the output
+dataset in my_file)
Collect and compute multi-year means for given dataset
+
+
Parameters:
+
+
my_file (str) – Path to multi-year .h5 file
+
source_files (list | str) – List of .h5 files to collect datasets from. This can also be a
+unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+however all resulting files must be .h5 otherwise an exception will
+be raised. NOTE: .h5 file names must indicate the year the data
+pertains to
Collect multi-year profiles associated with given dataset
+
+
Parameters:
+
+
my_file (str) – Path to multi-year .h5 file
+
source_files (list | str) – List of .h5 files to collect datasets from. This can also be a
+unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+however all resulting files must be .h5 otherwise an exception will
+be raised. NOTE: .h5 file names must indicate the year the data
+pertains to
dset_name (str) – Name of dataset to be added to h5 file
+
dset_data (ndarray) – Data to be added to h5 file
+
dtype (str) – Intended dataset datatype after scaling.
+
attrs (dict, optional) – Attributes to be set. May include ‘scale_factor’, by default None
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘a’
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
dset_name (str) – Name of the target dataset (should identify the means).
+
means (ndarray) – output means array.
+
dtype (str) – Intended dataset datatype after scaling.
+
attrs (dict, optional) – Attributes to be set. May include ‘scale_factor’, by default None
+
SAM_configs (dict, optional) – Dictionary of SAM configuration JSONs used to compute cf means,
+by default None
+
chunks (tuple, optional) – Chunk size for capacity factor means dataset, by default None
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘w-’
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
dset_name (str) – Name of the target dataset (should identify the profiles).
+
profiles (ndarray) – output result timeseries profiles
+
dtype (str) – Intended dataset datatype after scaling.
+
attrs (dict, optional) – Attributes to be set. May include ‘scale_factor’, by default None
+
SAM_configs (dict, optional) – Dictionary of SAM configuration JSONs used to compute cf means,
+by default None
+
chunks (tuple, optional) – Chunk size for capacity factor means dataset,
+by default (None, 100)
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘w-’
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
name (str) – Group name. Can be "none" for no collection groups.
+
out_dir (str) – Output directory - used for Pipeline handling.
+
source_files (str | list, optional) – Explicit list of source files. Use either this input OR
+source_dir + source_prefix. If this input is
+"PIPELINE", the source_files input is determined from
+the status file of the previous pipeline step.
+If None, use source_dir and source_prefix.
+By default, None.
+
source_dir (str, optional) – Directory to extract source files from (must be paired with
+source_prefix). By default, None.
+
source_prefix (str, optional) – File prefix to search for in source directory (must be
+paired with source_dir). By default, None.
+
source_pattern (str, optional) – Optional unix-style /filepath/pattern*.h5 to specify the
+source files. This takes priority over source_dir and
+source_prefix but is not used if source_files are
+specified explicitly. By default, None.
+
dsets (str | list | tuple, optional) – List of datasets to collect. This can be set to
+"PIPELINE" if running from the command line as part of a
+reV pipeline. In this case, all the datasets from the
+previous pipeline step will be collected.
+By default, ('cf_mean',).
+
pass_through_dsets (list | tuple, optional) – Optional list of datasets that are identical in the
+multi-year files (e.g. input datasets that don’t vary from
+year to year) that should be copied to the output multi-year
+file once without a year suffix or means/stdev calculation.
+By default, None.
Optional list of datasets that are identical in the multi-year
+files (e.g. input datasets that don’t vary from year to year) that
+should be copied to the output multi-year file once without a
+year suffix or means/stdev calculation
Class to handle multiple years of data and: - collect datasets from multiple years - compute multi-year means - compute multi-year standard deviations - compute multi-year coefficient of variations
Collect all groups into a single multi-year HDF5 file.
+
reV multi-year combines reV generation data from multiple
+years (typically stored in separate files) into a single multi-year
+file. Each dataset in the multi-year file is labeled with the
+corresponding years, and multi-year averages of the yearly datasets
+are also computed.
+
+
Parameters:
+
+
out_fpath (str) – Path to multi-year HDF5 file to use for multi-year
+collection.
+
groups (dict) – Dictionary of collection groups and their parameters. This
+should be a dictionary mapping group names (keys) to a set
+of key word arguments (values) that can be used to initialize
+MultiYearGroup (excluding the
+required name and out_dir inputs, which are populated
+automatically). For example:
The group names will be used as the HDF5 file group name under
+which the collected data will be stored. You can have exactly
+one group with the name "none" for a “no group” collection
+(this is typically what you want and all you need to specify).
+
+
clobber (bool, optional) – Flag to purge the multi-year output file prior to running the
+multi-year collection step if the file already exists on disk.
+This ensures the data is always freshly collected from the
+single-year files. If False, then datasets in the existing
+file will not be overwritten with (potentially new/updated)
+data from the single-year files. By default, True.
You can also use the Outputs handler to read output h5 files from disk.
+The Outputs handler will automatically parse the meta data and time index
+into the expected pandas objects (DataFrame and DatetimeIndex,
+respectively).
There are a few ways to use the Outputs handler to write data to a file.
+Here is one example using the pre-initialized file we created earlier.
+Note that the Outputs handler will automatically scale float data using
+the “scale_factor” attribute. The Outputs handler will unscale the data
+while being read unless the unscale kwarg is explicityly set to False.
+This behavior is intended to reduce disk storage requirements for big
+data and can be disabled by setting dtype=np.float32 or dtype=np.float64
+when writing data.
Note that the reV Outputs handler is specifically designed to read and
+write spatiotemporal data. It is therefore important to intialize the meta
+data and time index objects even if your data is only spatial or only
+temporal. Furthermore, the Outputs handler will always assume that 1D
+datasets represent scalar data (non-timeseries) that corresponds to the
+meta data shape, and that 2D datasets represent spatiotemporal data whose
+shape corresponds to (len(time_index), len(meta)). You can see these
+constraints here:
+
>>> Outputs.add_dataset(h5_file='test.h5',dset_name='bad_shape',
+ dset_data=np.ones((1, 100)) * 42.42,
+ attrs={'scale_factor': 100}, dtype=np.int32)
+HandlerValueError: 2D data with shape (1, 100) is not of the proper
+spatiotemporal shape: (8760, 100)
+
+
+
>>> Outputs.add_dataset(h5_file='test.h5',dset_name='bad_shape',
+ dset_data=np.ones((8760,)) * 42.42,
+ attrs={'scale_factor': 100}, dtype=np.int32)
+HandlerValueError: 1D data with shape (8760,) is not of the proper
+spatial shape: (100,)
+
+
+
+
Parameters:
+
+
h5_file (str) – Path to .h5 resource file
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘r’
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
dset_name (str) – Name of dataset to be added to h5 file
+
dset_data (ndarray) – Data to be added to h5 file
+
dtype (str) – Intended dataset datatype after scaling.
+
attrs (dict, optional) – Attributes to be set. May include ‘scale_factor’, by default None
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘a’
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
dset_name (str) – Name of the target dataset (should identify the means).
+
means (ndarray) – output means array.
+
dtype (str) – Intended dataset datatype after scaling.
+
attrs (dict, optional) – Attributes to be set. May include ‘scale_factor’, by default None
+
SAM_configs (dict, optional) – Dictionary of SAM configuration JSONs used to compute cf means,
+by default None
+
chunks (tuple, optional) – Chunk size for capacity factor means dataset, by default None
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘w-’
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
dset_name (str) – Name of the target dataset (should identify the profiles).
+
profiles (ndarray) – output result timeseries profiles
+
dtype (str) – Intended dataset datatype after scaling.
+
attrs (dict, optional) – Attributes to be set. May include ‘scale_factor’, by default None
+
SAM_configs (dict, optional) – Dictionary of SAM configuration JSONs used to compute cf means,
+by default None
+
chunks (tuple, optional) – Chunk size for capacity factor means dataset,
+by default (None, 100)
+
unscale (bool, optional) – Boolean flag to automatically unscale variables on extraction,
+by default True
+
mode (str, optional) – Mode to instantiate h5py.File instance, by default ‘w-’
+
str_decode (bool, optional) – Boolean flag to decode the bytestring meta data into normal
+strings. Setting this to False will speed up the meta data read,
+by default True
+
group (str, optional) – Group within .h5 resource file to open, by default None
Compute the total capcity by summing the individual capacities.
+
+
Parameters:
+
h (reV.hybrids.Hybridization) – Instance of reV.hybrids.Hybridization class containing the
+attribute hybrid_meta, which is a DataFrame containing
+hybridized meta data.
+
+
Returns:
+
data (Series | None) – A series of data containing the aggregated capacity, or None
+if the capacity columns are missing.
Compute the capacity-weighted mean capcity factor.
+
+
Parameters:
+
h (reV.hybrids.Hybridization) – Instance of reV.hybrids.Hybridization class containing the
+attribute hybrid_meta, which is a DataFrame containing
+hybridized meta data.
+
+
Returns:
+
data (Series | None) – A series of data containing the aggregated capacity, or None
+if the capacity and/or mean_cf columns are missing.
Compute the total solar capcity allowed in hybridization.
+
+
Parameters:
+
h (reV.hybrids.Hybridization) – Instance of reV.hybrids.Hybridization class containing the
+attribute hybrid_meta, which is a DataFrame containing
+hybridized meta data.
+
+
Returns:
+
data (Series | None) – A series of data containing the capacity allowed in the hybrid
+capacity sum, or None if ‘hybrid_solar_capacity’ already
+exists.
+
+
+
Notes
+
No limiting is done on the ratio of wind to solar. This method
+checks for an existing ‘hybrid_solar_capacity’. If one does not
+exist, it is assumed that there is no limit on the solar to wind
+capacity ratio and the solar capacity is copied into this new
+column.
Compute the total wind capcity allowed in hybridization.
+
+
Parameters:
+
h (reV.hybrids.Hybridization) – Instance of reV.hybrids.Hybridization class containing the
+attribute hybrid_meta, which is a DataFrame containing
+hybridized meta data.
+
+
Returns:
+
data (Series | None) – A series of data containing the capacity allowed in the hybrid
+capacity sum, or None if ‘hybrid_solar_capacity’ already
+exists.
+
+
+
Notes
+
No limiting is done on the ratio of wind to solar. This method
+checks for an existing ‘hybrid_wind_capacity’. If one does not
+exist, it is assumed that there is no limit on the solar to wind
+capacity ratio and the wind capacity is copied into this new column.
Framework to handle hybridization of SC and corresponding profiles.
+
reV hybrids computes a “hybrid” wind and solar supply curve,
+where each supply curve point contains some wind and some solar
+capacity. Various ratio limits on wind-to-solar farm properties
+(e.g. wind-to-solar capacity) can be applied during the
+hybridization process. Hybrid generation profiles are also
+computed during this process.
+
+
Parameters:
+
+
solar_fpath (str) – Filepath to rep profile output file to extract solar
+profiles and summaries from.
+
wind_fpath (str) – Filepath to rep profile output file to extract wind profiles
+and summaries from.
+
allow_solar_only (bool, optional) – Option to allow SC points with only solar capacity
+(no wind). By default, False.
+
allow_wind_only (bool, optional) – Option to allow SC points with only wind capacity
+(no solar). By default, False.
+
fillna (dict, optional) – Dictionary containing column_name, fill_value pairs
+representing any fill values that should be applied after
+merging the wind and solar meta. Note that column names will
+likely have to be prefixed with solar or wind.
+By default None.
+
limits (dict, optional) – Option to specify mapping (in the form of a dictionary) of
+{colum_name: max_value} representing the upper limit
+(maximum value) for the values of a column in the merged
+meta. For example, limits={'solar_capacity':100} would
+limit all the values of the solar capacity in the merged
+meta to a maximum value of 100. This limit is applied
+BEFORE ratio calculations. The names of the columns should
+match the column names in the merged meta, so they are
+likely prefixed with solar or wind.
+By default, None (no limits applied).
+
ratio_bounds (tuple, optional) – Option to set ratio bounds (in two-tuple form) on the
+columns of the ratio input. For example,
+ratio_bounds=(0.5,1.5) would adjust the values of both
+of the ratio columns such that their ratio is always
+between half and double (e.g., no value would be more than
+double the other). To specify a single ratio value, use the
+same value as the upper and lower bound. For example,
+ratio_bounds=(1,1) would adjust the values of both of
+the ratio columns such that their ratio is always equal.
+By default, None (no limit on the ratio).
+
ratio (str, optional) – Option to specify the columns used to calculate the ratio
+that is limited by the ratio_bounds input. This input is a
+string in the form “{numerator_column}/{denominator_column}”.
+For example, ratio='solar_capacity/wind_capacity'
+would limit the ratio of the solar to wind capacities as
+specified by the ratio_bounds input. If ratio_bounds
+is None, this input does nothing. The names of the columns
+should be prefixed with one of the prefixes defined as class
+variables. By default 'solar_capacity/wind_capacity'.
data (HybridsData) – Instance of HybridsData containing input data to
+hybridize.
+
allow_solar_only (bool, optional) – Option to allow SC points with only solar capacity
+(no wind). By default, False.
+
allow_wind_only (bool, optional) – Option to allow SC points with only wind capacity
+(no solar), By default, False.
+
fillna (dict, optional) – Dictionary containing column_name, fill_value pairs
+representing any fill values that should be applied after
+merging the wind and solar meta. Note that column names will
+likely have to be prefixed with solar or wind.
+By default, None.
+
limits (dict, optional) – Option to specify mapping (in the form of a dictionary) of
+{colum_name: max_value} representing the upper limit
+(maximum value) for the values of a column in the merged
+meta. For example, limits={‘solar_capacity’: 100} would
+limit all the values of the solar capacity in the merged
+meta to a maximum value of 100. This limit is applied
+BEFORE ratio calculations. The names of the columns should
+match the column names in the merged meta, so they are
+likely prefixed with solar or wind`.Bydefault,
+``None (no limits applied).
+
ratio_bounds (tuple, optional) – Option to set ratio bounds (in two-tuple form) on the
+columns of the ratio input. For example,
+ratio_bounds=(0.5, 1.5) would adjust the values of both of
+the ratio columns such that their ratio is always between
+half and double (e.g., no value would be more than double
+the other). To specify a single ratio value, use the same
+value as the upper and lower bound. For example,
+ratio_bounds=(1, 1) would adjust the values of both of the
+ratio columns such that their ratio is always equal.
+By default, None (no limit on the ratio).
+
ratio (str, optional) – Option to specify the columns used to calculate the ratio
+that is limited by the ratio_bounds input. This input is a
+string in the form
+“numerator_column_name/denominator_column_name”.
+For example, ratio=’solar_capacity/wind_capacity’ would
+limit the ratio of the solar to wind capacities as specified
+by the ratio_bounds input. If ratio_bounds is None,
+this input does nothing. The names of the columns should be
+prefixed with one of the prefixes defined as class
+variables. By default 'solar_capacity/wind_capacity'.
Abstract base class for power curve transformations.
+
This class is not meant to be instantiated.
+
This class provides an interface for power curve transformations,
+which are meant to more realistically represent certain types of
+losses when compared to simple haircut losses (i.e. constant loss
+value applied at all points on the power curve).
+
If you would like to implement your own power curve transformation,
+you should subclass this class and implement the apply()
+method and the bounds property. See the documentation for
+each of these below for more details.
Apply a transformation to the original power curve.
+
+
Parameters:
+
transformation_var (: float) – A single variable controlling the “strength” of the
+transformation. The PowerCurveLosses object will
+run an optimization using this variable to fit the target
+annual losses incurred with the transformed power curve
+compared to the original power curve using the given wind
+resource distribution.
+
+
Returns:
+
PowerCurve – An new power curve containing the generation values from the
+transformed power curve.
+
+
Raises:
+
NotImplementedError – If the transformation implementation did not set the
+ _transformed_generation attribute.
+
+
+
Notes
+
When implementing a new transformation, override this method and
+set the _transformed_generation protected attribute to be
+the generation corresponding to the transformed power curve.
+Then, call super().apply(transformation_var) in order to
+apply cutout speed curtailment and validation for the
+transformed power curve. For example, here is the implementation
+for a transformation that shifts the power curve horizontally:
Utility for applying an exponential stretch to the power curve.
+
The mathematical representation of this transformation is:
+
+\[P_{transformed}(u) = P_{original}(u^{1/t}),\]
+
where \(P_{transformed}\) is the transformed power curve,
+\(P_{original}\) is the original power curve, \(u\) is
+the wind speed, and \(t\) is the transformation variable
+(wind speed exponent).
+
The losses in this type of transformation are distributed primarily
+across regions 2 and 3 of the power curve. In particular, losses are
+smaller for wind speeds closer to the cut-in speed, and larger for
+speeds close to rated power:
Apply an exponential stretch to the original power curve.
+
This function stretches the original power curve along the
+“wind speed” (x) axis. Any power above the cutoff speed (if one
+was detected) is truncated after the transformation.
+
+
Parameters:
+
transformation_var (float) – The exponent of the wind speed scaling.
+
+
Returns:
+
PowerCurve – An new power curve containing the generation values from the
+shifted power curve.
Utility for applying horizontal power curve translations.
+
The mathematical representation of this transformation is:
+
+\[P_{transformed}(u) = P_{original}(u - t),\]
+
where \(P_{transformed}\) is the transformed power curve,
+\(P_{original}\) is the original power curve, \(u\) is
+the wind speed, and \(t\) is the transformation variable
+(horizontal translation amount).
+
This kind of power curve transformation is simplistic, and should
+only be used for a small handful of applicable turbine losses
+(i.e. blade degradation). See Warnings for more details.
+
The losses in this type of transformation are distributed primarily
+across region 2 of the power curve (the steep, almost linear,
+portion where the generation rapidly increases):
This kind of power curve translation is not generally realistic.
+Using this transformation as a primary source of losses (i.e. many
+different kinds of losses bundled together) is extremely likely to
+yield unrealistic results!
+
+
Abstract Power Curve Transformation class.
+
+
Parameters:
+
power_curve (PowerCurve) – The turbine power curve. This input is treated as the
+“original” power curve.
Apply a horizontal translation to the original power curve.
+
This function shifts the original power curve horizontally,
+along the “wind speed” (x) axis, by the given amount. Any power
+above the cutoff speed (if one was detected) is truncated after
+the transformation.
+
+
Parameters:
+
transformation_var (float) – The amount to shift the original power curve by, in wind
+speed units (typically, m/s).
+
+
Returns:
+
PowerCurve – An new power curve containing the generation values from the
+shifted power curve.
Utility for applying a linear stretch to the power curve.
+
The mathematical representation of this transformation is:
+
+\[P_{transformed}(u) = P_{original}(u/t),\]
+
where \(P_{transformed}\) is the transformed power curve,
+\(P_{original}\) is the original power curve, \(u\) is
+the wind speed, and \(t\) is the transformation variable
+(wind speed multiplier).
+
The losses in this type of transformation are distributed primarily
+across regions 2 and 3 of the power curve. In particular, losses are
+smaller for wind speeds closer to the cut-in speed, and larger for
+speeds close to rated power:
Apply a linear stretch to the original power curve.
+
This function stretches the original power curve along the
+“wind speed” (x) axis. Any power above the cutoff speed (if one
+was detected) is truncated after the transformation.
+
+
Parameters:
+
transformation_var (float) – The linear multiplier of the wind speed scaling.
+
+
Returns:
+
PowerCurve – An new power curve containing the generation values from the
+shifted power curve.
An array containing the generated power in kW at the corresponding
+wind speed in the wind_speed array. This input must have
+at least one positive value, and if a cutoff speed is detected
+(see Warnings section below), then all values above that wind
+speed must be set to 0.
This class will attempt to infer a cutoff speed from the
+generation input. Specifically, it will look for a transition
+from the highest rated power down to zero in a single wind_speed
+step of the power curve. If such a transition is detected, the wind
+speed corresponding to the zero value will be set as the cutoff
+speed, and all calculated power curves will be clipped at this
+speed. If your input power curve contains a cutoff speed, ensure
+that it adheres to the expected pattern of dropping from max rated
+power to zero power in a single wind speed step.
+
+
+
Parameters:
+
+
wind_speed (array_like) – An iterable containing the wind speeds corresponding to the
+generated power values in generation input. The input
+values should all be non-zero.
+
generation (array_like) – An iterable containing the generated power in kW at the
+corresponding wind speed in the wind_speed input. This
+input must have at least one positive value, and if a cutoff
+speed is detected (see Warnings section below), then all
+values above that wind speed must be set to 0.
A converter between annual losses and power curve transformation.
+
Given a target annual loss value, this class facilitates the
+calculation of a power curve transformation such that the annual
+generation losses incurred by using the transformed power curve when
+compared to the original (non-transformed) power curve match the
+target loss as close as possible.
+
The underlying assumption for this approach is that some types of
+losses can be realized by a transformation of the power curve (see
+the values of TRANSFORMATIONS for details on all of the
+power curve transformations that have been implemented).
+
The advantage of this approach is that, unlike haircut losses (where
+a single loss value is applied across the board to all generation),
+the losses are distributed non-uniformly across the power curve. For
+example, even in the overly simplified case of a horizontal
+translation of the power curve (which is only physically realistic
+for certain types of losses like blade degradation), the losses are
+distributed primarily across region 2 of the power curve (the steep,
+almost linear, portion where the generation rapidly increases). This
+means that, unlike with haircut losses, generation is able to reach
+max rated power (albeit at a greater wind speed).
An array containing the wind speeds (i.e. wind speed
+distribution) for the site at which the power curve will be
+used. This distribution is used to calculate the annual
+generation of the original power curve as well as any additional
+calculated power curves. The generation values are then compared
+in order to calculate the loss resulting from a transformed
+power curve.
power_curve (PowerCurve) – The “original” power curve to be adjusted.
+
wind_resource (array_like) – An iterable containing the wind speeds measured at the site
+where this power curve will be applied to calculate
+generation. These values are used to calculate the loss
+resulting from a transformed power curve compared to the
+generation of the original power curve. The input
+values should all be non-zero, and the units of
+should match the units of the power_curve input
+(typically, m/s).
+
weights (array_like, optional) – An iterable of the same length as wind_resource
+containing weights to apply to each generation value
+calculated for the corresponding wind speed.
+
site (int | str, optional) – Site number (gid) for debugging and logging.
+By default, None.
Calculate the annual losses from a transformed power curve.
+
This function uses the wind resource data that the object was
+initialized with to calculate the total annual power generation
+with a transformed power curve. This generation is compared with
+the generation of the original (non-transformed) power curve to
+compute the total annual losses as a result of the
+transformation.
+
+
Parameters:
+
transformed_power_curve (PowerCurve) – A transformed power curve. The power generated with this
+power curve will be compared with the power generated by the
+“original” power curve to calculate annual losses.
+
+
Returns:
+
float – Total losses (%) as a result of a the power curve
+transformation.
This function fits a transformation to the input power curve
+(the one used to initialize the object) to generate an annual
+loss percentage closest to the target. The losses are
+computed w.r.t the generation of the original (non-transformed)
+power curve.
+
+
Parameters:
+
+
target (float) – Target value for annual generation losses (%).
+
transformation (PowerCurveTransformation) – A PowerCurveTransformation class representing the power
+curve transformation to use.
+
+
+
Returns:
+
numpy.array – An array containing a transformed power curve that most
+closely yields the target annual generation losses.
+
+
Warns:
+
reVLossesWarning – If the fit did not meet the target annual losses to within
+1%.
+
+
+
+
Warning
+
This function attempts to find an optimal transformation for the
+power curve such that the annual generation losses match the
+target value, but there is no guarantee that a close match
+can be found, if it even exists. Therefore, it is possible that
+the losses resulting from the transformed power curve will not
+match the target. This is especially likely if the
+target is large or if the input power curve and/or wind
+resource is abnormal.
This class stores and validates information about the desired losses
+from a given type of power curve transformation. In particular, the
+target loss percentage must be provided. This input is then
+validated to be used power curve transformation fitting.
+
+
Parameters:
+
specs (dict) – A dictionary containing specifications for the power curve
+losses. This dictionary must contain the following keys:
+
+
+
+
target_losses_percent
An integer or float value representing the
+total percentage of annual energy production that
+should be lost due to the power curve transformation.
+This value must be in the range [0, 100].
+
+
+
+
+
+
The input dictionary can also provide the following optional
+keys:
+
+
+
transformation - by default, horizontal_translation
+A string representing the type of transformation to
+apply to the power curve. This sting must be one of
+the keys of TRANSFORMATIONS. See the relevant
+transformation class documentation for detailed
+information on that type of power curve
+transformation.
Adjust power curve in SAM config file to account for losses.
+
This function reads the information in the
+reV_power_curve_losses key of the sam_sys_inputs
+dictionary and computes a new power curve that accounts for the
+loss percentage specified from that input. If no power curve
+loss info is specified in sam_sys_inputs, the power curve
+will not be adjusted.
Wind resource data for calculating power curve shift.
+
Power Curve Wind Resource.
+
+
Parameters:
+
+
temperature (array_like) – An iterable representing the temperatures at a single site
+(in C). Must be the same length as the pressure and
+wind_speed inputs.
+
pressure (array_like) – An iterable representing the pressures at a single site
+(in PA or ATM). Must be the same length as the temperature
+and wind_speed inputs.
+
wind_speed (array_like) – An iterable representing the wind speeds at a single site
+(in m/s). Must be the same length as the temperature and
+pressure inputs.
Get the wind speeds for this site, accounting for the scaling
+done in SAM [1] based on air pressure [2]. These wind speeds
+can then be used to sample the power curve and obtain generation
+values.
site (int | str, optional) – Site number (gid) for debugging and logging.
+By default, None.
+
+
+
Returns:
+
PowerCurve – Power Curve shifted to meet the target losses. Power Curve is
+not adjusted if all wind speeds are above the cutout or below
+the cutin speed.
This class stores and validates information about a single type of
+outage. In particular, the number of outages, duration, percentage
+of farm down, and the allowed months for scheduling the outage
+must all be provided. These inputs are then validated so that they
+can be used in instances of scheduling objects.
+
+
Parameters:
+
specs (dict) – A dictionary containing specifications for this outage. This
+dictionary must contain the following keys:
+
+
+
+
count
An integer value representing the total number of
+times this outage should be scheduled. This number
+should be larger than 0.
+
+
+
+
+
duration
An integer value representing the total number of
+consecutive hours that this outage should take. This
+value must be larger than 0 and less than the number
+of hours in the allowed months.
+
+
+
+
+
percentage_of_capacity_lost
An integer or float value representing the total
+percentage of the total capacity that will be lost
+for the duration of the outage. This value must be
+in the range (0, 100].
+
+
+
+
+
allowed_months
A list of month names corresponding to the allowed
+months for the scheduled outages. Month names can be
+unformatted and can be specified using 3-letter
+month abbreviations.
+
+
+
+
+
+
The input dictionary can also provide the following optional
+keys:
+
+
+
+
allow_outage_overlap - by default, True
A bool flag indicating whether or not this outage is
+allowed to overlap with other outages, including
+itself. It is recommended to set this value to
+True whenever possible, as it allows for more
+flexible scheduling.
+
+
+
+
+
name - by default, string containing init parameters
A unique name for the outage, used for more
+descriptive error messages.
Given a list of information about different types of desired
+outages, this class leverages the stochastic scheduling routines of
+SingleOutageScheduler to calculate the total losses due to
+the input outages on an hourly basis.
The seed value used to seed the random generator in order
+to produce random but reproducible losses. This is useful
+for ensuring that stochastically scheduled losses vary
+between different sites (i.e. that randomly scheduled
+outages in two different location do not match perfectly on
+an hourly basis).
An array (of length 8760) containing the per-hour total loss
+percentage resulting from the stochastically scheduled outages.
+This array contains only zero values before the
+calculate() method is run.
A boolean array (of length 8760) indicating wether or not more
+losses can be scheduled for a given hour. This array keeps track
+of all the scheduling conflicts between input outages.
+
+
Type:
+
np.array
+
+
+
+
+
+
Warning
+
It is possible that not all outages input by the user will be
+scheduled. This can happen when there is not enough time allowed
+for all of the input outages. To avoid this issue, always be sure to
+allow a large enough month range for long outages that take up a big
+portion of the farm and try to allow outage overlap whenever
+possible.
outages (list of Outages) – A list of Outages, where each Outage
+contains info about a single type of outage. See the
+documentation of Outage for a description of the
+required keys of each outage dictionary.
+
seed (int, optional) – An integer value used to seed the random generator in order
+to produce random but reproducible losses. This is useful
+for ensuring that stochastically scheduled losses vary
+between different sites (i.e. that randomly scheduled
+outages in two different location do not match perfectly on
+an hourly basis). By default, the seed is set to 0.
Calculate total losses from stochastically scheduled outages.
+
This function calls SingleOutageScheduler.calculate()
+on every outage input (sorted by largest duration and then
+largest number of outages) and returns the aggregate the losses
+from the result.
+
+
Returns:
+
np.array – An array (of length 8760) containing the per-hour total loss
+percentage resulting from the stochastically scheduled
+outages.
Add stochastically scheduled losses to SAM config file.
+
This function reads the information in the reV_outages key
+of the sam_sys_inputs dictionary and computes stochastically
+scheduled losses from that input. If the value for
+reV_outages is a string, it must have been generated by
+calling json.dumps() on the list of dictionaries
+containing outage specifications. Otherwise, the outage
+information is expected to be a list of dictionaries containing
+outage specifications. See Outage for a description of
+the specifications allowed for each outage. The scheduled losses
+are passed to SAM via the hourly key to signify which hourly
+capacity factors should be adjusted with outage losses. If no
+outage info is specified in sam_sys_inputs, no scheduled
+losses are added.
+
+
Parameters:
+
resource (pd.DataFrame, optional) – Time series resource data for a single location with a
+pandas DatetimeIndex. The year value of the index will
+be used to seed the stochastically scheduled losses. If
+None, no yearly seed will be used.
The scheduled losses are passed to SAM via the hourly key to
+signify which hourly capacity factors should be adjusted with
+outage losses. If the user specifies other hourly adjustment
+factors via the hourly key, the effect is combined. For
+example, if the user inputs a 33% hourly adjustment factor and
+reV schedules an outage for 70% of the farm down for the same
+hour, then the resulting adjustment factor is
+
+
+
This means the generation will be reduced by ~80%, because the
+user requested 33% losses for the 30% the farm that remained
+operational during the scheduled outage (i.e. 20% remaining of
+the original generation).
Given information about a single type of outage, this class
+facilitates the (randomized) scheduling of all requested instances
+of the outage. See SingleOutageScheduler.calculate() for
+specific details about the scheduling process.
A boolean array (of length 8760) indicating wether or not more
+losses can be scheduled for a given hour. This is specific
+to the input outage only.
+
+
Type:
+
np.array
+
+
+
+
+
+
Warning
+
It is possible that not all outages input by the user can be
+scheduled. This can happen when there is not enough time allowed
+for all of the input outages. To avoid this issue, always be sure to
+allow a large enough month range for long outages that take up a big
+portion of the farm and try to allow outage overlap whenever
+possible.
outage (Outage) – An outage object containing info about the outage to be
+scheduled.
+
scheduler (OutageScheduler) – A scheduler object that keeps track of the total hourly
+losses from the input outage as well as any other outages
+it has already scheduled.
Calculate losses from stochastically scheduled outages.
+
This function attempts to schedule outages according to the
+specification provided in the Outage input. Specifically,
+it checks the available hours based on the main
+Scheduler (which may have other outages
+already scheduled) and attempts to randomly add new outages with
+the specified duration and percent of losses. The function
+terminates when the desired number of outages (specified by
+Outage.count) have been successfully scheduled, or when
+the number of attempts exceeds
+MAX_ITER + Outage.count.
+
+
Warns:
+
reVLossesWarning – If the number of requested outages could not be scheduled.
Find a random slot of time for this type of outage.
+
This function randomly selects a starting time for this outage
+given the allowed times in can_schedule_more. It does
+not verify that the outage can be scheduled for the entire
+requested duration.
+
+
Parameters:
+
seed (int, optional) – Integer used to seed the np.random.choice() call.
+If None, seed is not used.
+
+
Returns:
+
slice – A slice corresponding to the random slot of time for this
+type of outage.
Format an iterable of month names to match those in calendar.
+
This function will format each input name to match the formatting
+in calendar.month_name (upper case, no extra whitespace), and
+it will convert all abbreviations to full month names. No other
+assumptions are made about the inputs, so an input string “ abc ”
+will get formatted and passed though as “Abc”.
+
+
Parameters:
+
month_names (iter) – An iterable of strings representing the input month names.
+Month names can be unformatted and contain 3-letter month
+abbreviations.
+
+
Returns:
+
list – A list of month names matching the formatting of
+calendar.month_name (upper case, no extra whitespace).
+Abbreviations are also converted to a full month name.
Split the input into known and unknown month names.
+
+
Parameters:
+
month_names (iter) – An iterable of strings representing the input month names. Month
+names must match the formatting in calendar.month_name
+(upper case, no extra whitespace), otherwise they will be placed
+into the unknown_months return list.
Format a month name to match the names in the calendar module.
+
In particular, any extra spaces at the beginning or end of the
+string are stripped, and the name is converted to a title (first
+letter is uppercase).
+
+
Parameters:
+
month_name (str) – Name of month.
+
+
Returns:
+
str – Name of month, formatted to match the month names in the
+calendar module.
Convert month names into a list of hourly indices.
+
Given a list of month names, this function will return a list
+of indices such that any index value corresponds to an hour within
+the input months.
+
+
Parameters:
+
month_names (iter) – An iterable of month names for the desired starting indices.
+The month names must match the formatting in
+calendar.month_name (upper case, no extra whitespace),
+otherwise their hourly indices will not be included in the
+output.
+
+
Returns:
+
list – A list of hourly index values such that any index corresponds to
+an hour within the input months.
Convert a month name (as string) to an index (0-11) of the month.
+
+
Parameters:
+
month_name (str) – Name of month to corresponding to desired index. This input
+must match the formatting in calendar.month_name
+(upper case, no extra whitespace).
+
+
Returns:
+
int – The 0-index of the month, or -1 if the month name is not
+understood.
Convert input month names to an indices (0-11) of the months.
+
+
Parameters:
+
month_names (iter) – An iterable of month names for the desired starting indices.
+The month names must match the formatting in
+calendar.month_name (upper case, no extra whitespace),
+otherwise their index will not be included in the output.
+
+
Returns:
+
set – A set of month indices for the input month names. Unknown
+month indices (-1) are removed.
reV NRWAL analysis runs reV data through the NRWAL
+compute library. Everything in this module operates on the
+spatiotemporal resolution of the reV generation output file
+(usually the wind or solar resource resolution but could also be
+the supply curve resolution after representative profiles is
+run).
+
+
Parameters:
+
+
gen_fpath (str) – Full filepath to HDF5 file with reV generation or
+rep_profiles output. Anything in the output_request input
+is added to and/or manipulated within this file.
+
+
Note
+
If executing reV from the command line, this
+input can also be "PIPELINE" to parse the output of
+one of the previous step and use it as input to this call.
+However, note that duplicate executions of reV
+commands prior to this one within the pipeline may
+invalidate this parsing, meaning the gen_fpath input
+will have to be specified manually.
+
+
+
site_data (str | pd.DataFrame) – Site-specific input data for NRWAL calculation.If this input
+is a string, it should be a path that points to a CSV file.
+Otherwise, this input should be a DataFrame with
+pre-extracted site data. Rows in this table should match
+the meta_gid_col in the gen_fpath meta data input
+sites via a gid column. A config column must also be
+provided that corresponds to the nrwal_configs input. Only
+sites with a gid in this file’s gid column will be run
+through NRWAL.
+
sam_files (dict | str) – A dictionary mapping SAM input configuration ID(s) to SAM
+configuration(s). Keys are the SAM config ID(s) which
+correspond to the keys in the nrwal_configs input. Values
+for each key are either a path to a corresponding SAM
+config file or a full dictionary of SAM config inputs. For
+example:
This input can also be a string pointing to a single SAM
+config file. In this case, the config column of the
+CSV points input should be set to None or left out
+completely. See the documentation for the reV SAM class
+(e.g. reV.SAM.generation.WindPower,
+reV.SAM.generation.PvWattsv8,
+reV.SAM.generation.Geothermal, etc.) for
+documentation on the allowed and/or required SAM config file
+inputs.
+
+
nrwal_configs (dict) – A dictionary mapping SAM input configuration ID(s) to NRWAL
+configuration(s). Keys are the SAM config ID(s) which
+correspond to the keys in the sam_files input. Values
+for each key are either a path to a corresponding NRWAL YAML
+or JSON config file or a full dictionary of NRWAL config
+inputs. For example:
output_request (list | tuple) – List of output dataset names to be written to the
+gen_fpath file. Any key from the NRWAL configs or any of
+the inputs (site_data or sam_files) is available to be
+exported as an output dataset. If you want to manipulate a
+dset like cf_mean from gen_fpath and include it in the
+output_request, you should set save_raw=True and then
+use cf_mean_raw in the NRWAL equations as the input.
+This allows you to define an equation in the NRWAL configs
+for a manipulated cf_mean output that can be included in
+the output_request list.
+
save_raw (bool, optional) – Flag to save an initial (“raw”) copy of input datasets from
+gen_fpath that are also part of the output_request. For
+example, if you request cf_mean in output_request but
+also manipulate the cf_mean dataset in the NRWAL
+equations, the original cf_mean will be archived under
+the cf_mean_raw dataset in gen_fpath.
+By default, True.
+
meta_gid_col (str, optional) – Column label in the source meta data from gen_fpath that
+contains the unique gid identifier. This will be joined to
+the site_data gid column. By default, "gid".
+
site_meta_cols (list | tuple, optional) – Column labels from site_data to be added to the meta data
+table in gen_fpath. If None, only the columns in
+DEFAULT_META_COLS will be added. Any columns
+requested via this input will be considered in addition to
+the DEFAULT_META_COLS. By default, None.
Get a dict of NRWAL outputs. Only active analysis sites will have
+data in the output, sites that were not found in the site_data “gid”
+column will not have data in these output arrays
Combine NRWAL outputs with meta and write to output csv.
+
+
Parameters:
+
out_fpath (str, optional) – Full path to output NRWAL CSV file. The file path does not
+need to include file ending - it will be added automatically
+if missing. If None, the generation HDF5 filepath will
+be converted to a CSV out path by replacing the “.h5” file
+ending with “.csv”. By default, None.
csv_output (bool, optional) – Option to write H5 file meta + all requested outputs to
+CSV file instead of storing in the HDF5 file directly. This
+can be useful if the same HDF5 file is used for multiple
+sets of NRWAL runs. Note that all requested output datasets
+must be 1-dimensional in order to fir within the CSV output.
+
+
Important
+
This option is not compatible with
+save_raw=True. If you set csv_output=True, then
+the save_raw option is forced to be False.
+Therefore, make sure that you do not have any references
+to “input_dataset_name_raw” in your NRWAL config. If you
+need to manipulate an input dataset, save it to a
+different output name in the NRWAL config or manually add
+an “input_dataset_name_raw” dataset to your generation
+HDF5 file before running NRWAL.
+
+
By default, False.
+
+
out_fpath (str, optional) – This option has no effect if csv_output=False.
+Otherwise, this should be the full path to output NRWAL CSV
+file. The file path does not need to include file ending -
+it will be added automatically if missing. If None, the
+generation HDF5 filepath will be converted to a CSV out path
+by replacing the “.h5” file ending with “.csv”.
+By default, None.
This module runs reV data through the NRWAL compute library. This code was
+first developed to use a custom offshore wind LCOE equation library but has
+since been refactored to analyze any equation library in NRWAL.
+
Everything in this module operates on the spatiotemporal resolution of the reV
+generation output file. This is usually the wind or solar resource resolution
+but could be the supply curve resolution after representative profiles is run.
reV QA/QC performs quality assurance checks on reV output
+data. Users can specify the type of QA/QC that should be applied
+to each reV module.
+
+
Parameters:
+
+
modules (dict) – Dictionary of modules to QA/QC. Keys should be the names of the
+modules to QA/QC. The values are dictionaries that represent the
+config for the respective QA/QC step. Allowed config keys for
+QA/QC are the “property” attributes of
+QaQcModule.
+
out_dir (str) – Path to output directory.
+
max_workers (int, optional) – Max number of workers to run for QA/QA. If None, uses all
+CPU cores. By default, None.
Framework to handle rep profile for one resource region
+
+
Parameters:
+
+
gen_fpath (str) – Filepath to reV gen output file to extract “cf_profile” from.
+
rev_summary (pd.DataFrame) – Aggregated rev supply curve summary file trimmed to just one
+region to get a rep profile for.
+Must include “res_gids”, “gen_gids”, and the “weight” column (if
+weight is not None)
+
cf_dset (str) – Dataset name to pull generation profiles from.
+
rep_method (str) – Method identifier for calculation of the representative profile.
+
err_method (str | None) – Method identifier for calculation of error from the representative
+profile (e.g. “rmse”, “mae”, “mbe”). If this is None, the
+representative meanoid / medianoid profile will be returned
+directly
+
weight (str | None) – Column in rev_summary used to apply weighted mean to profiles.
+The supply curve table data in the weight column should have
+weight values corresponding to the res_gids in the same row.
+
n_profiles (int) – Number of representative profiles to retrieve.
weights (np.ndarray | None) – Flat array of weight values from the weight column. The supply
+curve table data in the weight column should have a list of weight
+values corresponding to the gen_gids list in the same row.
Class method for parallelization of rep profile calc.
+
+
Parameters:
+
+
gen_fpath (str) – Filepath to reV gen output file to extract “cf_profile” from.
+
rev_summary (pd.DataFrame) – Aggregated rev supply curve summary file trimmed to just one
+region to get a rep profile for.
+Must include “res_gids”, “gen_gids”, and the “weight” column (if
+weight is not None)
+
cf_dset (str) – Dataset name to pull generation profiles from.
+
rep_method (str) – Method identifier for calculation of the representative profile.
+
err_method (str | None) – Method identifier for calculation of error from the representative
+profile (e.g. “rmse”, “mae”, “mbe”). If this is None, the
+representative meanoid / medianoid profile will be returned
+directly
+
weight (str | None) – Column in rev_summary used to apply weighted mean to profiles.
+The supply curve table data in the weight column should have
+weight values corresponding to the res_gids in the same row.
+
n_profiles (int) – Number of representative profiles to retrieve.
+
+
+
Returns:
+
+
rep_profile (np.ndarray) – (time, n_profiles) array for the most representative profile(s)
+
i_rep (list) – Column Index in profiles of the representative profile(s).
+
gen_gid_reps (list) – Generation gid(s) of the representative profile(s).
+
res_gid_reps (list) – Resource gid(s) of the representative profile(s).
reV rep profiles compute representative generation profiles
+for each supply curve point output by reV supply curve
+aggregation. Representative profiles can either be a spatial
+aggregation of generation profiles or actual generation profiles
+that most closely resemble an aggregated profile (selected based
+on an error metric).
+
+
Parameters:
+
+
gen_fpath (str) – Filepath to reV generation output HDF5 file to extract
+cf_dset dataset from.
+
+
Note
+
If executing reV from the command line, this
+path can contain brackets {} that will be filled in by
+the analysis_years input. Alternatively, this input can
+be set to "PIPELINE", which will parse this input from
+one of these preceding pipeline steps: multi-year,
+collect, generation, or
+supply-curve-aggregation. However, note that duplicate
+executions of any of these commands within the pipeline
+may invalidate this parsing, meaning the gen_fpath input
+will have to be specified manually.
+
+
+
rev_summary (str | pd.DataFrame) – Aggregated reV supply curve summary file. Must include
+the following columns:
+
+
+
res_gids : string representation of python list
+containing the resource GID values corresponding to
+each supply curve point.
+
gen_gids : string representation of python list
+containing the reV generation GID values
+corresponding to each supply curve point.
+
weight column (name based on weight input) : string
+representation of python list containing the resource
+GID weights for each supply curve point.
+
+
+
+
Note
+
If executing reV from the command line, this
+input can be set to "PIPELINE", which will parse this
+input from one of these preceding pipeline steps:
+supply-curve-aggregation or supply-curve.
+However, note that duplicate executions of any of these
+commands within the pipeline may invalidate this parsing,
+meaning the rev_summary input will have to be specified
+manually.
+
+
+
reg_cols (str | list) – Label(s) for a categorical region column(s) to extract
+profiles for. For example, "state" will extract a rep
+profile for each unique entry in the "state" column in
+rev_summary. To get a profile for each supply curve point,
+try setting reg_cols to a primary key such as
+"sc_gid".
+
cf_dset (str, optional) – Dataset name to pull generation profiles from. This dataset
+must be present in the gen_fpath HDF5 file. By default,
+"cf_profile"
+
+
Note
+
If executing reV from the command line, this
+name can contain brackets {} that will be filled in by
+the analysis_years input (e.g. "cf_profile-{}").
+
+
+
rep_method ({‘mean’, ‘meanoid’, ‘median’, ‘medianoid’}, optional) – Method identifier for calculation of the representative
+profile. By default, 'meanoid'
+
err_method ({‘mbe’, ‘mae’, ‘rmse’}, optional) – Method identifier for calculation of error from the
+representative profile. If this input is None, the
+representative meanoid / medianoid profile will be returned
+directly. By default, 'rmse'.
+
weight (str, optional) – Column in rev_summary used to apply weights when computing
+mean profiles. The supply curve table data in the weight
+column should have weight values corresponding to the
+res_gids in the same row (i.e. string representation of
+python list containing weight values).
+
+
Important
+
You’ll often want to set this value to
+something other than None (typically "gid_counts"
+if running on standard reV outputs). Otherwise, the
+unique generation profiles within each supply curve point
+are weighted equally. For example, if you have a 64x64
+supply curve point, and one generation profile takes up
+4095 (99.98%) 90m cells while a second generation profile
+takes up only one 90m cell (0.02%), they will contribute
+equally to the meanoid profile unless these weights are
+specified.
+
+
By default, SupplyCurveField.GID_COUNTS.
+
+
n_profiles (int, optional) – Number of representative profiles to save to the output
+file. By default, 1.
+
aggregate_profiles (bool, optional) – Flag to calculate the aggregate (weighted meanoid) profile
+for each supply curve point. This behavior is in lieu of
+finding the single profile per region closest to the
+meanoid. If you set this flag to True, the rep_method,
+err_method, and n_profiles inputs will be forcibly set
+to the default values. By default, False.
Abstract utility framework for representative profile run classes.
+
+
Parameters:
+
+
gen_fpath (str) – Filepath to reV gen output file to extract “cf_profile” from.
+
rev_summary (str | pd.DataFrame) – Aggregated rev supply curve summary file. Str filepath or full df.
+Must include “res_gids”, “gen_gids”, and the “weight” column (if
+weight is not None)
+
reg_cols (str | list | None) – Label(s) for a categorical region column(s) to extract profiles
+for. e.g. “state” will extract a rep profile for each unique entry
+in the “state” column in rev_summary.
+
cf_dset (str) – Dataset name to pull generation profiles from.
+
rep_method (str) – Method identifier for calculation of the representative profile.
+
err_method (str | None) – Method identifier for calculation of error from the representative
+profile (e.g. “rmse”, “mae”, “mbe”). If this is None, the
+representative meanoid / medianoid profile will be returned
+directly
+
weight (str | None) – Column in rev_summary used to apply weighted mean to profiles.
+The supply curve table data in the weight column should have
+weight values corresponding to the res_gids in the same row.
+
n_profiles (int) – Number of representative profiles to save to fout.
weights (np.ndarray | list) – 1D array of weighting factors (multiplicative) for profiles.
+
rep_method (str) – Method identifier for calculation of the representative profile.
+
err_method (str | None) – Method identifier for calculation of error from the representative
+profile (e.g. “rmse”, “mae”, “mbe”). If this is None, the
+representative meanoid / medianoid profile will be returned
+directly
weights (np.ndarray | list) – 1D array of weighting factors (multiplicative) for profiles.
+
rep_method (str) – Method identifier for calculation of the representative profile.
+
err_method (str | None) – Method identifier for calculation of error from the representative
+profile (e.g. “rmse”, “mae”, “mbe”). If this is None, the
+representative meanoid / medianoid profile will be returned
+directly.
+
n_profiles (int) – Number of representative profiles to save to fout.
+
+
+
Returns:
+
+
profiles (np.ndarray) – (time, n_profiles) array for the most representative profile(s)
+
i_reps (list | None) – List (length of n_profiles) with column Index in profiles of the
+representative profile(s). If err_method is None, this value is
+also set to None.
Simple framework to handle aggregation file context managers.
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+by default None
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default ‘queen’
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km,
+by default None
Framework to handle aggregation file context manager:
+- exclusions .h5 file
+- h5 file to be aggregated
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
h5_fpath (str) – Filepath to .h5 file to be aggregated
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+by default None
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default ‘queen’
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km, by default None
+
h5_handler (rex.Resource | None) – Optional special handler similar to the rex.Resource handler which
+is default.
Concrete but generalized aggregation framework to aggregate ANY reV h5
+file to a supply curve grid (based on an aggregated exclusion grid).
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
agg_dset (str) – Dataset to aggreate, can supply multiple datasets. The datasets
+should be scalar values for each site. This method cannot aggregate
+timeseries data.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+by default None
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default “queen”
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km,
+by default None
+
resolution (int, optional) – SC resolution, must be input in combination with gid. Prefered
+option is to use the row/col slices to define the SC point instead,
+by default None
+
excl_area (float, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath,
+by default None
+
gids (list, optional) – List of supply curve point gids to get summary for (can use to
+subset if running in parallel), or None for all gids in the SC
+extent, by default None
+
pre_extract_inclusions (bool, optional) – Optional flag to pre-extract/compute the inclusion mask from the
+provided excl_dict, by default False. Typically faster to compute
+the inclusion mask on the fly with parallel workers.
Standalone method to aggregate - can be parallelized.
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
h5_fpath (str) – Filepath to .h5 file to aggregate
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
agg_dset (str) – Dataset to aggreate, can supply multiple datasets. The datasets
+should be scalar values for each site. This method cannot aggregate
+timeseries data.
+
agg_method (str, optional) – Aggregation method, either mean or sum/aggregate, by default “mean”
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+by default None
+
inclusion_mask (np.ndarray, optional) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. This must be either match the full exclusion shape or
+be a list of single-sc-point exclusion masks corresponding to the
+gids input, by default None
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default “queen”
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km,
+by default None
+
resolution (int, optional) – SC resolution, must be input in combination with gid. Prefered
+option is to use the row/col slices to define the SC point instead,
+by default 0.0081
+
excl_area (float, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath,
+by default None
+
gids (list, optional) – List of supply curve point gids to get summary for (can use to
+subset if running in parallel), or None for all gids in the SC
+extent, by default None
+
gen_index (np.ndarray, optional) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run, by default None
+
+
+
Returns:
+
agg_out (dict) – Aggregated values for each aggregation dataset
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
h5_fpath (str) – Filepath to .h5 file to aggregate
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
agg_dset (str) – Dataset to aggreate, can supply multiple datasets. The datasets
+should be scalar values for each site. This method cannot aggregate
+timeseries data.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+by default None
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default “queen”
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km,
+by default None
+
resolution (int, optional) – SC resolution, must be input in combination with gid. Prefered
+option is to use the row/col slices to define the SC point instead,
+by default None
+
excl_area (float, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath,
+by default None
+
gids (list, optional) – List of supply curve point gids to get summary for (can use to
+subset if running in parallel), or None for all gids in the SC
+extent, by default None
+
pre_extract_inclusions (bool, optional) – Optional flag to pre-extract/compute the inclusion mask from the
+provided excl_dict, by default False. Typically faster to compute
+the inclusion mask on the fly with parallel workers.
+
agg_method (str, optional) – Aggregation method, either mean or sum/aggregate, by default “mean”
+
max_workers (int, optional) – Number of cores to run summary on. None is all available cpus,
+by default None
+
sites_per_worker (int, optional) – Number of SC points to process on a single parallel worker,
+by default 100
Abstract supply curve points aggregation framework based on only an
+exclusion file and techmap.
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+by default None
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default “queen”
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km,
+by default None
+
resolution (int, optional) – SC resolution, must be input in combination with gid. Prefered
+option is to use the row/col slices to define the SC point instead,
+by default None
+
excl_area (float, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
gids (list, optional) – List of supply curve point gids to get summary for (can use to
+subset if running in parallel), or None for all gids in the SC
+extent, by default None
+
pre_extract_inclusions (bool, optional) – Optional flag to pre-extract/compute the inclusion mask from the
+provided excl_dict, by default False. Typically faster to compute
+the inclusion mask on the fly with parallel workers.
Handle competitive wind farm exclusion during supply curve sorting
+
+
Parameters:
+
+
wind_dirs (pandas.DataFrame | str) – path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+the neighboring supply curve point gids and power-rose value at
+each cardinal direction
+
sc_points (pandas.DataFrame | str) – Supply curve point summary table
+
n_dirs (int, optional) – Number of prominent directions to use, by default 2
+
offshore (bool) – Flag as to whether offshore farms should be included during
+CompetitiveWindFarms
Exclude given number of neighboring Supply Point gids based on most
+prominent wind directions
+
+
Parameters:
+
+
wind_dirs (pandas.DataFrame | str) – path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+the neighboring supply curve point gids and power-rose value at
+each cardinal direction
+
sc_points (pandas.DataFrame | str) – Supply curve point summary table
+
n_dirs (int, optional) – Number of prominent directions to use, by default 2
+
offshore (bool) – Flag as to whether offshore farms should be included during
+CompetitiveWindFarms
+
sort_on (str, optional) – column to sort on before excluding neighbors,
+by default ‘total_lcoe’
+
downwind (bool, optional) – Flag to remove downwind neighbors as well as upwind neighbors,
+by default False
+
out_fpath (str, optional) – Path to .csv file to save updated sc_points to,
+by default None
Extract the full inclusion mask from excl_fpath using the given
+exclusion layers and whether or not to run a minimum area filter
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+
area_filter_kernel (str, optional) – Contiguous area filter method to use on final exclusions mask,
+by default “queen”
+
min_area (float, optional) – Minimum required contiguous area filter in sq-km,
+by default None
+
+
+
Returns:
+
inclusion_mask (ndarray) – Pre-computed 2D inclusion mask (normalized with expected range:
+[0, 1], where 1 is included and 0 is excluded)
Class to convert exclusion layer to inclusion layer mask
+
+
Parameters:
+
+
layer (str) – Layer name.
+
exclude_values (int | float | list, optional) – Single value or list of values to exclude.
+
+
Important
+
The keyword arguments exclude_values,
+exclude_range, include_values, include_range,
+include_weights, force_include_values, and
+force_include_range are all mutually exclusive. Users
+should supply value(s) for exactly one of these arguments.
+
+
By default, None.
+
+
exclude_range (list | tuple, optional) – Two-item list of [min threshold, max threshold] (ends are
+inclusive) for values to exclude. Mutually exclusive
+with other inputs (see info in the description of
+exclude_values). By default, None.
+
include_values (int | float | list, optional) – Single value or list of values to include. Mutually
+exclusive with other inputs (see info in the description of
+exclude_values). By default, None.
+
include_range (list | tuple, optional) – Two-item list of [min threshold, max threshold] (ends are
+inclusive) for values to include. Mutually exclusive with
+other inputs (see info in the description of
+exclude_values). By default, None.
+
include_weights (dict, optional) – A dictionary of {value:weight} pairs, where the
+value in the layer that should be included with the
+given weight. Mutually exclusive with other inputs (see
+info in the description of exclude_values).
+By default, None.
+
force_include_values (int | float | list, optional) – Force the inclusion of the given value(s). This input
+completely replaces anything provided as include_values
+and is mutually exclusive with other inputs (eee info in
+the description of exclude_values). By default, None.
+
force_include_range (list | tuple, optional) – Force the inclusion of given values in the range
+[min threshold, max threshold] (ends are inclusive). This
+input completely replaces anything provided as
+include_range and is mutually exclusive with other inputs
+(see info in the description of exclude_values).
+By default, None.
+
use_as_weights (bool, optional) – Option to use layer as final inclusion weights (i.e.
+1 = fully included, 0.75 = 75% included, 0.5 = 50% included,
+etc.). If True, all inclusion/exclusions specifications
+for the layer are ignored and the raw values (scaled by the
+weight input) are used as inclusion weights.
+By default, False.
+
weight (float, optional) – Weight applied to exclusion layer after it is calculated.
+Can be used, for example, to turn a binary exclusion layer
+(i.e. data with 0 or 1 values and exclude_values=1
+input) into partial exclusions by setting the weight to
+a fraction (e.g. 0.5 for 50% exclusions). By default, 1.
+
exclude_nodata (bool, optional) – Flag to exclude nodata values (nodata_value). If
+nodata_value=None the nodata_value is inferred by
+reV.supply_curve.exclusions.ExclusionMask.
+By default, False.
+
nodata_value (int | float, optional) – Nodata value for the layer. If None, the value will be
+inferred when LayerMask is added to
+reV.supply_curve.exclusions.ExclusionMask.
+By default, None.
+
extent (dict, optional) – Optional dictionary with values that can be used to
+initialize this class (i.e. layer, exclude_values,
+include_range, etc.). This dictionary should contain the
+specifications to create a boolean mask that defines the
+extent to which the original mask should be applied.
+For example, suppose you specify the input the following
+way:
This would mean that you are masking out all viewshed layer
+values equal to 1, but only where the “federal_parks”
+layer is equal to 1, 2, 3, 4, or 5. Outside of these
+regions (i.e. outside of federal park regions), the viewshed
+exclusion is NOT applied. If the extent mask created by
+these options is not boolean, an error is thrown (i.e. do
+not specify weight or use_as_weights).
+By default None, which applies the original layer mask
+to the full extent.
+
+
**kwargs – Optional inputs to maintain legacy kwargs of inclusion_*
+instead of include_*.
Supply curve full extent framework. This class translates the 90m
+exclusion grid to the aggregated supply curve resolution.
+
+
Parameters:
+
+
f_excl (str | list | tuple | ExclusionLayers) – File path(s) to the exclusions grid, or pre-initialized
+ExclusionLayers. The exclusions dictate the SC analysis extent.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
List representing the supply curve points rows and which
+exclusions rows belong to each supply curve row.
+
+
Returns:
+
_rows_of_excl (list) – List representing the supply curve points rows. Each list entry
+contains the exclusion row indices that are included in the sc
+point.
List representing the supply curve points columns and which
+exclusions columns belong to each supply curve column.
+
+
Returns:
+
_cols_of_excl (list) – List representing the supply curve points columns. Each list entry
+contains the exclusion column indices that are included in the sc
+point.
List representing the supply curve points rows and which
+exclusions rows belong to each supply curve row.
+
+
Returns:
+
_excl_row_slices (list) – List representing the supply curve points rows. Each list entry
+contains the exclusion row slice that are included in the sc
+point.
List representing the supply curve points cols and which
+exclusions cols belong to each supply curve col.
+
+
Returns:
+
_excl_col_slices (list) – List representing the supply curve points cols. Each list entry
+contains the exclusion col slice that are included in the sc
+point.
Get a 1D array of row indices for every gid. That is, this property
+has length == len(gids) and row_indices[sc_gid] yields the row index of
+the target supply curve gid
Get a 1D array of col indices for every gid. That is, this property
+has length == len(gids) and col_indices[sc_gid] yields the col index of
+the target supply curve gid
Generic single SC point to aggregate data from an h5 file.
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
agg_h5 (str | Resource) – Filepath to .h5 file to aggregate or Resource handler
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
gen_index (np.ndarray) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run.
+
apply_exclusions (bool) – Flag to apply exclusions to the resource / generation gid’s on
+initialization.
Get the sum of the inclusion values in each resource/generation gid
+corresponding to this sc point. The sum of the gid counts can be less
+than the value provided by n_gids if fractional exclusion/inclusions
+are provided.
Compute exclusions weight mean for the sc point from data
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
agg_h5 (str | Resource) – Filepath to .h5 file to aggregate or Resource handler
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
agg_dset (str) – Dataset to aggreate, can supply multiple datasets or no datasets.
+The datasets should be scalar values for each site. This method
+cannot aggregate timeseries data.
+
agg_method (str) – Aggregation method, either mean or sum/aggregate
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
gen_index (np.ndarray) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run.
+
+
+
Returns:
+
out (dict) – Given datasets and meta data aggregated to supply curve points
Perform additional data layer aggregation. If there is no valid data
+in the included area, the data layer will be taken from the full SC
+point extent (ignoring exclusions). If there is still no valid data,
+a warning will be raised and the data layer will have a NaN/None value.
+
+
Parameters:
+
+
summary (dict) – Dictionary of summary outputs for this sc point.
+
data_layers (None | dict) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”.
+
+
+
Returns:
+
summary (dict) – Dictionary of summary outputs for this sc point. A new entry for
+each data layer is added.
Calc the exclusions-weighted mean value of an array of resource data.
+
+
Parameters:
+
+
arr (np.ndarray) – Array of resource data.
+
drop_nan (bool) – Flag to drop nan values from the mean calculation (only works for
+1D arr input, profiles should not have NaN’s)
+
+
+
Returns:
+
mean (float | np.ndarray) – Mean of arr masked by the binary exclusions then weighted by
+the non-zero exclusions. This will be a 1D numpy array if the
+input data is a 2D numpy array (averaged along axis=1)
The area in km2 of a single exclusion pixel. If this value was not
+provided on initialization, it is determined from the profile of the
+exclusion file.
Compute exclusions weight mean for the sc point from data
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
data (ndarray | ResourceDataset) – Array of data or open dataset handler to apply exclusions too
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit
+
+
+
Returns:
+
ndarray – Exclusions weighted means of data for supply curve point
Compute the aggregate (sum) of data for the sc point
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
data (ndarray | ResourceDataset) – Array of data or open dataset handler to apply exclusions too
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
+
+
Returns:
+
ndarray – Sum / aggregation of data for supply curve point
Supply curve point summary framework that ties a reV SC point to its
+respective generation and resource data.
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
gen (str | reV.handlers.Outputs) – Filepath to .h5 reV generation output results or reV Outputs file
+handler.
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
gen_index (np.ndarray) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
res_class_dset (str | np.ndarray | None) – Dataset in the generation file dictating resource classes.
+Can be pre-extracted resource data in np.ndarray.
+None if no resource classes.
+
res_class_bin (list | None) – Two-entry lists dictating the single resource class bin.
+None if no resource classes.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
power_density (float | None | pd.DataFrame) – Constant power density float, None, or opened dataframe with
+(resource) “gid” and “power_density columns”.
+
cf_dset (str | np.ndarray) – Dataset name from gen containing capacity factor mean values.
+This name is used to infer AC capacity factor dataset for
+solar runs (i.e. the AC vsersion of “cf_mean-means” would
+be inferred to be “cf_mean_ac-means”). This input can also
+be pre-extracted generation output data in np.ndarray, in
+which case all DC solar outputs are set to None.
+
lcoe_dset (str | np.ndarray) – Dataset name from gen containing LCOE mean values.
+Can be pre-extracted generation output data in np.ndarray.
+
h5_dsets (None | list | dict) – Optional list of dataset names to summarize from the gen/econ h5
+files. Can also be pre-extracted data dictionary where keys are
+the dataset names and values are the arrays of data from the
+h5 files.
+
resolution (int | None) – SC resolution, must be input in combination with gid.
+
exclusion_shape (tuple) – Shape of the exclusions extent (rows, cols). Inputing this will
+speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
friction_layer (None | FrictionMask) – Friction layer with scalar friction values if valid friction inputs
+were entered. Otherwise, None to not apply friction layer.
+
recalc_lcoe (bool) – Flag to re-calculate the LCOE from the multi-year mean capacity
+factor and annual energy production data. This requires several
+datasets to be aggregated in the gen input: system_capacity,
+fixed_charge_rate, capital_cost, fixed_operating_cost,
+and variable_operating_cost.
+
apply_exclusions (bool) – Flag to apply exclusions to the resource / generation gid’s on
+initialization.
Calc the exclusions-weighted mean value of a flat array of gen data.
+
+
Parameters:
+
flat_arr (np.ndarray) – Flattened array of resource/generation/econ data. Must be
+index-able with the self._gen_gids array (must be a 1D array with
+an entry for every site in the generation extent).
+
+
Returns:
+
mean (float) – Mean of flat_arr masked by the binary exclusions then weighted by
+the non-zero exclusions.
This output is only not None for solar runs where cf_dset
+was specified as a string.
+
+
Returns:
+
gen_ac_data (np.ndarray | None) – Multi-year-mean ac capacity factor data array for all sites
+in the generation data output file or None if none
+detected.
Get the mean capacity factor for the non-excluded data. Capacity
+factor is weighted by the exclusions (usually 0 or 1, but 0.5
+exclusions will weight appropriately).
+
This value represents DC capacity factor for solar and AC
+capacity factor for all other technologies. This is the capacity
+factor that should be used for all cost calculations for ALL
+technologies (to align with SAM).
+
+
Returns:
+
mean_cf (float | None) – Mean capacity factor value for the non-excluded data.
Get the mean friction scalar for the non-excluded data.
+
+
Returns:
+
friction (None | float) – Mean value of the friction data layer for the non-excluded data.
+If friction layer is not input to this class, None is returned.
Get the friction data for the full SC point (no exclusions)
+
+
Returns:
+
friction_data (None | np.ndarray) – 2D friction data layer corresponding to the exclusions grid in
+the SC domain. If friction layer is not input to this class,
+None is returned.
Get the estimated AC power density either from input or
+inferred from generation output meta.
+
This value is only available for solar runs with a “dc_ac_ratio”
+dataset in the generation file. If these conditions are not met,
+this value is None.
+
+
Returns:
+
_power_density_ac (float | None) – Estimated AC power density in MW/km2
Get the estimated capacity in MW of the supply curve point in the
+current resource class with the applied exclusions.
+
This value represents DC capacity for solar and AC capacity for
+all other technologies. This is the capacity that should be used
+for all cost calculations for ALL technologies (to align with
+SAM).
+
+
Returns:
+
capacity (float) – Estimated capacity in MW of the supply curve point in the
+current resource class with the applied exclusions.
Get the AC estimated capacity in MW of the supply curve point in the
+current resource class with the applied exclusions.
+
This value is provided only for solar inputs that have
+the “dc_ac_ratio” dataset in the generation file. If these
+conditions are not met, this value is None.
+
+
Returns:
+
capacity (float | None) – Estimated AC capacity in MW of the supply curve point in the
+current resource class with the applied exclusions. Only not
+None for solar runs with “dc_ac_ratio” dataset in the
+generation file
Get the DC estimated capacity in MW of the supply curve point
+in the current resource class with the applied exclusions.
+
This value is provided only for solar inputs that have
+the “dc_ac_ratio” dataset in the generation file. If these
+conditions are not met, this value is None.
+
+
Returns:
+
capacity (float | None) – Estimated AC capacity in MW of the supply curve point in the
+current resource class with the applied exclusions. Only not
+None for solar runs with “dc_ac_ratio” dataset in the
+generation file
Get the total annual energy (MWh) for the entire SC point.
+
This value is computed using the capacity of the supply curve
+point as well as the mean capacity factor. If the mean capacity
+factor is None, this value will also be None.
+
+
Returns:
+
sc_point_annual_energy (float | None) – Total annual energy (MWh) for the entire SC point.
cap_cost_scale (str) – LCOE scaling equation to implement “economies of scale”.
+Equation must be in python string format and return a scalar
+value to multiply the capital cost by. Independent variables in
+the equation should match the names of the columns in the reV
+supply curve aggregation table.
+
summary (dict) – Dictionary of summary outputs for this sc point.
+
+
+
Returns:
+
summary (dict) – Dictionary of summary outputs for this sc point.
Get a summary dictionary of a single supply curve point.
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl_fpath (str) – Filepath to exclusions h5.
+
gen_fpath (str) – Filepath to .h5 reV generation output results.
+
tm_dset (str) – Dataset name in the techmap file containing the
+exclusions-to-resource mapping data.
+
gen_index (np.ndarray) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
res_class_dset (str | np.ndarray | None) – Dataset in the generation file dictating resource classes.
+Can be pre-extracted resource data in np.ndarray.
+None if no resource classes.
+
res_class_bin (list | None) – Two-entry lists dictating the single resource class bin.
+None if no resource classes.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
power_density (float | None | pd.DataFrame) – Constant power density float, None, or opened dataframe with
+(resource) “gid” and “power_density columns”.
+
cf_dset (str | np.ndarray) – Dataset name from gen containing capacity factor mean values.
+Can be pre-extracted generation output data in np.ndarray.
+
lcoe_dset (str | np.ndarray) – Dataset name from gen containing LCOE mean values.
+Can be pre-extracted generation output data in np.ndarray.
+
h5_dsets (None | list | dict) – Optional list of dataset names to summarize from the gen/econ h5
+files. Can also be pre-extracted data dictionary where keys are
+the dataset names and values are the arrays of data from the
+h5 files.
+
resolution (int | None) – SC resolution, must be input in combination with gid.
+
exclusion_shape (tuple) – Shape of the exclusions extent (rows, cols). Inputing this will
+speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
friction_layer (None | FrictionMask) – Friction layer with scalar friction values if valid friction inputs
+were entered. Otherwise, None to not apply friction layer.
+
args (tuple | list, optional) – List of summary arguments to include. None defaults to all
+available args defined in the class attr, by default None
+
data_layers (dict, optional) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”, by default None
+
cap_cost_scale (str | None) – Optional LCOE scaling equation to implement “economies of scale”.
+Equations must be in python string format and return a scalar
+value to multiply the capital cost by. Independent variables in
+the equation should match the names of the columns in the reV
+supply curve aggregation table.
+
recalc_lcoe (bool) – Flag to re-calculate the LCOE from the multi-year mean capacity
+factor and annual energy production data. This requires several
+datasets to be aggregated in the gen input: system_capacity,
+fixed_charge_rate, capital_cost, fixed_operating_cost,
+and variable_operating_cost.
+
+
+
Returns:
+
summary (dict) – Dictionary of summary outputs for this sc point.
Perform additional data layer aggregation. If there is no valid data
+in the included area, the data layer will be taken from the full SC
+point extent (ignoring exclusions). If there is still no valid data,
+a warning will be raised and the data layer will have a NaN/None value.
+
+
Parameters:
+
+
summary (dict) – Dictionary of summary outputs for this sc point.
+
data_layers (None | dict) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”.
+
+
+
Returns:
+
summary (dict) – Dictionary of summary outputs for this sc point. A new entry for
+each data layer is added.
The area in km2 of a single exclusion pixel. If this value was not
+provided on initialization, it is determined from the profile of the
+exclusion file.
Compute exclusions weight mean for the sc point from data
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
agg_h5 (str | Resource) – Filepath to .h5 file to aggregate or Resource handler
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
agg_dset (str) – Dataset to aggreate, can supply multiple datasets or no datasets.
+The datasets should be scalar values for each site. This method
+cannot aggregate timeseries data.
+
agg_method (str) – Aggregation method, either mean or sum/aggregate
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
gen_index (np.ndarray) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run.
+
+
+
Returns:
+
out (dict) – Given datasets and meta data aggregated to supply curve points
Compute exclusions weight mean for the sc point from data
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
data (ndarray | ResourceDataset) – Array of data or open dataset handler to apply exclusions too
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit
+
+
+
Returns:
+
ndarray – Exclusions weighted means of data for supply curve point
Compute the aggregate (sum) of data for the sc point
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
data (ndarray | ResourceDataset) – Array of data or open dataset handler to apply exclusions too
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
+
+
Returns:
+
ndarray – Sum / aggregation of data for supply curve point
Generic single SC point based on exclusions, resolution, and techmap
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | list | tuple | ExclusionMask) – Filepath(s) to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
inclusion_mask (np.ndarray) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. The shape of this will be checked against the input
+resolution.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
The area in km2 of a single exclusion pixel. If this value was not
+provided on initialization, it is determined from the profile of the
+exclusion file.
Calc the exclusions-weighted mean value of an array of resource data.
+
+
Parameters:
+
+
arr (np.ndarray) – Array of resource data.
+
drop_nan (bool) – Flag to drop nan values from the mean calculation (only works for
+1D arr input, profiles should not have NaN’s)
+
+
+
Returns:
+
mean (float | np.ndarray) – Mean of arr masked by the binary exclusions then weighted by
+the non-zero exclusions. This will be a 1D numpy array if the
+input data is a 2D numpy array (averaged along axis=1)
Compute exclusions weight mean for the sc point from data
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
data (ndarray | ResourceDataset) – Array of data or open dataset handler to apply exclusions too
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit
+
+
+
Returns:
+
ndarray – Exclusions weighted means of data for supply curve point
Compute the aggregate (sum) of data for the sc point
+
+
Parameters:
+
+
gid (int) – gid for supply curve point to analyze.
+
excl (str | ExclusionMask) – Filepath to exclusions h5 or ExclusionMask file handler.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
data (ndarray | ResourceDataset) – Array of data or open dataset handler to apply exclusions too
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+None if excl input is pre-initialized.
+
resolution (int) – Number of exclusion points per SC point along an axis.
+This number**2 is the total number of exclusion points per
+SC point.
+
exclusion_shape (tuple) – Shape of the full exclusions extent (rows, cols). Inputing this
+will speed things up considerably.
+
close (bool) – Flag to close object file handlers on exit.
+
+
+
Returns:
+
ndarray – Sum / aggregation of data for supply curve point
Perform additional data layer aggregation. If there is no valid data
+in the included area, the data layer will be taken from the full SC
+point extent (ignoring exclusions). If there is still no valid data,
+a warning will be raised and the data layer will have a NaN/None value.
+
+
Parameters:
+
+
summary (dict) – Dictionary of summary outputs for this sc point.
+
data_layers (None | dict) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”.
+
+
+
Returns:
+
summary (dict) – Dictionary of summary outputs for this sc point. A new entry for
+each data layer is added.
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
gen_fpath (str) – Filepath to .h5 reV generation output results.
+
econ_fpath (str | None) – Filepath to .h5 reV econ output results. This is optional and only
+used if the lcoe_dset is not present in the gen_fpath file.
+
data_layers (None | dict) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”.
+
power_density (float | str | None) – Power density in MW/km2 or filepath to variable power
+density file. None will attempt to infer a constant
+power density from the generation meta data technology.
+Variable power density csvs must have “gid” and “power_density”
+columns where gid is the resource gid (typically wtk or nsrdb gid)
+and the power_density column is in MW/km2.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+
friction_fpath (str | None) – Filepath to friction surface data (cost based exclusions).
+Must be paired with friction_dset. The friction data must be the
+same shape as the exclusions. Friction input creates a new output
+“mean_lcoe_friction” which is the nominal LCOE multiplied by the
+friction data.
+
friction_dset (str | None) – Dataset name in friction_fpath for the friction surface data.
+Must be paired with friction_fpath. Must be same shape as
+exclusions.
+
area_filter_kernel (str) – Contiguous area filter method to use on final exclusions mask
+
min_area (float | None) – Minimum required contiguous area filter in sq-km
_power_density (float | None | pd.DataFrame) – Constant power density float, None, or opened dataframe with
+(resource) “gid” and “power_density columns”.
reV supply curve aggregation combines a high-resolution
+(e.g. 90m) exclusion dataset with a (typically) lower resolution
+(e.g. 2km) generation dataset by mapping all data onto the high-
+resolution grid and aggregating it by a large factor (e.g. 64 or
+128). The result is coarsely-gridded data that summarizes
+capacity and generation potential as well as associated
+economics under a particular land access scenario. This module
+can also summarize extra data layers during the aggregation
+process, allowing for complementary land characterization
+analysis.
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions data HDF5 file. The exclusions HDF5
+file should contain the layers specified in excl_dict
+and data_layers. These layers may also be spread out
+across multiple HDF5 files, in which case this input should
+be a list or tuple of filepaths pointing to the files
+containing the layers. Note that each data layer must be
+uniquely defined (i.e.only appear once and in a single
+input file).
+
tm_dset (str) – Dataset name in the excl_fpath file containing the
+techmap (exclusions-to-resource mapping data). This data
+layer links the supply curve GID’s to the generation GID’s
+that are used to evaluate performance metrics such as
+mean_cf.
+
+
Important
+
This dataset uniquely couples the (typically
+high-resolution) exclusion layers to the (typically
+lower-resolution) resource data. Therefore, a separate
+techmap must be used for every unique combination of
+resource and exclusion coordinates.
+
+
+
Note
+
If executing reV from the command line, you
+can specify a name that is not in the exclusions HDF5
+file, and reV will calculate the techmap for you. Note
+however that computing the techmap and writing it to the
+exclusion HDF5 file is a blocking operation, so you may
+only run a single reV aggregation step at a time this
+way.
+
+
+
econ_fpath (str, optional) – Filepath to HDF5 file with reV econ output results
+containing an lcoe_dset dataset. If None, lcoe_dset
+should be a dataset in the gen_fpath HDF5 file that
+aggregation is executed on.
+
+
Note
+
If executing reV from the command line, this
+input can be set to "PIPELINE" to parse the output
+from one of these preceding pipeline steps:
+multi-year, collect, or generation. However,
+note that duplicate executions of any of these commands
+within the pipeline may invalidate this parsing, meaning
+the econ_fpath input will have to be specified manually.
+
+
By default, None.
+
+
excl_dict (dict | None) – Dictionary of exclusion keyword arguments of the format
+{layer_dset_name:{kwarg:value}}, where
+layer_dset_name is a dataset in the exclusion h5 file
+and the kwarg:value pair is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+For example:
Note that all the keys given in this dictionary should be
+datasets of the excl_fpath file. If None or empty
+dictionary, no exclusions are applied. By default, None.
+
+
area_filter_kernel ({“queen”, “rook”}, optional) – Contiguous area filter method to use on final exclusions
+mask. The filters are defined as:
These filters define how neighboring pixels are “connected”.
+Once pixels in the final exclusion layer are connected, the
+area of each resulting cluster is computed and compared
+against the min_area input. Any cluster with an area
+less than min_area is excluded from the final mask.
+This argument has no effect if min_area is None.
+By default, "queen".
+
+
min_area (float, optional) – Minimum area (in km2) required to keep an isolated
+cluster of (included) land within the resulting exclusions
+mask. Any clusters of land with areas less than this value
+will be marked as exclusions. See the documentation for
+area_filter_kernel for an explanation of how the area of
+each land cluster is computed. If None, no area
+filtering is performed. By default, None.
+
resolution (int, optional) – Supply Curve resolution. This value defines how many pixels
+are in a single side of a supply curve cell. For example,
+a value of 64 would generate a supply curve where the
+side of each supply curve cell is 64x64 exclusion
+pixels. By default, 64.
+
excl_area (float, optional) – Area of a single exclusion mask pixel (in km2).
+If None, this value will be inferred from the profile
+transform attribute in excl_fpath. By default, None.
+
res_fpath (str, optional) – Filepath to HDF5 resource file (e.g. WTK or NSRDB). This
+input is required if techmap dset is to be created or if the
+gen_fpath input to the summarize or run methods
+is None. By default, None.
+
gids (list, optional) – List of supply curve point gids to get summary for. If you
+would like to obtain all available reV supply curve
+points to run, you can use the
+reV.supply_curve.extent.SupplyCurveExtent class
+like so:
If None, supply curve aggregation is computed for all
+gids in the supply curve extent. By default, None.
+
+
pre_extract_inclusions (bool, optional) – Optional flag to pre-extract/compute the inclusion mask from
+the excl_dict input. It is typically faster to compute
+the inclusion mask on the fly with parallel workers.
+By default, False.
+
res_class_dset (str, optional) – Name of dataset in the reV generation HDF5 output file
+containing resource data. If None, no aggregated
+resource classification is performed (i.e. no mean_res
+output), and the res_class_bins is ignored.
+By default, None.
+
res_class_bins (list, optional) – Optional input to perform separate aggregations for various
+resource data ranges. If None, only a single aggregation
+per supply curve point is performed. Otherwise, this input
+should be a list of floats or ints representing the resource
+bin boundaries. One aggregation per resource value range is
+computed, and only pixels within the given resource range
+are aggregated. By default, None.
+
cf_dset (str, optional) – Dataset name from the reV generation HDF5 output file
+containing a 1D dataset of mean capacity factor values. This
+dataset will be mapped onto the high-resolution grid and
+used to compute the mean capacity factor for non-excluded
+area. By default, "cf_mean-means".
+
lcoe_dset (str, optional) – Dataset name from the reV generation HDF5 output file
+containing a 1D dataset of mean LCOE values. This
+dataset will be mapped onto the high-resolution grid and
+used to compute the mean LCOE for non-excluded area, but
+only if the LCOE is not re-computed during processing (see
+the recalc_lcoe input for more info).
+By default, "lcoe_fcr-means".
+
h5_dsets (list, optional) – Optional list of additional datasets from the reV
+generation/econ HDF5 output file to aggregate. If None,
+no extra datasets are aggregated.
+
+
Warning
+
This input is meant for passing through 1D
+datasets. If you specify a 2D or higher-dimensional
+dataset, you may run into memory errors. If you wish to
+aggregate 2D datasets, see the rep-profiles module.
+
+
By default, None.
+
+
data_layers (dict, optional) –
+
Dictionary of aggregation data layers of the format:
The "output_layer_name" is the column name under which
+the aggregated data will appear in the output CSV file. The
+"output_layer_name" does not have to match the dset
+input value. The latter should match the layer name in the
+HDF5 from which the data to aggregate should be pulled. The
+method should be one of
+{"mode","mean","min","max","sum","category"},
+describing how the high-resolution data should be aggregated
+for each supply curve point. fpath is an optional key
+that can point to an HDF5 file containing the layer data. If
+left out, the data is assumed to exist in the file(s)
+specified by the excl_fpath input. If None, no data
+layer aggregation is performed. By default, None
+
+
power_density (float | str, optional) – Power density value (in MW/km2) or filepath to
+variable power density CSV file containing the following
+columns:
+
+
+
gid : resource gid (typically wtk or nsrdb gid)
+
power_density : power density value (in
+MW/km2)
+
+
+
If None, a constant power density is inferred from the
+generation meta data technology. By default, None.
+
+
friction_fpath (str, optional) – Filepath to friction surface data (cost based exclusions).
+Must be paired with the friction_dset input below. The
+friction data must be the same shape as the exclusions.
+Friction input creates a new output column
+"mean_lcoe_friction" which is the nominal LCOE
+multiplied by the friction data. If None, no friction
+data is aggregated. By default, None.
+
friction_dset (str, optional) – Dataset name in friction_fpath for the friction surface
+data. Must be paired with the friction_fpath above. If
+None, no friction data is aggregated.
+By default, None.
+
cap_cost_scale (str, optional) – Optional LCOE scaling equation to implement “economies of
+scale”. Equations must be in python string format and must
+return a scalar value to multiply the capital cost by.
+Independent variables in the equation should match the names
+of the columns in the reV supply curve aggregation
+output table (see the documentation of
+SupplyCurveAggregation
+for details on available outputs). If None, no economies
+of scale are applied. By default, None.
+
recalc_lcoe (bool, optional) – Flag to re-calculate the LCOE from the multi-year mean
+capacity factor and annual energy production data. This
+requires several datasets to be aggregated in the h5_dsets
+input:
+
+
+
system_capacity
+
fixed_charge_rate
+
capital_cost
+
fixed_operating_cost
+
variable_operating_cost
+
+
+
If any of these datasets are missing from the reV
+generation HDF5 output, or if recalc_lcoe is set to
+False, the mean LCOE will be computed from the data
+stored under the lcoe_dset instead. By default, True.
+
+
+
+
+
Examples
+
Standard outputs:
+
+
sc_gidint
Unique supply curve gid. This is the enumerated supply curve
+points, which can have overlapping geographic locations due
+to different resource bins at the same geographic SC point.
+
+
res_gidslist
Stringified list of resource gids (e.g. original WTK or
+NSRDB resource GIDs) corresponding to each SC point.
+
+
gen_gidslist
Stringified list of generation gids (e.g. GID in the reV
+generation output, which corresponds to the reV project
+points and not necessarily the resource GIDs).
+
+
gid_countslist
Stringified list of the sum of inclusion scalar values
+corresponding to each gen_gid and res_gid, where 1 is
+included, 0 is excluded, and 0.7 is included with 70 percent
+of available land. Each entry in this list is associated
+with the corresponding entry in the gen_gids and
+res_gids lists.
+
+
n_gidsint
Total number of included pixels. This is a boolean sum and
+considers partial inclusions to be included (e.g. 1).
+
+
mean_cffloat
Mean capacity factor of each supply curve point (the
+arithmetic mean is weighted by the inclusion layer)
+(unitless).
+
+
mean_lcoefloat
Mean LCOE of each supply curve point (the arithmetic mean is
+weighted by the inclusion layer). Units match the reV econ
+output ($/MWh). By default, the LCOE is re-calculated using
+the multi-year mean capacity factor and annual energy
+production. This requires several datasets to be aggregated
+in the h5_dsets input: fixed_charge_rate,
+capital_cost,
+fixed_operating_cost, annual_energy_production, and
+variable_operating_cost. This recalc behavior can be
+disabled by setting recalc_lcoe=False.
+
+
mean_resfloat
Mean resource, the resource dataset to average is provided
+by the user in res_class_dset. The arithmetic mean is
+weighted by the inclusion layer.
+
+
capacityfloat
Total capacity of each supply curve point (MW). Units are
+contingent on the power_density input units of MW/km2.
+
+
area_sq_kmfloat
Total included area for each supply curve point in km2. This
+is based on the nominal area of each exclusion pixel which
+by default is calculated from the exclusion profile
+attributes. The NREL reV default is 0.0081 km2 pixels
+(90m x 90m). The area sum considers partial inclusions.
+
+
latitudefloat
Supply curve point centroid latitude coordinate, in degrees
+(does not consider exclusions).
+
+
longitudefloat
Supply curve point centroid longitude coordinate, in degrees
+(does not consider exclusions).
+
+
countrystr
Country of the supply curve point based on the most common
+country of the associated resource meta data. Does not
+consider exclusions.
+
+
statestr
State of the supply curve point based on the most common
+state of the associated resource meta data. Does not
+consider exclusions.
+
+
countystr
County of the supply curve point based on the most common
+county of the associated resource meta data. Does not
+consider exclusions.
+
+
elevationfloat
Mean elevation of the supply curve point based on the mean
+elevation of the associated resource meta data. Does not
+consider exclusions.
+
+
timezoneint
UTC offset of local timezone based on the most common
+timezone of the associated resource meta data. Does not
+consider exclusions.
+
+
sc_point_gidint
Spatially deterministic supply curve point gid. Duplicate
+sc_point_gid values can exist due to resource binning.
+
+
sc_row_indint
Row index of the supply curve point in the aggregated
+exclusion grid.
+
+
sc_col_indint
Column index of the supply curve point in the aggregated
+exclusion grid
+
+
res_classint
Resource class for the supply curve gid. Each geographic
+supply curve point (sc_point_gid) can have multiple
+resource classes associated with it, resulting in multiple
+supply curve gids (sc_gid) associated with the same
+spatially deterministic supply curve point.
+
+
+
Optional outputs:
+
+
mean_frictionfloat
Mean of the friction data provided in ‘friction_fpath’ and
+‘friction_dset’. The arithmetic mean is weighted by boolean
+inclusions and considers partial inclusions to be included.
+
+
mean_lcoe_frictionfloat
Mean of the nominal LCOE multiplied by mean_friction value.
+
+
mean_{dset}float
Mean input h5 dataset(s) provided by the user in ‘h5_dsets’.
+These mean calculations are weighted by the partial
+inclusion layer.
+
+
data_layersfloat | int | str | dict
Requested data layer aggregations, each data layer must be
+the same shape as the exclusion layers.
+
+
+
+
mode: int | str
Most common value of a given data layer after
+applying the boolean inclusion mask.
+
+
+
+
+
meanfloat
Arithmetic mean value of a given data layer weighted
+by the scalar inclusion mask (considers partial
+inclusions).
+
+
+
+
+
minfloat | int
Minimum value of a given data layer after applying
+the boolean inclusion mask.
+
+
+
+
+
maxfloat | int
Maximum value of a given data layer after applying
+the boolean inclusion mask.
+
+
+
+
+
sumfloat
Sum of a given data layer weighted by the scalar
+inclusion mask (considers partial inclusions).
+
+
+
+
+
categorydict
Dictionary mapping the unique values in the
+data_layer to the sum of inclusion scalar values
+associated with all pixels with that unique value.
Standalone method to create agg summary - can be parallelized.
+
+
Parameters:
+
+
excl_fpath (str | list | tuple) – Filepath to exclusions h5 with techmap dataset
+(can be one or more filepaths).
+
gen_fpath (str) – Filepath to .h5 reV generation output results.
+
tm_dset (str) – Dataset name in the exclusions file containing the
+exclusions-to-resource mapping data.
+
gen_index (np.ndarray) – Array of generation gids with array index equal to resource gid.
+Array value is -1 if the resource index was not used in the
+generation run.
+
econ_fpath (str | None) – Filepath to .h5 reV econ output results. This is optional and only
+used if the lcoe_dset is not present in the gen_fpath file.
+
excl_dict (dict | None) – Dictionary of exclusion keyword arugments of the format
+{layer_dset_name: {kwarg: value}} where layer_dset_name is a
+dataset in the exclusion h5 file and kwarg is a keyword argument to
+the reV.supply_curve.exclusions.LayerMask class.
+
inclusion_mask (np.ndarray | dict | optional) – 2D array pre-extracted inclusion mask where 1 is included and 0 is
+excluded. This must be either match the full exclusion shape or
+be a dict lookup of single-sc-point exclusion masks corresponding
+to the gids input and keyed by gids, by default None which will
+calculate exclusions on the fly for each sc point.
+
area_filter_kernel (str) – Contiguous area filter method to use on final exclusions mask
+
min_area (float | None) – Minimum required contiguous area filter in sq-km
+
resolution (int | None) – SC resolution, must be input in combination with gid. Prefered
+option is to use the row/col slices to define the SC point instead.
+
gids (list | None) – List of supply curve point gids to get summary for (can use to
+subset if running in parallel), or None for all gids in the SC
+extent, by default None
+
args (list | None) – List of positional args for sc_point_method
+
res_class_dset (str | None) – Dataset in the generation file dictating resource classes.
+None if no resource classes.
+
res_class_bins (list | None) – List of two-entry lists dictating the resource class bins.
+None if no resource classes.
+
cf_dset (str) – Dataset name from f_gen containing capacity factor mean values.
+
lcoe_dset (str) – Dataset name from f_gen containing LCOE mean values.
+
h5_dsets (list | None) – Optional list of additional datasets from the source h5 gen/econ
+files to aggregate.
+
data_layers (None | dict) – Aggregation data layers. Must be a dictionary keyed by data label
+name. Each value must be another dictionary with “dset”, “method”,
+and “fpath”.
+
power_density (float | str | None) – Power density in MW/km2 or filepath to variable power
+density file. None will attempt to infer a constant
+power density from the generation meta data technology.
+Variable power density csvs must have “gid” and “power_density”
+columns where gid is the resource gid (typically wtk or nsrdb gid)
+and the power_density column is in MW/km2.
+
friction_fpath (str | None) – Filepath to friction surface data (cost based exclusions).
+Must be paired with friction_dset. The friction data must be the
+same shape as the exclusions. Friction input creates a new output
+“mean_lcoe_friction” which is the nominal LCOE multiplied by the
+friction data.
+
friction_dset (str | None) – Dataset name in friction_fpath for the friction surface data.
+Must be paired with friction_fpath. Must be same shape as
+exclusions.
+
excl_area (float | None, optional) – Area of an exclusion pixel in km2. None will try to infer the area
+from the profile transform attribute in excl_fpath, by default None
+
cap_cost_scale (str | None) – Optional LCOE scaling equation to implement “economies of scale”.
+Equations must be in python string format and return a scalar
+value to multiply the capital cost by. Independent variables in
+the equation should match the names of the columns in the reV
+supply curve aggregation table.
+
recalc_lcoe (bool) – Flag to re-calculate the LCOE from the multi-year mean capacity
+factor and annual energy production data. This requires several
+datasets to be aggregated in the h5_dsets input: system_capacity,
+fixed_charge_rate, capital_cost, fixed_operating_cost,
+and variable_operating_cost.
+
+
+
Returns:
+
summary (list) – List of dictionaries, each being an SC point summary.
gen_fpath (str, optional) – Filepath to HDF5 file with reV generation output
+results. If None, a simple aggregation without any
+generation, resource, or cost data is performed.
+
+
Note
+
If executing reV from the command line, this
+input can be set to "PIPELINE" to parse the output
+from one of these preceding pipeline steps:
+multi-year, collect, or econ. However, note
+that duplicate executions of any of these commands within
+the pipeline may invalidate this parsing, meaning the
+gen_fpath input will have to be specified manually.
+
+
By default, None.
+
+
args (tuple | list, optional) – List of columns to include in summary output table. None
+defaults to all available args defined in the
+SupplyCurveAggregation
+documentation. By default, None.
+
max_workers (int, optional) – Number of cores to run summary on. None is all available
+CPUs. By default, None.
+
sites_per_worker (int, optional) – Number of sc_points to summarize on each worker.
+By default, 100.
ReV LCOT calculation and SupplyCurve sorting class.
+
reV supply curve computes the transmission costs associated
+with each supply curve point output by reV supply curve
+aggregation. Transmission costs can either be computed
+competitively (where total capacity remaining on the
+transmission grid is tracked and updated after each new
+connection) or non-competitively (where the cheapest connections
+for each supply curve point are allowed regardless of the
+remaining transmission grid capacity). In both cases, the
+permutation of transmission costs between supply curve points
+and transmission grid features should be computed using the
+reVX Least Cost Transmission Paths
+utility.
+
+
Parameters:
+
+
sc_points (str | pandas.DataFrame) – Path to CSV or JSON or DataFrame containing supply curve
+point summary. Can also be a filepath to a reV bespoke
+HDF5 output file where the meta dataset has the same
+format as the supply curve aggregation output.
+
+
Note
+
If executing reV from the command line, this
+input can also be "PIPELINE" to parse the output of
+the previous pipeline step and use it as input to this
+call. However, note that duplicate executions of any
+preceding commands within the pipeline may invalidate this
+parsing, meaning the sc_points input will have to be
+specified manually.
+
+
+
trans_table (str | pandas.DataFrame | list) – Path to CSV or JSON or DataFrame containing supply curve
+transmission mapping. This can also be a list of
+transmission tables with different line voltage (capacity)
+ratings. See the reVX Least Cost Transmission Paths
+utility to generate these input tables.
+
sc_features (str | pandas.DataFrame, optional) – Path to CSV or JSON or DataFrame containing additional
+supply curve features (e.g. transmission multipliers,
+regions, etc.). These features will be merged to the
+sc_points input table on ALL columns that both have in
+common. If None, no extra supply curve features are
+added. By default, None.
+
sc_capacity_col (str, optional) – Name of capacity column in trans_sc_table. The values in
+this column determine the size of transmission lines built.
+The transmission capital costs per MW and the reinforcement
+costs per MW will be returned in terms of these capacity
+values. Note that if this column != “capacity”, then
+“capacity” must also be included in trans_sc_table since
+those values match the “mean_cf” data (which is used to
+calculate LCOT and Total LCOE). This input can be used to,
+e.g., size transmission lines based on solar AC capacity (
+sc_capacity_col="capacity_ac"). By default,
+"capacity".
Transmission cost multiplier that scales the line cost
+but not the tie-in cost in the calculation of LCOT.
+
+
+
+
+
trans_gidint
Unique transmission feature identifier that each supply
+curve point was connected to.
+
+
+
+
+
trans_capacityfloat
Total capacity (not available capacity) of the
+transmission feature that each supply curve point was
+connected to. Default units are MW.
+
+
+
+
+
trans_typestr
Tranmission feature type that each supply curve point
+was connected to (e.g. Transline, Substation).
+
+
+
+
+
trans_cap_cost_per_mwfloat
Capital cost of connecting each supply curve point to
+their respective transmission feature. This value
+includes line cost with transmission_multiplier and the
+tie-in cost. Default units are $/MW.
+
+
+
+
+
dist_kmfloat
Distance in km from supply curve point to transmission
+connection.
+
+
+
+
+
lcotfloat
Levelized cost of connecting to transmission ($/MWh).
+
+
+
+
+
total_lcoefloat
Total LCOE of each supply curve point (mean_lcoe + lcot)
+($/MWh).
+
+
+
+
+
total_lcoe_frictionfloat
Total LCOE of each supply curve point considering the
+LCOE friction scalar from the aggregation step
+(mean_lcoe_friction + lcot) ($/MWh).
Compute LCOT and total LCOE for all sc point to transmission feature
+connections
+
+
Parameters:
+
+
fcr (float) – Fixed charge rate, used to compute LCOT
+
transmission_costs (str | dict, optional) – Transmission feature costs to use with TransmissionFeatures
+handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+center_tie_in_cost, sink_tie_in_cost, by default None
+
avail_cap_frac (int, optional) – Fraction of transmissions features capacity ‘ac_cap’ to make
+available for connection to supply curve points, by default 1
+
line_limited (bool, optional) – Flag to have substation connection is limited by maximum capacity
+of the attached lines, legacy method, by default False
+
connectable (bool, optional) – Flag to only compute tranmission capital cost if transmission
+feature has enough available capacity, by default True
+
max_workers (int | NoneType, optional) – Number of workers to use to compute lcot, if > 1 run in parallel.
+None uses all available cpu’s. by default None
+
consider_friction (bool, optional) – Flag to consider friction layer on LCOE when “mean_lcoe_friction”
+is in the sc points input, by default True
sum_cols (dict) – Mapping of new column label(s) to multiple column labels to sum.
+Example: sum_col={‘total_cap_cost’: [‘cap_cost1’, ‘cap_cost2’]}
+Which would add a new ‘total_cap_cost’ column which would be the
+sum of ‘cap_cost1’ and ‘cap_cost2’ if they are present in table.
+
+
+
Returns:
+
table (pd.DataFrame) – Supply curve table with additional summation columns.
fcr (float) – Fixed charge rate, used to compute LCOT
+
transmission_costs (str | dict, optional) – Transmission feature costs to use with TransmissionFeatures
+handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+center_tie_in_cost, sink_tie_in_cost, by default None
+
avail_cap_frac (int, optional) – Fraction of transmissions features capacity ‘ac_cap’ to make
+available for connection to supply curve points, by default 1
+
line_limited (bool, optional) – Flag to have substation connection is limited by maximum capacity
+of the attached lines, legacy method, by default False
+
connectable (bool, optional) – Flag to only compute tranmission capital cost if transmission
+feature has enough available capacity, by default True
+
max_workers (int | NoneType, optional) – Number of workers to use to compute lcot, if > 1 run in parallel.
+None uses all available cpu’s. by default None
+
consider_friction (bool, optional) – Flag to consider friction layer on LCOE when “mean_lcoe_friction”
+is in the sc points input, by default True
+
sort_on (str, optional) – Column label to sort the Supply Curve table on. This affects the
+build priority - connections with the lowest value in this column
+will be built first, by default None, which will use
+total LCOE without any reinforcement costs as the sort value.
wind_dirs (pandas.DataFrame | str, optional) – path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+the neighboring supply curve point gids and power-rose value at
+each cardinal direction, by default None
+
n_dirs (int, optional) – Number of prominent directions to use, by default 2
+
downwind (bool, optional) – Flag to remove downwind neighbors as well as upwind neighbors,
+by default False
+
offshore_compete (bool, default) – Flag as to whether offshore farms should be included during
+CompetitiveWindFarms, by default False
+
+
+
Returns:
+
supply_curve (pandas.DataFrame) – Updated sc_points table with transmission connections, LCOT
+and LCOE+LCOT based on full supply curve connections
Run simple supply curve sorting that does not take into account
+available capacity
+
+
Parameters:
+
+
fcr (float) – Fixed charge rate, used to compute LCOT
+
transmission_costs (str | dict, optional) – Transmission feature costs to use with TransmissionFeatures
+handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+center_tie_in_cost, sink_tie_in_cost, by default None
+
avail_cap_frac (int, optional) – Fraction of transmissions features capacity ‘ac_cap’ to make
+available for connection to supply curve points, by default 1
+
line_limited (bool, optional) – Flag to have substation connection is limited by maximum capacity
+of the attached lines, legacy method, by default False
+
connectable (bool, optional) – Flag to only compute tranmission capital cost if transmission
+feature has enough available capacity, by default True
+
max_workers (int | NoneType, optional) – Number of workers to use to compute lcot, if > 1 run in parallel.
+None uses all available cpu’s. by default None
+
consider_friction (bool, optional) – Flag to consider friction layer on LCOE when “mean_lcoe_friction”
+is in the sc points input, by default True
+
sort_on (str, optional) – Column label to sort the Supply Curve table on. This affects the
+build priority - connections with the lowest value in this column
+will be built first, by default None, which will use
+total LCOE without any reinforcement costs as the sort value.
+
columns (list | tuple, optional) – Columns to preserve in output connections dataframe.
+By default, DEFAULT_COLUMNS.
+
wind_dirs (pandas.DataFrame | str, optional) – path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+the neighboring supply curve point gids and power-rose value at
+each cardinal direction, by default None
+
n_dirs (int, optional) – Number of prominent directions to use, by default 2
+
downwind (bool, optional) – Flag to remove downwind neighbors as well as upwind neighbors
+
offshore_compete (bool, default) – Flag as to whether offshore farms should be included during
+CompetitiveWindFarms, by default False
+
+
+
Returns:
+
supply_curve (pandas.DataFrame) – Updated sc_points table with transmission connections, LCOT
+and LCOE+LCOT based on simple supply curve connections
Run full supply curve taking into account available capacity of
+tranmission features when making connections.
+
+
Parameters:
+
+
out_fpath (str) – Full path to output CSV file. Does not need to include file
+ending - it will be added automatically if missing.
+
fixed_charge_rate (float) – Fixed charge rate, (in decimal form: 5% = 0.05). This value
+is used to compute LCOT.
+
simple (bool, optional) – Option to run the simple sort (does not keep track of
+capacity available on the existing transmission grid). If
+False, a full transmission sort (where connections are
+limited based on available transmission capacity) is run.
+Note that the full transmission sort requires the
+avail_cap_frac and line_limited inputs.
+By default, True.
+
avail_cap_frac (int, optional) – This input has no effect if simple=True. Fraction of
+transmissions features capacity ac_cap to make available
+for connection to supply curve points. By default, 1.
+
line_limited (bool, optional) – This input has no effect if simple=True. Flag to have
+substation connection limited by maximum capacity
+of the attached lines. This is a legacy method.
+By default, False.
+
transmission_costs (str | dict, optional) – Dictionary of transmission feature costs or path to JSON
+file containing a dictionary of transmission feature costs.
+These costs are used to compute transmission capital cost
+if the input transmission tables do not have a
+"trans_cap_cost" column (this input is ignored
+otherwise). The dictionary must include:
+
+
+
line_tie_in_cost
+
line_cost
+
station_tie_in_cost
+
center_tie_in_cost
+
sink_tie_in_cost
+
+
+
By default, None.
+
+
consider_friction (bool, optional) – Flag to add a new "total_lcoe_friction" column to the
+supply curve output that contains the sum of the computed
+"total_lcoe" value and the input
+"mean_lcoe_friction" values. If "mean_lcoe_friction"
+is not in the sc_points input, this option is ignored.
+By default, True.
+
sort_on (str, optional) – Column label to sort the supply curve table on. This affects
+the build priority when doing a “full” sort - connections
+with the lowest value in this column will be built first.
+For a “simple” sort, only connections with the lowest value
+in this column will be considered. If None, the sort is
+performed on the total LCOE without any reinforcement
+costs added (this is typically what you want - it avoids
+unrealistically long spur-line connections).
+By default None.
+
columns (list | tuple, optional) – Columns to preserve in output supply curve dataframe.
+By default, DEFAULT_COLUMNS.
+
max_workers (int, optional) – Number of workers to use to compute LCOT. If > 1,
+computation is run in parallel. If None, computation
+uses all available CPU’s. By default, None.
+
competition (dict, optional) – Optional dictionary of arguments for competitive wind farm
+exclusions, which removes supply curve points upwind (and
+optionally downwind) of the lowest LCOE supply curves.
+If None, no competition is applied. Otherwise, this
+dictionary can have up to four keys:
+
+
+
wind_dirs (required) : A path to a CSV file or
+reVXProminentWindDirections
+output with the neighboring supply curve point gids
+and power-rose values at each cardinal direction.
+
n_dirs (optional) : An integer representing the
+number of prominent directions to use during wind farm
+competition. By default, 2.
+
downwind (optional) : A flag indicating that
+downwind neighbors should be removed in addition to
+upwind neighbors during wind farm competition.
+By default, False.
+
offshore_compete (optional) : A flag indicating
+that offshore farms should be included during wind
+farm competition. By default, False.
Framework to create map between tech layer (exclusions), res, and gen
+
+
Parameters:
+
+
excl_fpath (str) – Filepath to exclusions h5 file, must contain latitude and longitude
+arrays to allow for mapping to resource points
+
res_fpath (str) – Filepath to .h5 resource file that we’re mapping to.
+
sc_resolution (int | None, optional) – Supply curve resolution, does not affect the exclusion to resource
+(tech) mapping, but defines how many exclusion pixels are mapped
+at a time, by default 2560
+
dist_margin (float, optional) – Extra margin to multiply times the computed distance between
+neighboring resource points, by default 1.05
gids (np.ndarray) – Supply curve gids with tech exclusion points to map to the
+resource meta points.
+
excl_fpath (str) – Filepath to exclusions h5 file, must contain latitude and longitude
+arrays to allow for mapping to resource points
+
sc_row_indices (list) – List of row indices in exclusion array for for every sc_point gid
+
sc_col_indices (list) – List of column indices in exclusion array for for every sc_point
+gid
+
excl_row_slices (list) – List representing the supply curve points rows. Each list entry
+contains the exclusion row slice that are included in the sc
+point.
+
excl_col_slices (list) – List representing the supply curve points columns. Each list entry
+contains the exclusion columns slice that are included in the sc
+point.
+
tree (cKDTree) – cKDTree built from resource lat, lon coordinates
+
dist_tresh (float) – Estimate the distance between resource points. Calculated as half
+of the diagonal between closest resource points, with an extra
+5% margin
+
+
+
Returns:
+
ind (list) – List of arrays of index values from the NN. List entries correspond
+to input gids.
Save tech mapping indices and coordinates to an h5 output file.
+
+
Parameters:
+
+
excl_fpath (str) – Filepath to exclusions h5 file to add techmap to as ‘dset’
+
dset (str) – Dataset name in fpath_out to save mapping results to.
+
indices (np.ndarray) – Index values of the NN resource point. -1 if no res point found.
+2D integer array with shape equal to the exclusions extent shape.
+
distance_threshold (float) – Distance upper bound to save as attr.
+
res_fpath (str, optional) – Filepath to .h5 resource file that we’re mapping to,
+by default None
+
chunks (tuple) – Chunk shape of the 2D output datasets.
max_workers (int, optional) – Number of cores to run mapping on. None uses all available cpus,
+by default None
+
points_per_worker (int, optional) – Number of supply curve points to map to resource gids on each
+worker, by default 10
+
+
+
Returns:
+
indices (np.ndarray) – Index values of the NN resource point. -1 if no res point found.
+2D integer array with shape equal to the exclusions extent shape.
excl_fpath (str) – Filepath to exclusions h5 (tech layer). dset will be
+created in excl_fpath.
+
res_fpath (str) – Filepath to .h5 resource file that we’re mapping to.
+
dset (str, optional) – Dataset name in excl_fpath to save mapping results to, if None
+do not save tech_map to excl_fpath, by default None
+
sc_resolution (int | None, optional) – Supply curve resolution, does not affect the exclusion to resource
+(tech) mapping, but defines how many exclusion pixels are mapped
+at a time, by default 2560
+
dist_margin (float, optional) – Extra margin to multiply times the computed distance between
+neighboring resource points, by default 1.05
+
max_workers (int, optional) – Number of cores to run mapping on. None uses all available cpus,
+by default None
+
points_per_worker (int, optional) – Number of supply curve points to map to resource gids on each
+worker, by default 10
+
+
+
Returns:
+
indices (np.ndarray) – Index values of the NN resource point. -1 if no res point found.
+2D integer array with shape equal to the exclusions extent shape.
Map from a dictionary of name / member pairs to this enum.
+
+
Parameters:
+
other (dict) – Dictionary mapping key values (typically old aliases) to
+enum values. For example, {'sc_gid':'SC_GID'} would
+return a dictionary that maps 'sc_gid' to the SC_GID
+member of this enum.
+
+
Returns:
+
dict – Mapping of input dictionary keys to member values of this
+enum.
A collection of the module names available in reV.
+
Each module name should match the name of the click command
+that will be used to invoke its respective cli. As of 3/1/2022,
+this means that all commands are lowercase with underscores
+replaced by dashes.
analysis_years (int | str | list, optional) – Years to run reV analysis on. Can be an integer or string, or a
+list of integers or strings (or None). This input will get
+converted to a list of values automatically. If None, a
+ConfigWarning will be thrown. By default, None.
+
+
Returns:
+
list – List of analysis years. This list will never be empty, but it
+can contain None as the only value.
random_seed (int | NoneType) – Number to seed the numpy random number generator. Used to generate
+reproducable psuedo-random results if the probability of curtailment
+is not set to 1. Numpy random will be seeded with the system time if
+this is None.
+
+
+
Returns:
+
resource (reV.handlers.sam_resource.SAMResource) – Same as the input argument but with the wind speed dataset set to zero
+where curtailment is in effect.
Add output variables from another instance into this instance.
+
+
Parameters:
+
slotted_dict (SlottedDict) – An different instance of this class (slotted dictionary class) to
+merge into this instance. Variable data in this instance could be
+overwritten by the new data.
Dictionary containing keyword-argument pairs to pass to
+init_logger. This
+initializes logging for the batch command. Note that
+each pipeline job submitted via batch has it’s own
+logging key that will initialize pipeline step
+logging. Therefore, it’s only ever necessary to use
+this input if you want logging information about the
+batching portion of the execution.
+
+
pipeline_configstr
Path to the pipeline configuration defining the commands to
+run for every parametric set.
+
+
setslist of dicts
A list of dictionaries, where each dictionary defines a
+“set” of parametric runs. Each dictionary should have
+the following keys:
+
+
+
argsdict
A dictionary defining the arguments across all input
+configuration files to parameterize. Each argument
+to be parametrized should be a key in this
+dictionary, and the value should be a list of the
+parameter values to run for this argument (single-item lists
+are allowed and can be used to vary a parameter value across
+sets).
Remember that the keys in the args dictionary
+should be part of (at least) one of your other
+configuration files.
+
+
fileslist
A list of paths to the configuration files that
+contain the arguments to be updated for every
+parametric run. Arguments can be spread out over
+multiple files. For example:
Optional string defining a set tag that will prefix
+each job tag for this set. This tag does not need to
+include an underscore, as that is provided during
+concatenation.
Flag to monitor all batch pipelines continuously in the background. Note that the stdout/stderr will not be captured, but you can set a pipeline "log_file" to capture logs.
Much like generation, reV bespoke analysis runs SAM
+simulations by piping in renewable energy resource data (usually
+from the WTK), loading the SAM config, and then executing the
+PySAM.Windpower.Windpower compute module.
+However, unlike reV generation, bespoke analysis is
+performed on the supply-curve grid resolution, and the plant
+layout is optimized for every supply-curve point based on an
+optimization objective specified by the user. See the NREL
+publication on the bespoke methodology for more information.
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
nodes:
+
(int, optional)
+Number of nodes to split the project points across.
+Note that the total number of requested nodes for
+a job may be larger than this value if the command
+splits across other inputs. Default is 1.
+
+
max_workers:
+
(int, optional)
+Number of local workers to run on. If None, uses all available cores (typically 36). By default, None.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
excl_fpathstr | list | tuple
Filepath to exclusions data HDF5 file. The exclusions HDF5 file should contain the layers specified in excl_dict and data_layers. These layers may also be spread out across multiple HDF5 files, in which case this input should be a list or tuple of filepaths pointing to the files containing the layers. Note that each data layer must be uniquely defined (i.e.only appear once and in a single input file).
+
+
res_fpathstr
Unix shell style path to wind resource HDF5 file in NREL WTK format. Can also be a path including a wildcard input like /h5_dir/prefix*suffix to run bespoke on multiple years of resource data. Can also be an explicit list of resource HDF5 file paths, which themselves can contain wildcards. If multiple files are specified in this way, they must have the same coordinates but can have different time indices (i.e. different years). This input must be readable by rex.multi_year_resource.MultiYearWindResource (i.e. the resource data conform to the rex data format). This means the data file(s) must contain a 1D time_index dataset indicating the UTC time of observation, a 1D meta dataset represented by a DataFrame with site-specific columns, and 2D resource datasets that match the dimensions of (time_index, meta). The time index must start at 00:00 of January 1st of the year under consideration, and its shape must be a multiple of 8760.
+
+
tm_dsetstr
Dataset name in the excl_fpath file containing the techmap (exclusions-to-resource mapping data). This data layer links the supply curve GID’s to the generation GID’s that are used to evaluate the performance metrics of each wind plant. By default, the generation GID’s are assumed to match the resource GID’s, but this mapping can be customized via the gid_map input (see the documentation for gid_map for more details).
+
+
Important
+
This dataset uniquely couples the (typically
+high-resolution) exclusion layers to the (typically
+lower-resolution) resource data. Therefore, a separate
+techmap must be used for every unique combination of
+resource and exclusion coordinates.
+
+
+
objective_functionstr
The objective function of the optimization written out as a string. This expression should compute the objective to be minimized during layout optimization. Variables available for computation are:
+
+
+
n_turbines: the number of turbines
+
system_capacity: wind plant capacity
+
aep: annual energy production
+
avg_sl_dist_to_center_m: Average straight-line
+distance to the supply curve point center from all
+turbine locations (in m). Useful for computing plant
+BOS costs.
+
avg_sl_dist_to_medoid_m: Average straight-line
+distance to the medoid of all turbine locations
+(in m). Useful for computing plant BOS costs.
+
nn_conn_dist_m: Total BOS connection distance
+using nearest-neighbor connections. This variable is
+only available for the
+balance_of_system_cost_function equation.
+
fixed_charge_rate: user input fixed_charge_rate if
+included as part of the sam system config.
+
capital_cost: plant capital cost as evaluated
+by capital_cost_function
+
fixed_operating_cost: plant fixed annual operating
+cost as evaluated by fixed_operating_cost_function
+
variable_operating_cost: plant variable annual
+operating cost as evaluated by
+variable_operating_cost_function
+
balance_of_system_cost: plant balance of system
+cost as evaluated by balance_of_system_cost_function
+
self.wind_plant: the SAM wind plant object,
+through which all SAM variables can be accessed
+
+
+
+
capital_cost_functionstr
The plant capital cost function written out as a string. This expression must return the total plant capital cost in $. This expression has access to the same variables as the objective_function argument above.
+
+
fixed_operating_cost_functionstr
The plant annual fixed operating cost function written out as a string. This expression must return the fixed operating cost in $/year. This expression has access to the same variables as the objective_function argument above.
+
+
variable_operating_cost_functionstr
The plant annual variable operating cost function written out as a string. This expression must return the variable operating cost in $/kWh. This expression has access to the same variables as the objective_function argument above. You can set this to “0” to effectively ignore variable operating costs.
+
+
balance_of_system_cost_functionstr
The plant balance-of-system cost function as a string, must return the variable operating cost in $. Has access to the same variables as the objective_function. You can set this to “0” to effectively ignore balance-of-system costs.
Input specifying which sites to process. A single integer representing the supply curve GID of a site may be specified to evaluate reV at a supply curve point. A list or tuple of integers (or slice) representing the supply curve GIDs of multiple sites can be specified to evaluate reV at multiple specific locations. A string pointing to a project points CSV file may also be specified. Typically, the CSV contains the following columns:
+
+
+
gid: Integer specifying the supply curve GID of
+each site.
+
config: Key in the sam_files input dictionary
+(see below) corresponding to the SAM configuration to
+use for each particular site. This value can also be
+None (or left out completely) if you specify only
+a single SAM configuration file as the sam_files
+input.
+
+
+
The CSV file may also contain site-specific inputs by including a column named after a config keyword (e.g. a column called capital_cost may be included to specify a site-specific capital cost value for each location). Columns that do not correspond to a config key may also be included, but they will be ignored. The CSV file input can also have these extra, optional columns:
+
+
+
capital_cost_multiplier
+
fixed_operating_cost_multiplier
+
variable_operating_cost_multiplier
+
balance_of_system_cost_multiplier
+
+
+
These particular inputs are treated as multipliers to be applied to the respective cost curves (capital_cost_function, fixed_operating_cost_function, variable_operating_cost_function, and balance_of_system_cost_function) both during and after the optimization. A DataFrame following the same guidelines as the CSV input (or a dictionary that can be used to initialize such a DataFrame) may be used for this input as well. If you would like to obtain all available reV supply curve points to run, you can use the reV.supply_curve.extent.SupplyCurveExtent class like so:
+
importpandasaspd
+fromreV.supply_curve.extentimportSupplyCurveExtent
+
+excl_fpath="..."
+resolution=...
+withSupplyCurveExtent(excl_fpath,resolution)assc:
+ points=sc.valid_sc_points(tm_dset).tolist()
+ points=pd.DataFrame({"gid":points})
+ points["config"]="default"# or a list of config choices
+
+# Use the points directly or save them to csv for CLI usage
+points.to_csv("project_points.csv",index=False)
+
+
+
+
sam_filesdict | str
A dictionary mapping SAM input configuration ID(s) to SAM configuration(s). Keys are the SAM config ID(s) which correspond to the config column in the project points CSV. Values for each key are either a path to a corresponding SAM config file or a full dictionary of SAM config inputs. For example:
This input can also be a string pointing to a single SAM config file. In this case, the config column of the CSV points input should be set to None or left out completely. See the documentation for the reV SAM class (e.g. reV.SAM.generation.WindPower, reV.SAM.generation.PvWattsv8, reV.SAM.generation.Geothermal, etc.) for info on the allowed and/or required SAM config file inputs.
+
+
min_spacingfloat | int | str, optional
Minimum spacing between turbines (in meters). This input can also be a string like “5x”, which is interpreted as 5 times the turbine rotor diameter. By default, "5x".
+
+
wake_loss_multiplierfloat, optional
A multiplier used to scale the annual energy lost due to wake losses.
+
+
Warning
+
This multiplier will ONLY be applied during the
+optimization process and will NOT come through in output
+values such as the hourly profiles, aep, any of the cost
+functions, or even the output objective.
+
+
By default, 1.
+
+
ga_kwargsdict, optional
Dictionary of keyword arguments to pass to GA initialization. If None, default initialization values are used. See GeneticAlgorithm for a description of the allowed keyword arguments. By default, None.
+
+
output_requestlist | tuple, optional
Outputs requested from the SAM windpower simulation after the bespoke plant layout optimization. Can be any of the parameters in the “Outputs” group of the PySAM module PySAM.Windpower.Windpower.Outputs, PySAM module. This list can also include a select number of SAM config/resource parameters to include in the output: any key in any of the output attribute JSON files may be requested. Time-series profiles requested via this input are output in UTC. This input can also be used to request resource means like "ws_mean", "windspeed_mean", "temperature_mean", and "pressure_mean". By default, ('system_capacity','cf_mean').
+
+
ws_binstuple, optional
A 3-entry tuple with (start,stop,step) for the windspeed binning of the wind joint probability distribution. The stop value is inclusive, so ws_bins=(0,20,5) would result in four bins with bin edges (0, 5, 10, 15, 20). By default, (0.0,20.0,5.0).
+
+
wd_binstuple, optional
A 3-entry tuple with (start,stop,step) for the wind direction binning of the wind joint probability distribution. The stop value is inclusive, so wd_bins=(0,360,90) would result in four bins with bin edges (0, 90, 180, 270, 360). By default, (0.0,360.0,45.0).
+
+
excl_dictdict, optional
Dictionary of exclusion keyword arguments of the format {layer_dset_name:{kwarg:value}}, where layer_dset_name is a dataset in the exclusion h5 file and the kwarg:value pair is a keyword argument to the reV.supply_curve.exclusions.LayerMask class. For example:
Note that all the keys given in this dictionary should be datasets of the excl_fpath file. If None or empty dictionary, no exclusions are applied. By default, None.
+
+
area_filter_kernel{“queen”, “rook”}, optional
Contiguous area filter method to use on final exclusions mask. The filters are defined as:
These filters define how neighboring pixels are “connected”. Once pixels in the final exclusion layer are connected, the area of each resulting cluster is computed and compared against the min_area input. Any cluster with an area less than min_area is excluded from the final mask. This argument has no effect if min_area is None. By default, "queen".
+
+
min_areafloat, optional
Minimum area (in km2) required to keep an isolated cluster of (included) land within the resulting exclusions mask. Any clusters of land with areas less than this value will be marked as exclusions. See the documentation for area_filter_kernel for an explanation of how the area of each land cluster is computed. If None, no area filtering is performed. By default, None.
+
+
resolutionint, optional
Supply Curve resolution. This value defines how many pixels are in a single side of a supply curve cell. For example, a value of 64 would generate a supply curve where the side of each supply curve cell is 64x64 exclusion pixels. By default, 64.
+
+
excl_areafloat, optional
Area of a single exclusion mask pixel (in km2). If None, this value will be inferred from the profile transform attribute in excl_fpath. By default, None.
+
+
data_layersdict, optional
Dictionary of aggregation data layers of the format:
The "output_layer_name" is the column name under which the aggregated data will appear in the meta DataFrame of the output file. The "output_layer_name" does not have to match the dset input value. The latter should match the layer name in the HDF5 from which the data to aggregate should be pulled. The method should be one of {"mode","mean","min","max","sum","category"}, describing how the high-resolution data should be aggregated for each supply curve point. fpath is an optional key that can point to an HDF5 file containing the layer data. If left out, the data is assumed to exist in the file(s) specified by the excl_fpath input. If None, no data layer aggregation is performed. By default, None.
+
+
pre_extract_inclusionsbool, optional
Optional flag to pre-extract/compute the inclusion mask from the excl_dict input. It is typically faster to compute the inclusion mask on the fly with parallel workers. By default, False.
+
+
eos_mult_baseline_cap_mwint | float, optional
Baseline plant capacity (MW) used to calculate economies of scale (EOS) multiplier from the capital_cost_function. EOS multiplier is calculated as the $-per-kW of the wind plant divided by the $-per-kW of a plant with this baseline capacity. By default, 200 (MW), which aligns the baseline with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
+
+
prior_runstr, optional
Optional filepath to a bespoke output HDF5 file belonging to a prior run. If specified, this module will only run the timeseries power generation step and assume that all of the wind plant layouts are fixed from the prior run. The meta data of this file must contain the following columns (automatically satisfied if the HDF5 file was generated by reV bespoke):
+
+
+
capacity : Capacity of the plant, in MW.
+
turbine_x_coords: A string representation of a
+python list containing the X coordinates (in m; origin
+of cell at bottom left) of the turbines within the
+plant (supply curve cell).
+
turbine_y_coords : A string representation of a
+python list containing the Y coordinates (in m; origin
+of cell at bottom left) of the turbines within the
+plant (supply curve cell).
+
+
+
If None, no previous run data is considered. By default, None
+
+
gid_mapstr | dict, optional
Mapping of unique integer generation gids (keys) to single integer resource gids (values). This enables unique generation gids in the project points to map to non-unique resource gids, which can be useful when evaluating multiple resource datasets in reV (e.g., forecasted ECMWF resource data to complement historical WTK meteorology). This input can be a pre-extracted dictionary or a path to a JSON or CSV file. If this input points to a CSV file, the file must have the columns gid (which matches the project points) and gid_map (gids to extract from the resource input). If None, the GID values in the project points are assumed to match the resource GID values. By default, None.
+
+
bias_correctstr | pd.DataFrame, optional
Optional DataFrame or CSV filepath to a wind or solar resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless of the optional gid_map input. Only windspeedorGHI + DNI + DHI are corrected, depending on the technology (wind for the former, PV or CSP for the latter). See the functions in the rex.bias_correction module for available inputs for method. Any additional kwargs required for the requested method can be input as additional columns in the bias_correct table e.g., for linear bias correction functions you can include scalar and adder inputs as columns in the bias_correct table on a site-by-site basis. If None, no corrections are applied. By default, None.
+
+
pre_load_databool, optional
Option to pre-load resource data. This step can be time-consuming up front, but it drastically reduces the number of parallel reads to the res_fpath HDF5 file(s), and can have a significant overall speedup on systems with slow parallel I/O capabilities. Pre-loaded data can use a significant amount of RAM, so be sure to split execution across many nodes (e.g. 100 nodes, 36 workers each for CONUS) or request large amounts of memory for a smaller number of nodes. By default, False.
+
+
log_directorystr
Path to log output directory.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
execution_control:
+ option:local
+ allocation:'[REQUIRED IF ON HPC]'
+ walltime:'[REQUIRED IF ON HPC]'
+ qos:normal
+ memory:null
+ queue:null
+ feature:null
+ conda_env:null
+ module:null
+ sh_script:null
+ num_test_nodes:null
+log_directory:./logs
+log_level:INFO
+project_points:null
+datasets:null
+purge_chunks:false
+clobber:true
+collect_pattern:PIPELINE
+
+
+
log_directory="./logs"
+log_level="INFO"
+purge_chunks=false
+clobber=true
+collect_pattern="PIPELINE"
+
+[execution_control]
+option="local"
+allocation="[REQUIRED IF ON HPC]"
+walltime="[REQUIRED IF ON HPC]"
+qos="normal"
+
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
project_pointsstr | list, optional
This input should represent the project points that correspond to the full collection of points contained in the input HDF5 files to be collected. You may simply point to a ProjectPoints csv file that contains the GID’s that should be collected. You may also input the GID’s as a list, though this may not be suitable for collections with a large number of points. You may also set this to input to None to generate a list of GID’s automatically from the input files. By default, None.
+
+
datasetslist of str, optional
List of dataset names to collect into the output file. If collection is performed into multiple files (i.e. multiple input patterns), this list can contain all relevant datasets across all files (a warning wil be thrown, but it is safe to ignore it). If None, all datasets from the input files are collected. By default, None.
+
+
purge_chunksbool, optional
Option to delete single-node input HDF5 files. Note that the input files will not be removed if any of the datasets they contain have not been collected, regardless of the value of this input. By default, False.
+
+
clobberbool, optional
Flag to purge all collection output HDF5 files prior to running the collection step if they exist on disk. This helps avoid any surprising data byproducts when re-running the collection step in a project directory. By default, True.
+
+
collect_patternstr | list | dict, optional
Unix-style /filepath/pattern*.h5 representing the files to be collected into a single output HDF5 file. If no output file path is specified (i.e. this input is a single pattern or a list of patterns), the output file path will be inferred from the pattern itself (specifically, the wildcard will be removed and the result will be the output file path). If a list of patterns is provided, each pattern will be collected into a separate output file. To specify the name of the output file(s), set this input to a dictionary where the keys are paths to the output file (including the filename itself; relative paths are allowed) and the values are patterns representing the files that should be collected into the output file. If running a collect job as part of a pipeline, this input can be set to "PIPELINE", which will parse the output of the previous step and generate the input file pattern and output file name automatically. By default, "PIPELINE".
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV econ analysis runs SAM econ calculations, typically to
+compute LCOE (using PySAM.Lcoefcr.Lcoefcr), though
+PySAM.Singleowner.Singleowner or
+PySAM.Windbos.Windbos calculations can also be
+performed simply by requesting outputs from those computation
+modules. See the keys of
+Econ.OPTIONS for all
+available econ outputs. Econ computations rely on an input a
+generation (i.e. capacity factor) profile. You can request
+reV to run the analysis for one or more “sites”, which
+correspond to the meta indices in the generation data.
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
nodes:
+
(int, optional)
+Number of nodes to split the project points across.
+Note that the total number of requested nodes for
+a job may be larger than this value if the command
+splits across other inputs. Default is 1.
+
+
max_workers:
+
(int, optional)
+Number of local workers to run on. By default, 1.
+
+
sites_per_worker:
+
(int, optional)
+Number of sites to run in series on a worker. None defaults to the resource file chunk size. By default, None.
+
+
memory_utilization_limit:
+
(float, optional)
+Memory utilization limit (fractional). Must be a value between 0 and 1. This input sets how many site results will be stored in-memory at any given time before flushing to disk. By default, 0.4.
+
+
timeout:
+
(int, optional)
+Number of seconds to wait for parallel run iteration to complete before returning zeros. By default, 1800 seconds.
+
+
pool_size:
+
(int, optional)
+Number of futures to submit to a single process pool for parallel futures. If None, the pool size is set to os.cpu_count()*2. By default, None.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
Input specifying which sites to process. A single integer representing the GID of a site may be specified to evaluate reV at a single location. A list or tuple of integers (or slice) representing the GIDs of multiple sites can be specified to evaluate reV at multiple specific locations. A string pointing to a project points CSV file may also be specified. Typically, the CSV contains the following columns:
+
+
+
gid: Integer specifying the generation GID of each
+site.
+
config: Key in the sam_files input dictionary
+(see below) corresponding to the SAM configuration to
+use for each particular site. This value can also be
+None (or left out completely) if you specify only
+a single SAM configuration file as the sam_files
+input.
+
capital_cost_multiplier: This is an optional
+multiplier input that, if included, will be used to
+regionally scale the capital_cost input in the SAM
+config. If you include this column in your CSV, you
+do not need to specify capital_cost, unless you
+would like that value to vary regionally and
+independently of the multiplier (i.e. the multiplier
+will still be applied on top of the capital_cost
+input).
+
+
+
The CSV file may also contain other site-specific inputs by including a column named after a config keyword (e.g. a column called wind_turbine_rotor_diameter may be included to specify a site-specific turbine diameter for each location). Columns that do not correspond to a config key may also be included, but they will be ignored. A DataFrame following the same guidelines as the CSV input (or a dictionary that can be used to initialize such a DataFrame) may be used for this input as well.
+
+
sam_filesdict | str
A dictionary mapping SAM input configuration ID(s) to SAM configuration(s). Keys are the SAM config ID(s) which correspond to the config column in the project points CSV. Values for each key are either a path to a corresponding SAM config file or a full dictionary of SAM config inputs. For example:
This input can also be a string pointing to a single SAM config file. In this case, the config column of the CSV points input should be set to None or left out completely. See the documentation for the reV SAM class (e.g. reV.SAM.generation.WindPower, reV.SAM.generation.PvWattsv8, reV.SAM.generation.Geothermal, etc.) for documentation on the allowed and/or required SAM config file inputs.
+
+
cf_filestr
Path to reV output generation file containing a capacity factor output.
+
+
Note
+
If executing reV from the command line, this
+path can contain brackets {} that will be filled in
+by the analysis_years input. Alternatively, this input
+can be set to "PIPELINE" to parse the output of the
+previous step (reV generation) and use it as input to
+this call. However, note that duplicate executions of
+reV generation within the pipeline may invalidate this
+parsing, meaning the cf_file input will have to be
+specified manually.
+
+
+
site_datastr | pd.DataFrame, optional
Site-specific input data for SAM calculation. If this input is a string, it should be a path that points to a CSV file. Otherwise, this input should be a DataFrame with pre-extracted site data. Rows in this table should match the input sites via a gid column. The rest of the columns should match configuration input keys that will take site-specific values. Note that some or all site-specific inputs can be specified via the project_points input table instead. If None, no site-specific data is considered. By default, None.
+
+
output_requestlist | tuple, optional
List of output variables requested from SAM. Can be any of the parameters in the “Outputs” group of the PySAM module (e.g. PySAM.Windpower.Windpower.Outputs, PySAM.Pvwattsv8.Pvwattsv8.Outputs, PySAM.Geothermal.Geothermal.Outputs, etc.) being executed. This list can also include a select number of SAM config/resource parameters to include in the output: any key in any of the output attribute JSON files may be requested. Time-series profiles requested via this input are output in UTC. By default, ('lcoe_fcr',).
+
+
appendbool
Option to append econ datasets to source cf_file. By default, False.
+
+
log_directorystr
Path to log output directory.
+
+
analysis_yearsint | list, optional
A single year or list of years to perform analysis for. These years will be used to fill in any brackets {} in the resource_file input. If None, the resource_file input is assumed to be the full path to the single resource file to be processed. By default, None.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV generation analysis runs SAM simulations by piping in
+renewable energy resource data (usually from the NSRDB or WTK),
+loading the SAM config, and then executing the PySAM compute
+module for a given technology. See the documentation for the
+reV SAM class (e.g. reV.SAM.generation.WindPower,
+reV.SAM.generation.PvWattsv8,
+reV.SAM.generation.Geothermal, etc.) for info on the
+allowed and/or required SAM config file inputs. If economic
+parameters are supplied in the SAM config, then you can bundle a
+“follow-on” econ calculation by just adding the desired econ
+output keys to the output_request. You can request reV to
+run the analysis for one or more “sites”, which correspond to
+the meta indices in the resource data (also commonly called the
+gid's).
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
nodes:
+
(int, optional)
+Number of nodes to split the project points across.
+Note that the total number of requested nodes for
+a job may be larger than this value if the command
+splits across other inputs. Default is 1.
+
+
max_workers:
+
(int, optional)
+Number of local workers to run on. If None, or if running from the command line and omitting this argument from your config file completely, this input is set to os.cpu_count(). Otherwise, the default is 1.
+
+
sites_per_worker:
+
(int, optional)
+Number of sites to run in series on a worker. None defaults to the resource file chunk size. By default, None.
+
+
memory_utilization_limit:
+
(float, optional)
+Memory utilization limit (fractional). Must be a value between 0 and 1. This input sets how many site results will be stored in-memory at any given time before flushing to disk. By default, 0.4.
+
+
timeout:
+
(int, optional)
+Number of seconds to wait for parallel run iteration to complete before returning zeros. By default, 1800 seconds.
+
+
pool_size:
+
(int, optional)
+Number of futures to submit to a single process pool for parallel futures. If None, the pool size is set to os.cpu_count()*2. By default, None.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
technologystr
String indicating which SAM technology to analyze. Must be one of the keys of OPTIONS. The string should be lower-cased with spaces and underscores removed.
Input specifying which sites to process. A single integer representing the generation GID of a site may be specified to evaluate reV at a single location. A list or tuple of integers (or slice) representing the generation GIDs of multiple sites can be specified to evaluate reV at multiple specific locations. A string pointing to a project points CSV file may also be specified. Typically, the CSV contains the following columns:
+
+
+
gid: Integer specifying the generation GID of each
+site.
+
config: Key in the sam_files input dictionary
+(see below) corresponding to the SAM configuration to
+use for each particular site. This value can also be
+None (or left out completely) if you specify only
+a single SAM configuration file as the sam_files
+input.
+
capital_cost_multiplier: This is an optional
+multiplier input that, if included, will be used to
+regionally scale the capital_cost input in the SAM
+config. If you include this column in your CSV, you
+do not need to specify capital_cost, unless you
+would like that value to vary regionally and
+independently of the multiplier (i.e. the multiplier
+will still be applied on top of the capital_cost
+input).
+
+
+
The CSV file may also contain other site-specific inputs by including a column named after a config keyword (e.g. a column called wind_turbine_rotor_diameter may be included to specify a site-specific turbine diameter for each location). Columns that do not correspond to a config key may also be included, but they will be ignored. A DataFrame following the same guidelines as the CSV input (or a dictionary that can be used to initialize such a DataFrame) may be used for this input as well.
+
+
Note
+
By default, the generation GID of each site is
+assumed to match the resource GID to be evaluated for that
+site. However, unique generation GID’s can be mapped to
+non-unique resource GID’s via the gid_map input (see the
+documentation for gid_map for more details).
+
+
+
sam_filesdict | str
A dictionary mapping SAM input configuration ID(s) to SAM configuration(s). Keys are the SAM config ID(s) which correspond to the config column in the project points CSV. Values for each key are either a path to a corresponding SAM config file or a full dictionary of SAM config inputs. For example:
This input can also be a string pointing to a single SAM config file. In this case, the config column of the CSV points input should be set to None or left out completely. See the documentation for the reV SAM class (e.g. reV.SAM.generation.WindPower, reV.SAM.generation.PvWattsv8, reV.SAM.generation.Geothermal, etc.) for info on the allowed and/or required SAM config file inputs.
+
+
resource_filestr
Filepath to resource data. This input can be path to a single resource HDF5 file or a path including a wildcard input like /h5_dir/prefix*suffix (i.e. if your datasets for a single year are spread out over multiple files). In all cases, the resource data must be readable by rex.resource.Resource or rex.multi_file_resource.MultiFileResource. (i.e. the resource data conform to the rex data format). This means the data file(s) must contain a 1D time_index dataset indicating the UTC time of observation, a 1D meta dataset represented by a DataFrame with site-specific columns, and 2D resource datasets that match the dimensions of (time_index, meta). The time index must start at 00:00 of January 1st of the year under consideration, and its shape must be a multiple of 8760.
+
+
Note
+
If executing reV from the command line, this
+input string can contain brackets {} that will be
+filled in by the analysis_years input. Alternatively,
+this input can be a list of explicit files to process. In
+this case, the length of the list must match the length of
+the analysis_years input exactly, and the path are
+assumed to align with the analysis_years (i.e. the first
+path corresponds to the first analysis year, the second
+path corresponds to the second analysis year, and so on).
+
+
+
Important
+
If you are using custom resource data (i.e.
+not NSRDB/WTK/Sup3rCC, etc.), ensure the following:
The meta DataFrame is organized such that every
+row is a pixel and at least the columns
+latitude, longitude, timezone, and
+elevation are given for each location.
+
The time index and associated temporal data is in
+UTC.
+
The latitude is between -90 and 90 and longitude is
+between -180 and 180.
+
For solar data, ensure the DNI/DHI are not zero. You
+can calculate one of these these inputs from the
+other using the relationship
+
+\[GHI = DNI * cos(SZA) + DHI\]
+
+
+
+
+
+
low_res_resource_filestr, optional
Optional low resolution resource file that will be dynamically mapped+interpolated to the nominal-resolution resource_file. This needs to be of the same format as resource_file - both files need to be handled by the same rexResource handler (e.g. WindResource). All of the requirements from the resource_file apply to this input as well. If None, no dynamic mapping to higher resolutions is performed. By default, None.
+
+
output_requestlist | tuple, optional
List of output variables requested from SAM. Can be any of the parameters in the “Outputs” group of the PySAM module (e.g. PySAM.Windpower.Windpower.Outputs, PySAM.Pvwattsv8.Pvwattsv8.Outputs, PySAM.Geothermal.Geothermal.Outputs, etc.) being executed. This list can also include a select number of SAM config/resource parameters to include in the output: any key in any of the output attribute JSON files may be requested. If cf_mean is not included in this list, it will automatically be added. Time-series profiles requested via this input are output in UTC.
+
+
Note
+
If you are performing reV solar runs using
+PVWatts and would like reV to include AC capacity
+values in your aggregation/supply curves, then you must
+include the "dc_ac_ratio" time series as an output in
+output_request when running reV generation. The AC
+capacity outputs will automatically be added during the
+aggregation/supply curve step if the "dc_ac_ratio"
+dataset is detected in the generation file.
+
+
By default, ('cf_mean',).
+
+
site_datastr | pd.DataFrame, optional
Site-specific input data for SAM calculation. If this input is a string, it should be a path that points to a CSV file. Otherwise, this input should be a DataFrame with pre-extracted site data. Rows in this table should match the input sites via a gid column. The rest of the columns should match configuration input keys that will take site-specific values. Note that some or all site-specific inputs can be specified via the project_points input table instead. If None, no site-specific data is considered.
+
+
Note
+
This input is often used to provide site-based
+regional capital cost multipliers. reV does not
+ingest multipliers directly; instead, this file is
+expected to have a capital_cost column that gives the
+multiplier-adjusted capital cost value for each location.
+Therefore, you must re-create this input file every
+time you change your base capital cost assumption.
+
+
By default, None.
+
+
curtailmentdict | str, optional
Inputs for curtailment parameters, which can be:
+
+
+
Explicit namespace of curtailment variables (dict)
+
Pointer to curtailment config file with path (str)
+
+
+
The allowed key-value input pairs in the curtailment configuration are documented as properties of the reV.config.curtailment.Curtailment class. If None, no curtailment is modeled. By default, None.
+
+
gid_mapdict | str, optional
Mapping of unique integer generation gids (keys) to single integer resource gids (values). This enables unique generation gids in the project points to map to non-unique resource gids, which can be useful when evaluating multiple resource datasets in reV (e.g., forecasted ECMWF resource data to complement historical WTK meteorology). This input can be a pre-extracted dictionary or a path to a JSON or CSV file. If this input points to a CSV file, the file must have the columns gid (which matches the project points) and gid_map (gids to extract from the resource input). If None, the GID values in the project points are assumed to match the resource GID values. By default, None.
+
+
drop_leapbool, optional
Drop leap day instead of final day of year when handling leap years. By default, False.
+
+
scale_outputsbool, optional
Flag to scale outputs in-place immediately upon Gen returning data. By default, True.
+
+
write_mapped_gidsbool, optional
Option to write mapped gids to output meta instead of resource gids. By default, False.
+
+
bias_correctstr | pd.DataFrame, optional
Optional DataFrame or CSV filepath to a wind or solar resource bias correction table. This has columns:
+
+
+
gid: GID of site (can be index name of dataframe)
+
method: function name from rex.bias_correction module
+
+
+
The gid field should match the true resource gid regardless of the optional gid_map input. Only windspeedorGHI + DNI + DHI are corrected, depending on the technology (wind for the former, PV or CSP for the latter). See the functions in the rex.bias_correction module for available inputs for method. Any additional kwargs required for the requested method can be input as additional columns in the bias_correct table e.g., for linear bias correction functions you can include scalar and adder inputs as columns in the bias_correct table on a site-by-site basis. If None, no corrections are applied. By default, None.
+
+
log_directorystr
Path to log output directory.
+
+
analysis_yearsint | list, optional
A single year or list of years to perform analysis for. These years will be used to fill in any brackets {} in the resource_file input. If None, the resource_file input is assumed to be the full path to the single resource file to be processed. By default, None.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV hybrids computes a “hybrid” wind and solar supply curve,
+where each supply curve point contains some wind and some solar
+capacity. Various ratio limits on wind-to-solar farm properties
+(e.g. wind-to-solar capacity) can be applied during the
+hybridization process. Hybrid generation profiles are also
+computed during this process.
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
solar_fpathstr
Filepath to rep profile output file to extract solar profiles and summaries from.
+
+
wind_fpathstr
Filepath to rep profile output file to extract wind profiles and summaries from.
+
+
allow_solar_onlybool, optional
Option to allow SC points with only solar capacity (no wind). By default, False.
+
+
allow_wind_onlybool, optional
Option to allow SC points with only wind capacity (no solar). By default, False.
+
+
fillnadict, optional
Dictionary containing column_name, fill_value pairs representing any fill values that should be applied after merging the wind and solar meta. Note that column names will likely have to be prefixed with solar or wind. By default None.
+
+
limitsdict, optional
Option to specify mapping (in the form of a dictionary) of {colum_name: max_value} representing the upper limit (maximum value) for the values of a column in the merged meta. For example, limits={'solar_capacity':100} would limit all the values of the solar capacity in the merged meta to a maximum value of 100. This limit is applied BEFORE ratio calculations. The names of the columns should match the column names in the merged meta, so they are likely prefixed with solar or wind. By default, None (no limits applied).
+
+
ratio_boundstuple, optional
Option to set ratio bounds (in two-tuple form) on the columns of the ratio input. For example, ratio_bounds=(0.5,1.5) would adjust the values of both of the ratio columns such that their ratio is always between half and double (e.g., no value would be more than double the other). To specify a single ratio value, use the same value as the upper and lower bound. For example, ratio_bounds=(1,1) would adjust the values of both of the ratio columns such that their ratio is always equal. By default, None (no limit on the ratio).
+
+
ratiostr, optional
Option to specify the columns used to calculate the ratio that is limited by the ratio_bounds input. This input is a string in the form “{numerator_column}/{denominator_column}”. For example, ratio='solar_capacity/wind_capacity' would limit the ratio of the solar to wind capacities as specified by the ratio_bounds input. If ratio_bounds is None, this input does nothing. The names of the columns should be prefixed with one of the prefixes defined as class variables. By default 'solar_capacity/wind_capacity'.
+
+
save_hybrid_metabool, optional
Flag to save hybrid SC table to hybrid rep profile output. By default, True.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV multi-year combines reV generation data from multiple
+years (typically stored in separate files) into a single multi-year
+file. Each dataset in the multi-year file is labeled with the
+corresponding years, and multi-year averages of the yearly datasets
+are also computed.
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
execution_control:
+ option:local
+ allocation:'[REQUIRED IF ON HPC]'
+ walltime:'[REQUIRED IF ON HPC]'
+ qos:normal
+ memory:null
+ queue:null
+ feature:null
+ conda_env:null
+ module:null
+ sh_script:null
+ num_test_nodes:null
+log_directory:./logs
+log_level:INFO
+groups:'[REQUIRED]'
+clobber:true
+
+
+
log_directory="./logs"
+log_level="INFO"
+groups="[REQUIRED]"
+clobber=true
+
+[execution_control]
+option="local"
+allocation="[REQUIRED IF ON HPC]"
+walltime="[REQUIRED IF ON HPC]"
+qos="normal"
+
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
groupsdict
Dictionary of collection groups and their parameters. This should be a dictionary mapping group names (keys) to a set of key word arguments (values) that can be used to initialize MultiYearGroup (excluding the required name and out_dir inputs, which are populated automatically). For example:
The group names will be used as the HDF5 file group name under which the collected data will be stored. You can have exactly one group with the name "none" for a “no group” collection (this is typically what you want and all you need to specify).
+
+
clobberbool, optional
Flag to purge the multi-year output file prior to running the multi-year collection step if the file already exists on disk. This ensures the data is always freshly collected from the single-year files. If False, then datasets in the existing file will not be overwritten with (potentially new/updated) data from the single-year files. By default, True.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV NRWAL analysis runs reV data through the NRWAL
+compute library. Everything in this module operates on the
+spatiotemporal resolution of the reV generation output file
+(usually the wind or solar resource resolution but could also be
+the supply curve resolution after representative profiles is
+run).
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
gen_fpathstr
Full filepath to HDF5 file with reV generation or rep_profiles output. Anything in the output_request input is added to and/or manipulated within this file.
+
+
Note
+
If executing reV from the command line, this
+input can also be "PIPELINE" to parse the output of
+one of the previous step and use it as input to this call.
+However, note that duplicate executions of reV
+commands prior to this one within the pipeline may
+invalidate this parsing, meaning the gen_fpath input
+will have to be specified manually.
+
+
+
site_datastr | pd.DataFrame
Site-specific input data for NRWAL calculation.If this input is a string, it should be a path that points to a CSV file. Otherwise, this input should be a DataFrame with pre-extracted site data. Rows in this table should match the meta_gid_col in the gen_fpath meta data input sites via a gid column. A config column must also be provided that corresponds to the nrwal_configs input. Only sites with a gid in this file’s gid column will be run through NRWAL.
+
+
sam_filesdict | str
A dictionary mapping SAM input configuration ID(s) to SAM configuration(s). Keys are the SAM config ID(s) which correspond to the keys in the nrwal_configs input. Values for each key are either a path to a corresponding SAM config file or a full dictionary of SAM config inputs. For example:
This input can also be a string pointing to a single SAM config file. In this case, the config column of the CSV points input should be set to None or left out completely. See the documentation for the reV SAM class (e.g. reV.SAM.generation.WindPower, reV.SAM.generation.PvWattsv8, reV.SAM.generation.Geothermal, etc.) for documentation on the allowed and/or required SAM config file inputs.
+
+
nrwal_configsdict
A dictionary mapping SAM input configuration ID(s) to NRWAL configuration(s). Keys are the SAM config ID(s) which correspond to the keys in the sam_files input. Values for each key are either a path to a corresponding NRWAL YAML or JSON config file or a full dictionary of NRWAL config inputs. For example:
List of output dataset names to be written to the gen_fpath file. Any key from the NRWAL configs or any of the inputs (site_data or sam_files) is available to be exported as an output dataset. If you want to manipulate a dset like cf_mean from gen_fpath and include it in the output_request, you should set save_raw=True and then use cf_mean_raw in the NRWAL equations as the input. This allows you to define an equation in the NRWAL configs for a manipulated cf_mean output that can be included in the output_request list.
+
+
save_rawbool, optional
Flag to save an initial (“raw”) copy of input datasets from gen_fpath that are also part of the output_request. For example, if you request cf_mean in output_request but also manipulate the cf_mean dataset in the NRWAL equations, the original cf_mean will be archived under the cf_mean_raw dataset in gen_fpath. By default, True.
+
+
meta_gid_colstr, optional
Column label in the source meta data from gen_fpath that contains the unique gid identifier. This will be joined to the site_data gid column. By default, "gid".
+
+
site_meta_colslist | tuple, optional
Column labels from site_data to be added to the meta data table in gen_fpath. If None, only the columns in DEFAULT_META_COLS will be added. Any columns requested via this input will be considered in addition to the DEFAULT_META_COLS. By default, None.
+
+
csv_outputbool, optional
Option to write H5 file meta + all requested outputs to CSV file instead of storing in the HDF5 file directly. This can be useful if the same HDF5 file is used for multiple sets of NRWAL runs. Note that all requested output datasets must be 1-dimensional in order to fir within the CSV output.
+
+
Important
+
This option is not compatible with
+save_raw=True. If you set csv_output=True, then
+the save_raw option is forced to be False.
+Therefore, make sure that you do not have any references
+to “input_dataset_name_raw” in your NRWAL config. If you
+need to manipulate an input dataset, save it to a
+different output name in the NRWAL config or manually add
+an “input_dataset_name_raw” dataset to your generation
+HDF5 file before running NRWAL.
+
+
By default, False.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
Path to the pipeline configuration file. This argument can be
+left out, but one and only one file with “pipeline” in the
+name should exist in the directory and contain the config
+information. Below is a sample template config
A list of dictionaries, where each dictionary represents one
+step in the pipeline. Each dictionary should have one of two
+configurations:
+
+
+
A single key-value pair, where the key is the name of
+the CLI command to run, and the value is the path to
+a config file containing the configuration for that
+command
+
Exactly two key-value pairs, where one of the keys is
+"command", with a value that points to the name of
+a command to execute, while the second key is a _unique_
+user-defined name of the pipeline step to execute, with
+a value that points to the path to a config file
+containing the configuration for the command specified
+by the other key. This configuration allows users to
+specify duplicate commands as part of their pipeline
+execution.
+
+
+
+
loggingdict, optional
Dictionary containing keyword-argument pairs to pass to
+init_logger. This
+initializes logging for the submission portion of the
+pipeline. Note, however, that each step (command) will
+also record the submission step log output to a
+common “project” log file, so it’s only ever necessary
+to use this input if you want a different (lower) level
+of verbosity than the log_level specified in the
+config for the step of the pipeline being executed.
Flag to recursively submit pipelines, starting from the current directory and checking every sub-directory therein. The -c option will be completely ignored if you use this option. Instead, the code will check every sub-directory for exactly one file with the word pipeline in it. If found, that file is assumed to be the pipeline config and is used to kick off the pipeline. In any other case, the directory is skipped.
Flag to monitor pipeline jobs continuously in the background. Note that the stdout/stderr will not be captured, but you can set a pipeline ‘log_file’ to capture logs.
execution_control:
+ option:local
+ allocation:'[REQUIRED IF ON HPC]'
+ walltime:'[REQUIRED IF ON HPC]'
+ qos:normal
+ memory:null
+ queue:null
+ feature:null
+ conda_env:null
+ module:null
+ sh_script:null
+ num_test_nodes:null
+ max_workers:null
+log_directory:./logs
+log_level:INFO
+modules:'[REQUIRED]'
+
+
+
log_directory="./logs"
+log_level="INFO"
+modules="[REQUIRED]"
+
+[execution_control]
+option="local"
+allocation="[REQUIRED IF ON HPC]"
+walltime="[REQUIRED IF ON HPC]"
+qos="normal"
+
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
max_workers:
+
(int, optional)
+Max number of workers to run for QA/QA. If None, uses all CPU cores. By default, None.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
modulesdict
Dictionary of modules to QA/QC. Keys should be the names of the modules to QA/QC. The values are dictionaries that represent the config for the respective QA/QC step. Allowed config keys for QA/QC are the “property” attributes of QaQcModule.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV rep profiles compute representative generation profiles
+for each supply curve point output by reV supply curve
+aggregation. Representative profiles can either be a spatial
+aggregation of generation profiles or actual generation profiles
+that most closely resemble an aggregated profile (selected based
+on an error metric).
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
max_workers:
+
(int, optional)
+Number of parallel rep profile workers. 1 will run serial, while None will use all available. By default, None.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
gen_fpathstr
Filepath to reV generation output HDF5 file to extract cf_dset dataset from.
+
+
Note
+
If executing reV from the command line, this
+path can contain brackets {} that will be filled in by
+the analysis_years input. Alternatively, this input can
+be set to "PIPELINE", which will parse this input from
+one of these preceding pipeline steps: multi-year,
+collect, generation, or
+supply-curve-aggregation. However, note that duplicate
+executions of any of these commands within the pipeline
+may invalidate this parsing, meaning the gen_fpath input
+will have to be specified manually.
+
+
+
rev_summarystr | pd.DataFrame
Aggregated reV supply curve summary file. Must include the following columns:
+
+
+
res_gids : string representation of python list
+containing the resource GID values corresponding to
+each supply curve point.
+
gen_gids : string representation of python list
+containing the reV generation GID values
+corresponding to each supply curve point.
+
weight column (name based on weight input) : string
+representation of python list containing the resource
+GID weights for each supply curve point.
+
+
+
+
Note
+
If executing reV from the command line, this
+input can be set to "PIPELINE", which will parse this
+input from one of these preceding pipeline steps:
+supply-curve-aggregation or supply-curve.
+However, note that duplicate executions of any of these
+commands within the pipeline may invalidate this parsing,
+meaning the rev_summary input will have to be specified
+manually.
+
+
+
reg_colsstr | list
Label(s) for a categorical region column(s) to extract profiles for. For example, "state" will extract a rep profile for each unique entry in the "state" column in rev_summary. To get a profile for each supply curve point, try setting reg_cols to a primary key such as "sc_gid".
+
+
cf_dsetstr, optional
Dataset name to pull generation profiles from. This dataset must be present in the gen_fpath HDF5 file. By default, "cf_profile"
+
+
Note
+
If executing reV from the command line, this
+name can contain brackets {} that will be filled in by
+the analysis_years input (e.g. "cf_profile-{}").
Method identifier for calculation of the representative profile. By default, 'meanoid'
+
+
err_method{‘mbe’, ‘mae’, ‘rmse’}, optional
Method identifier for calculation of error from the representative profile. If this input is None, the representative meanoid / medianoid profile will be returned directly. By default, 'rmse'.
+
+
weightstr, optional
Column in rev_summary used to apply weights when computing mean profiles. The supply curve table data in the weight column should have weight values corresponding to the res_gids in the same row (i.e. string representation of python list containing weight values).
+
+
Important
+
You’ll often want to set this value to
+something other than None (typically "gid_counts"
+if running on standard reV outputs). Otherwise, the
+unique generation profiles within each supply curve point
+are weighted equally. For example, if you have a 64x64
+supply curve point, and one generation profile takes up
+4095 (99.98%) 90m cells while a second generation profile
+takes up only one 90m cell (0.02%), they will contribute
+equally to the meanoid profile unless these weights are
+specified.
+
+
By default, SupplyCurveField.GID_COUNTS.
+
+
n_profilesint, optional
Number of representative profiles to save to the output file. By default, 1.
+
+
aggregate_profilesbool, optional
Flag to calculate the aggregate (weighted meanoid) profile for each supply curve point. This behavior is in lieu of finding the single profile per region closest to the meanoid. If you set this flag to True, the rep_method, err_method, and n_profiles inputs will be forcibly set to the default values. By default, False.
+
+
save_rev_summarybool, optional
Flag to save full reV supply curve table to rep profile output. By default, True.
+
+
scaled_precisionbool, optional
Flag to scale cf_profiles by 1000 and save as uint16. By default, False.
+
+
analysis_yearsint | list, optional
A single year or list of years to perform analysis for. These years will be used to fill in any brackets {} in the cf_dset or gen_fpath inputs. If None, the cf_dset and gen_fpath inputs are assumed to be the full dataset name and the full path to the single resource file to be processed, respectively. Note that only one of cf_dset or gen_fpath are allowed to contain brackets ({}) to be filled in by the analysis years. By default, None.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
Reset pipeline starting after the given pipeline step. The status of this step will remain unaffected, but the status of steps following it will be reset completely.
execution_control:
+ option:local
+ allocation:'[REQUIRED IF ON HPC]'
+ walltime:'[REQUIRED IF ON HPC]'
+ qos:normal
+ memory:null
+ queue:null
+ feature:null
+ conda_env:null
+ module:null
+ sh_script:null
+ num_test_nodes:null
+log_directory:./logs
+log_level:INFO
+cmd:'[REQUIRED]'
+
+
+
log_directory="./logs"
+log_level="INFO"
+cmd="[REQUIRED]"
+
+[execution_control]
+option="local"
+allocation="[REQUIRED IF ON HPC]"
+walltime="[REQUIRED IF ON HPC]"
+qos="normal"
+
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
cmdstr | list
A single command represented as a string or a list of command strings to execute on a node. If the input is a list, each command string in the list will be executed on a separate node. For example, to run a python script, simply specify
+
"cmd":"python my_script.py"
+
+
+
This will run the python file “my_script.py” (in the project directory) on a single node.
+
+
Important
+
It is inefficient to run scripts that only use a
+single processor on HPC nodes for extended periods of time.
+Always make sure your long-running scripts use Python’s
+multiprocessing library wherever possible to make the most
+use of shared HPC resources.
+
+
To run multiple commands in parallel, supply them as a list:
This input will run two commands (a python script with the specified arguments and a wget command to download a file from the web), each on their own node and in parallel as part of this pipeline step. Note that commands are always executed from the project directory.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_cli/reV status.html b/_cli/reV status.html
new file mode 100644
index 000000000..ca8fd0a80
--- /dev/null
+++ b/_cli/reV status.html
@@ -0,0 +1,695 @@
+
+
+
+
+
+
+
+
+ reV status — reV 0.9.4 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Filter status for the given pipeline step(s). Multiple steps can be specified by repeating this option (e.g. -psstep1-psstep2...) By default, the status of all pipeline steps is displayed.
Extra status keys to include in the print output for each job. Multiple status keys can be specified by repeating this option (e.g. -ikey1-ikey2...) By default, no extra keys are displayed.
Execute the supply-curve-aggregation step from a config file.
+
reV supply curve aggregation combines a high-resolution
+(e.g. 90m) exclusion dataset with a (typically) lower resolution
+(e.g. 2km) generation dataset by mapping all data onto the high-
+resolution grid and aggregating it by a large factor (e.g. 64 or
+128). The result is coarsely-gridded data that summarizes
+capacity and generation potential as well as associated
+economics under a particular land access scenario. This module
+can also summarize extra data layers during the aggregation
+process, allowing for complementary land characterization
+analysis.
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
max_workers:
+
(int, optional)
+Number of cores to run summary on. None is all available CPUs. By default, None.
+
+
sites_per_worker:
+
(int, optional)
+Number of sc_points to summarize on each worker. By default, 100.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
excl_fpathstr | list | tuple
Filepath to exclusions data HDF5 file. The exclusions HDF5 file should contain the layers specified in excl_dict and data_layers. These layers may also be spread out across multiple HDF5 files, in which case this input should be a list or tuple of filepaths pointing to the files containing the layers. Note that each data layer must be uniquely defined (i.e.only appear once and in a single input file).
+
+
tm_dsetstr
Dataset name in the excl_fpath file containing the techmap (exclusions-to-resource mapping data). This data layer links the supply curve GID’s to the generation GID’s that are used to evaluate performance metrics such as mean_cf.
+
+
Important
+
This dataset uniquely couples the (typically
+high-resolution) exclusion layers to the (typically
+lower-resolution) resource data. Therefore, a separate
+techmap must be used for every unique combination of
+resource and exclusion coordinates.
+
+
+
Note
+
If executing reV from the command line, you
+can specify a name that is not in the exclusions HDF5
+file, and reV will calculate the techmap for you. Note
+however that computing the techmap and writing it to the
+exclusion HDF5 file is a blocking operation, so you may
+only run a single reV aggregation step at a time this
+way.
+
+
+
econ_fpathstr, optional
Filepath to HDF5 file with reV econ output results containing an lcoe_dset dataset. If None, lcoe_dset should be a dataset in the gen_fpath HDF5 file that aggregation is executed on.
+
+
Note
+
If executing reV from the command line, this
+input can be set to "PIPELINE" to parse the output
+from one of these preceding pipeline steps:
+multi-year, collect, or generation. However,
+note that duplicate executions of any of these commands
+within the pipeline may invalidate this parsing, meaning
+the econ_fpath input will have to be specified manually.
+
+
By default, None.
+
+
excl_dictdict | None
Dictionary of exclusion keyword arguments of the format {layer_dset_name:{kwarg:value}}, where layer_dset_name is a dataset in the exclusion h5 file and the kwarg:value pair is a keyword argument to the reV.supply_curve.exclusions.LayerMask class. For example:
Note that all the keys given in this dictionary should be datasets of the excl_fpath file. If None or empty dictionary, no exclusions are applied. By default, None.
+
+
area_filter_kernel{“queen”, “rook”}, optional
Contiguous area filter method to use on final exclusions mask. The filters are defined as:
These filters define how neighboring pixels are “connected”. Once pixels in the final exclusion layer are connected, the area of each resulting cluster is computed and compared against the min_area input. Any cluster with an area less than min_area is excluded from the final mask. This argument has no effect if min_area is None. By default, "queen".
+
+
min_areafloat, optional
Minimum area (in km2) required to keep an isolated cluster of (included) land within the resulting exclusions mask. Any clusters of land with areas less than this value will be marked as exclusions. See the documentation for area_filter_kernel for an explanation of how the area of each land cluster is computed. If None, no area filtering is performed. By default, None.
+
+
resolutionint, optional
Supply Curve resolution. This value defines how many pixels are in a single side of a supply curve cell. For example, a value of 64 would generate a supply curve where the side of each supply curve cell is 64x64 exclusion pixels. By default, 64.
+
+
excl_areafloat, optional
Area of a single exclusion mask pixel (in km2). If None, this value will be inferred from the profile transform attribute in excl_fpath. By default, None.
+
+
res_fpathstr, optional
Filepath to HDF5 resource file (e.g. WTK or NSRDB). This input is required if techmap dset is to be created or if the gen_fpath input to the summarize or run methods is None. By default, None.
+
+
gidslist, optional
List of supply curve point gids to get summary for. If you would like to obtain all available reV supply curve points to run, you can use the reV.supply_curve.extent.SupplyCurveExtent class like so:
If None, supply curve aggregation is computed for all gids in the supply curve extent. By default, None.
+
+
pre_extract_inclusionsbool, optional
Optional flag to pre-extract/compute the inclusion mask from the excl_dict input. It is typically faster to compute the inclusion mask on the fly with parallel workers. By default, False.
+
+
res_class_dsetstr, optional
Name of dataset in the reV generation HDF5 output file containing resource data. If None, no aggregated resource classification is performed (i.e. no mean_res output), and the res_class_bins is ignored. By default, None.
+
+
res_class_binslist, optional
Optional input to perform separate aggregations for various resource data ranges. If None, only a single aggregation per supply curve point is performed. Otherwise, this input should be a list of floats or ints representing the resource bin boundaries. One aggregation per resource value range is computed, and only pixels within the given resource range are aggregated. By default, None.
+
+
cf_dsetstr, optional
Dataset name from the reV generation HDF5 output file containing a 1D dataset of mean capacity factor values. This dataset will be mapped onto the high-resolution grid and used to compute the mean capacity factor for non-excluded area. By default, "cf_mean-means".
+
+
lcoe_dsetstr, optional
Dataset name from the reV generation HDF5 output file containing a 1D dataset of mean LCOE values. This dataset will be mapped onto the high-resolution grid and used to compute the mean LCOE for non-excluded area, but only if the LCOE is not re-computed during processing (see the recalc_lcoe input for more info). By default, "lcoe_fcr-means".
+
+
h5_dsetslist, optional
Optional list of additional datasets from the reV generation/econ HDF5 output file to aggregate. If None, no extra datasets are aggregated.
+
+
Warning
+
This input is meant for passing through 1D
+datasets. If you specify a 2D or higher-dimensional
+dataset, you may run into memory errors. If you wish to
+aggregate 2D datasets, see the rep-profiles module.
+
+
By default, None.
+
+
data_layersdict, optional
Dictionary of aggregation data layers of the format:
The "output_layer_name" is the column name under which the aggregated data will appear in the output CSV file. The "output_layer_name" does not have to match the dset input value. The latter should match the layer name in the HDF5 from which the data to aggregate should be pulled. The method should be one of {"mode","mean","min","max","sum","category"}, describing how the high-resolution data should be aggregated for each supply curve point. fpath is an optional key that can point to an HDF5 file containing the layer data. If left out, the data is assumed to exist in the file(s) specified by the excl_fpath input. If None, no data layer aggregation is performed. By default, None
+
+
power_densityfloat | str, optional
Power density value (in MW/km2) or filepath to variable power density CSV file containing the following columns:
+
+
+
gid : resource gid (typically wtk or nsrdb gid)
+
power_density : power density value (in
+MW/km2)
+
+
+
If None, a constant power density is inferred from the generation meta data technology. By default, None.
+
+
friction_fpathstr, optional
Filepath to friction surface data (cost based exclusions). Must be paired with the friction_dset input below. The friction data must be the same shape as the exclusions. Friction input creates a new output column "mean_lcoe_friction" which is the nominal LCOE multiplied by the friction data. If None, no friction data is aggregated. By default, None.
+
+
friction_dsetstr, optional
Dataset name in friction_fpath for the friction surface data. Must be paired with the friction_fpath above. If None, no friction data is aggregated. By default, None.
+
+
cap_cost_scalestr, optional
Optional LCOE scaling equation to implement “economies of scale”. Equations must be in python string format and must return a scalar value to multiply the capital cost by. Independent variables in the equation should match the names of the columns in the reV supply curve aggregation output table (see the documentation of SupplyCurveAggregation for details on available outputs). If None, no economies of scale are applied. By default, None.
+
+
recalc_lcoebool, optional
Flag to re-calculate the LCOE from the multi-year mean capacity factor and annual energy production data. This requires several datasets to be aggregated in the h5_dsets input:
+
+
+
system_capacity
+
fixed_charge_rate
+
capital_cost
+
fixed_operating_cost
+
variable_operating_cost
+
+
+
If any of these datasets are missing from the reV generation HDF5 output, or if recalc_lcoe is set to False, the mean LCOE will be computed from the data stored under the lcoe_dset instead. By default, True.
+
+
gen_fpathstr, optional
Filepath to HDF5 file with reV generation output results. If None, a simple aggregation without any generation, resource, or cost data is performed.
+
+
Note
+
If executing reV from the command line, this
+input can be set to "PIPELINE" to parse the output
+from one of these preceding pipeline steps:
+multi-year, collect, or econ. However, note
+that duplicate executions of any of these commands within
+the pipeline may invalidate this parsing, meaning the
+gen_fpath input will have to be specified manually.
+
+
By default, None.
+
+
argstuple | list, optional
List of columns to include in summary output table. None defaults to all available args defined in the SupplyCurveAggregation documentation. By default, None.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
reV supply curve computes the transmission costs associated
+with each supply curve point output by reV supply curve
+aggregation. Transmission costs can either be computed
+competitively (where total capacity remaining on the
+transmission grid is tracked and updated after each new
+connection) or non-competitively (where the cheapest connections
+for each supply curve point are allowed regardless of the
+remaining transmission grid capacity). In both cases, the
+permutation of transmission costs between supply curve points
+and transmission grid features should be computed using the
+reVX Least Cost Transmission Paths
+utility.
+
The general structure for calling this CLI command is given below
+(add --help to print help info to the terminal).
Dictionary containing execution control arguments. Allowed arguments are:
+
+
option:
+
({‘local’, ‘kestrel’, ‘eagle’, ‘awspc’, ‘slurm’, ‘peregrine’})
+Hardware run option. Determines the type of job
+scheduler to use as well as the base AU cost. The
+“slurm” option is a catchall for HPC systems
+that use the SLURM scheduler and should only be
+used if desired hardware is not listed above. If
+“local”, no other HPC-specific keys in are
+required in execution_control (they are ignored
+if provided).
+
+
allocation:
+
(str)
+HPC project (allocation) handle.
+
+
walltime:
+
(int)
+Node walltime request in hours.
+
+
qos:
+
(str, optional)
+Quality-of-service specifier. For Kestrel users:
+This should be one of {‘standby’, ‘normal’,
+‘high’}. Note that ‘high’ priority doubles the AU
+cost. By default, "normal".
+
+
memory:
+
(int, optional)
+Node memory max limit (in GB). By default, None,
+which uses the scheduler’s default memory limit.
+For Kestrel users: If you would like to use the
+full node memory, leave this argument unspecified
+(or set to None) if you are running on standard
+nodes. However, if you would like to use the bigmem
+nodes, you must specify the full upper limit of
+memory you would like for your job, otherwise you
+will be limited to the standard node memory size
+(250GB).
+
+
max_workers:
+
(int, optional)
+Number of workers to use to compute LCOT. If > 1, computation is run in parallel. If None, computation uses all available CPU’s. By default, None.
+
+
queue:
+
(str, optional; PBS ONLY)
+HPC queue to submit job to. Examples include: ‘debug’,
+‘short’, ‘batch’, ‘batch-h’, ‘long’, etc.
+By default, None, which uses “test_queue”.
+
+
feature:
+
(str, optional)
+Additional flags for SLURM job (e.g. “-p debug”).
+By default, None, which does not specify any
+additional flags.
+
+
conda_env:
+
(str, optional)
+Name of conda environment to activate. By default,
+None, which does not load any environments.
+
+
module:
+
(str, optional)
+Module to load. By default, None, which does not
+load any modules.
+
+
sh_script:
+
(str, optional)
+Extra shell script to run before command call.
+By default, None, which does not run any
+scripts.
+
+
num_test_nodes:
+
(str, optional)
+Number of nodes to submit before terminating the
+submission process. This can be used to test a
+new submission configuration without sumbitting
+all nodes (i.e. only running a handful to ensure
+the inputs are specified correctly and the
+outputs look reasonable). By default, None,
+which submits all node jobs.
+
+
+
Only the option key is required for local execution. For execution on the HPC, the allocation and walltime keys are also required. All other options are populated with default values, as seen above.
+
+
log_directorystr
Path to directory where logs should be written. Path can be relative and does not have to exist on disk (it will be created if missing). By default, "./logs".
+
+
log_level{“DEBUG”, “INFO”, “WARNING”, “ERROR”}
String representation of desired logger verbosity. Suitable options are DEBUG (most verbose), INFO (moderately verbose), WARNING (only log warnings and errors), and ERROR (only log errors). By default, "INFO".
+
+
sc_pointsstr | pandas.DataFrame
Path to CSV or JSON or DataFrame containing supply curve point summary. Can also be a filepath to a reV bespoke HDF5 output file where the meta dataset has the same format as the supply curve aggregation output.
+
+
Note
+
If executing reV from the command line, this
+input can also be "PIPELINE" to parse the output of
+the previous pipeline step and use it as input to this
+call. However, note that duplicate executions of any
+preceding commands within the pipeline may invalidate this
+parsing, meaning the sc_points input will have to be
+specified manually.
+
+
+
trans_tablestr | pandas.DataFrame | list
Path to CSV or JSON or DataFrame containing supply curve transmission mapping. This can also be a list of transmission tables with different line voltage (capacity) ratings. See the reVX Least Cost Transmission Paths utility to generate these input tables.
+
+
sc_featuresstr | pandas.DataFrame, optional
Path to CSV or JSON or DataFrame containing additional supply curve features (e.g. transmission multipliers, regions, etc.). These features will be merged to the sc_points input table on ALL columns that both have in common. If None, no extra supply curve features are added. By default, None.
+
+
sc_capacity_colstr, optional
Name of capacity column in trans_sc_table. The values in this column determine the size of transmission lines built. The transmission capital costs per MW and the reinforcement costs per MW will be returned in terms of these capacity values. Note that if this column != “capacity”, then “capacity” must also be included in trans_sc_table since those values match the “mean_cf” data (which is used to calculate LCOT and Total LCOE). This input can be used to, e.g., size transmission lines based on solar AC capacity ( sc_capacity_col="capacity_ac"). By default, "capacity".
+
+
fixed_charge_ratefloat
Fixed charge rate, (in decimal form: 5% = 0.05). This value is used to compute LCOT.
+
+
simplebool, optional
Option to run the simple sort (does not keep track of capacity available on the existing transmission grid). If False, a full transmission sort (where connections are limited based on available transmission capacity) is run. Note that the full transmission sort requires the avail_cap_frac and line_limited inputs. By default, True.
+
+
avail_cap_fracint, optional
This input has no effect if simple=True. Fraction of transmissions features capacity ac_cap to make available for connection to supply curve points. By default, 1.
+
+
line_limitedbool, optional
This input has no effect if simple=True. Flag to have substation connection limited by maximum capacity of the attached lines. This is a legacy method. By default, False.
+
+
transmission_costsstr | dict, optional
Dictionary of transmission feature costs or path to JSON file containing a dictionary of transmission feature costs. These costs are used to compute transmission capital cost if the input transmission tables do not have a "trans_cap_cost" column (this input is ignored otherwise). The dictionary must include:
+
+
+
line_tie_in_cost
+
line_cost
+
station_tie_in_cost
+
center_tie_in_cost
+
sink_tie_in_cost
+
+
+
By default, None.
+
+
consider_frictionbool, optional
Flag to add a new "total_lcoe_friction" column to the supply curve output that contains the sum of the computed "total_lcoe" value and the input "mean_lcoe_friction" values. If "mean_lcoe_friction" is not in the sc_points input, this option is ignored. By default, True.
+
+
sort_onstr, optional
Column label to sort the supply curve table on. This affects the build priority when doing a “full” sort - connections with the lowest value in this column will be built first. For a “simple” sort, only connections with the lowest value in this column will be considered. If None, the sort is performed on the total LCOE without any reinforcement costs added (this is typically what you want - it avoids unrealistically long spur-line connections). By default None.
+
+
columnslist | tuple, optional
Columns to preserve in output supply curve dataframe. By default, DEFAULT_COLUMNS.
+
+
competitiondict, optional
Optional dictionary of arguments for competitive wind farm exclusions, which removes supply curve points upwind (and optionally downwind) of the lowest LCOE supply curves. If None, no competition is applied. Otherwise, this dictionary can have up to four keys:
+
+
+
wind_dirs (required) : A path to a CSV file or
+reVXProminentWindDirections
+output with the neighboring supply curve point gids
+and power-rose values at each cardinal direction.
+
n_dirs (optional) : An integer representing the
+number of prominent directions to use during wind farm
+competition. By default, 2.
+
downwind (optional) : A flag indicating that
+downwind neighbors should be removed in addition to
+upwind neighbors during wind farm competition.
+By default, False.
+
offshore_compete (optional) : A flag indicating
+that offshore farms should be included during wind
+farm competition. By default, False.
+
+
+
By default None.
+
+
+
Note that you may remove any keys with a null value if you do not intend to update them yourself.
Typically, a good place to start is to set up a reV job with a pipeline
+config that points to several reV modules that you want to run in serial.
+
To begin, you can generate some template configuration files using:
+
$ reV template-configs
+
+
+
By default, this generates template JSON configuration files, though you
+can request JSON5, YAML, or TOML configuration files instead. You can run
+$reVtemplate-configs--help on the command line to see all available
+options for the template-configs command. Once the template configuration
+files have been generated, you can fill them out by referring to the
+module CLI documentation (if available) or the help pages of the module CLIs
+for more details on the config options for each CLI command:
After appropriately filling our the configuration files for each module you
+want to run, you can call the reV pipeline CLI using:
+
$ reV pipeline -c config_pipeline.json
+
+
+
This command will run each pipeline step in sequence.
+
+
Note
+
You will need to re-submit the pipeline command above after
+each completed pipeline step.
+
+
To check the status of the pipeline, you can run:
+
$ reV status
+
+
+
This will print a report to the command line detailing the progress of the
+current pipeline. See $reVstatus--help for all status command
+options.
+
If you need to parameterize the pipeline execution, you can use the batch
+command. For details on setting up a batch config file, see the documentation
+or run:
+
$ reV batch --help
+
+
+
on the command line. Once you set up a batch config file, you can execute
+it using:
[docs]classSamResourceRetriever:
+"""Factory utility to get the SAM resource handler."""
+
+ # Mapping for reV technology and SAM module to h5 resource handler type
+ # SolarResource is swapped for NSRDB if the res_file contains "nsrdb"
+ RESOURCE_TYPES={
+ "geothermal":GeothermalResource,
+ "pvwattsv5":SolarResource,
+ "pvwattsv7":SolarResource,
+ "pvwattsv8":SolarResource,
+ "pvsamv1":SolarResource,
+ "tcsmoltensalt":SolarResource,
+ "solarwaterheat":SolarResource,
+ "troughphysicalheat":SolarResource,
+ "lineardirectsteam":SolarResource,
+ "windpower":WindResource,
+ "mhkwave":WaveResource,
+ }
+
+ @staticmethod
+ def_get_base_handler(res_file,module):
+"""Get the base SAM resource handler, raise error if module not found.
+
+ Parameters
+ ----------
+ res_file : str
+ Single resource file (with full path) to retrieve.
+ module : str
+ SAM module name or reV technology to force interpretation
+ of the resource file type.
+ Example: module set to 'pvwatts' or 'tcsmolten' means that this
+ expects a SolarResource file. If 'nsrdb' is in the res_file name,
+ the NSRDB handler will be used.
+
+ Returns
+ -------
+ res_handler : SolarResource | WindResource | NSRDB
+ Solar or Wind resource handler based on input.
+ """
+
+ try:
+ res_handler=SamResourceRetriever.RESOURCE_TYPES[module.lower()]
+
+ exceptKeyErrorase:
+ msg=(
+ "Cannot interpret what kind of resource handler the SAM "
+ 'module or reV technology "{}" requires. Expecting one of '
+ "the following SAM modules or reV technologies: {}".format(
+ module,list(SamResourceRetriever.RESOURCE_TYPES.keys())
+ )
+ )
+ logger.exception(msg)
+ raiseSAMExecutionError(msg)frome
+
+ ifres_handler==SolarResourceand"nsrdb"inres_file.lower():
+ # Use NSRDB handler if definitely an NSRDB file
+ res_handler=NSRDB
+
+ returnres_handler
+
+ @staticmethod
+ def_parse_gid_map_sites(gen_gids,gid_map=None):
+"""Parse resource gids based on the generation gids used by
+ project_points and a gid_map. If gid_map is None, the input gen_gids
+ are just passed through as the res_gids.
+
+ Parameters
+ ----------
+ gen_gids : list
+ List of project_points "sites" that are the generation gids.
+ gid_map : None | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This enables the user to input unique
+ generation gids in the project points that map to non-unique
+ resource gids. This can be None or a pre-extracted dict.
+
+ Returns
+ -------
+ res_gids : list
+ List of resource gids corresponding to the generation gids used by
+ project points. If gid_map is None, then this is the same as the
+ input gen_gids.
+ """
+ ifgid_mapisNone:
+ res_gids=gen_gids
+ else:
+ res_gids=[gid_map[i]foriingen_gids]
+ returnres_gids
+
+ @classmethod
+ def_make_res_kwargs(
+ cls,res_handler,project_points,output_request,gid_map
+ ):
+"""
+ Make Resource.preloadSam args and kwargs
+
+ Parameters
+ ----------
+ res_handler : Resource handler
+ Wind resource handler.
+ project_points : reV.config.ProjectPoints
+ reV Project Points instance used to retrieve resource data at a
+ specific set of sites.
+ output_request : list
+ Outputs to retrieve from SAM.
+ gid_map : None | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This enables the user to input unique
+ generation gids in the project points that map to non-unique
+ resource gids. This can be None or a pre-extracted dict.
+
+ Returns
+ -------
+ kwargs : dict
+ Extra input args to preload sam resource.
+ args : tuple
+ Args for res_handler.preload_SAM class method
+ """
+ sites=cls._parse_gid_map_sites(project_points.sites,gid_map=gid_map)
+ args=(sites,)
+
+ kwargs={}
+ ifres_handlerin(SolarResource,NSRDB):
+ # check for clearsky irradiation analysis for NSRDB
+ kwargs["clearsky"]=project_points.sam_config_obj.clearsky
+ kwargs["bifacial"]=project_points.sam_config_obj.bifacial
+ kwargs["tech"]=project_points.tech
+
+ downscale=project_points.sam_config_obj.downscale
+ # check for downscaling request
+ ifdownscaleisnotNone:
+ # make sure that downscaling is only requested for NSRDB
+ # resource
+ ifres_handler!=NSRDB:
+ msg=(
+ "Downscaling was requested for a non-NSRDB "
+ "resource file. reV does not have this capability "
+ "at the current time. Please contact a developer "
+ "for more information on this feature."
+ )
+ logger.warning(msg)
+ warn(msg,SAMInputWarning)
+ else:
+ # pass through the downscaling request
+ kwargs["downscale"]=downscale
+
+ elifres_handler==WindResource:
+ args+=(project_points.h,)
+ kwargs["icing"]=project_points.sam_config_obj.icing
+ if(
+ project_points.curtailmentisnotNone
+ andproject_points.curtailment.precipitation
+ ):
+ # make precip rate available for curtailment analysis
+ kwargs["precip_rate"]=True
+
+ sam_configs=project_points.sam_inputs.values()
+ needs_wd=any(_sam_config_contains_turbine_layout(sam_config)
+ forsam_configinsam_configs)
+ kwargs["require_wind_dir"]=needs_wd
+
+ elifres_handler==GeothermalResource:
+ args+=(project_points.d,)
+
+ # Check for resource means
+ ifany(req.endswith("_mean")forreqinoutput_request):
+ kwargs["means"]=True
+
+ returnkwargs,args
+
+ @staticmethod
+ def_multi_file_mods(res_handler,kwargs,res_file):
+"""
+ Check if res_file is a multi-file resource dir and update handler
+
+ Parameters
+ ----------
+ res_handler : Resource
+ Resource handler.
+ kwargs : dict
+ Key word arguments for resource init.
+ res_file : str
+ Single resource file (with full path) or multi h5 dir.
+
+ Returns
+ -------
+ res_handler : Resource | MultiFileResource
+ Resource handler, replaced by the multi file resource handler if
+ necessary.
+ kwargs : dict
+ Key word arguments for resource init with h5_dir, prefix,
+ and suffix.
+ res_file : str
+ Single resource file (with full path) or multi h5 dir.
+ """
+ ifres_handler==WindResource:
+ res_handler=MultiFileWTK
+ elifres_handlerin(NSRDB,SolarResource):
+ res_handler=MultiFileNSRDB
+ else:
+ res_handler=MultiFileResource
+
+ returnres_handler,kwargs,res_file
+
+
[docs]@classmethod
+ defget(
+ cls,
+ res_file,
+ project_points,
+ module,
+ output_request=("cf_mean",),
+ gid_map=None,
+ lr_res_file=None,
+ nn_map=None,
+ bias_correct=None,
+ ):
+"""Get the SAM resource iterator object (single year, single file).
+
+ Parameters
+ ----------
+ res_file : str
+ Single resource file (with full path) to retrieve.
+ project_points : reV.config.ProjectPoints
+ reV Project Points instance used to retrieve resource data at a
+ specific set of sites.
+ module : str
+ SAM module name or reV technology to force interpretation
+ of the resource file type.
+ Example: module set to 'pvwatts' or 'tcsmolten' means that this
+ expects a SolarResource file. If 'nsrdb' is in the res_file name,
+ the NSRDB handler will be used.
+ output_request : list | tuple, optional
+ Outputs to retrieve from SAM, by default ('cf_mean', )
+ gid_map : None | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This enables the user to input unique
+ generation gids in the project points that map to non-unique
+ resource gids. This can be None or a pre-extracted dict.
+ lr_res_file : str | None
+ Optional low resolution resource file that will be dynamically
+ mapped+interpolated to the nominal-resolution res_file. This
+ needs to be of the same format as resource_file, e.g. they both
+ need to be handled by the same rex Resource handler such as
+ WindResource
+ nn_map : np.ndarray
+ Optional 1D array of nearest neighbor mappings associated with the
+ res_file to lr_res_file spatial mapping. For details on this
+ argument, see the rex.MultiResolutionResource docstring.
+ bias_correct : None | pd.DataFrame
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+
+
+ Returns
+ -------
+ res : reV.resource.SAMResource
+ Resource iterator object to pass to SAM.
+ """
+
+ res_handler=cls._get_base_handler(res_file,module)
+ kwargs,args=cls._make_res_kwargs(
+ res_handler,project_points,output_request,gid_map
+ )
+
+ multi_h5_res,hsds=check_res_file(res_file)
+ ifmulti_h5_res:
+ res_handler,kwargs,res_file=cls._multi_file_mods(
+ res_handler,kwargs,res_file
+ )
+ else:
+ kwargs["hsds"]=hsds
+
+ kwargs["time_index_step"]=(
+ project_points.sam_config_obj.time_index_step
+ )
+
+ iflr_res_fileisNone:
+ res=res_handler.preload_SAM(res_file,*args,**kwargs)
+ else:
+ kwargs["handler_class"]=res_handler
+ kwargs["nn_map"]=nn_map
+ res=MultiResolutionResource.preload_SAM(
+ res_file,lr_res_file,*args,**kwargs
+ )
+
+ ifbias_correctisnotNone:
+ res.bias_correct(bias_correct)
+
+ returnres
+
+
+
[docs]classSam:
+"""reV wrapper on the PySAM framework."""
+
+ # PySAM object wrapped by this class
+ PYSAM=generic
+
+ # callable attributes to be ignored in the get/set logic
+ IGNORE_ATTRS=["assign","execute","export"]
+
+ def__init__(self):
+ self._pysam=self.PYSAM.new()
+ self._attr_dict=None
+ self._inputs=[]
+ self.sam_sys_inputs={}
+ if"constant"inself.input_list:
+ self["constant"]=0.0
+
+ def__getitem__(self,key):
+"""Get the value of a PySAM attribute (either input or output).
+
+ Parameters
+ ----------
+ key : str
+ Lowest level attribute name.
+
+ Returns
+ -------
+ out : object
+ PySAM data.
+ """
+
+ group=self._get_group(key)
+ try:
+ out=getattr(getattr(self.pysam,group),key)
+ exceptException:
+ out=None
+
+ returnout
+
+ def__setitem__(self,key,value):
+"""Set a PySAM input data attribute.
+
+ Parameters
+ ----------
+ key : str
+ Lowest level attribute name.
+ value : object
+ Data to set to the key.
+ """
+
+ ifkeynotinself.input_list:
+ msg=(
+ 'Could not set input key "{}". Attribute not '
+ 'found in PySAM object: "{}"'.format(key,self.pysam)
+ )
+ logger.exception(msg)
+ raiseSAMInputError(msg)
+ self.sam_sys_inputs[key]=value
+ group=self._get_group(key,outputs=False)
+ try:
+ setattr(getattr(self.pysam,group),key,value)
+ exceptExceptionase:
+ msg=(
+ 'Could not set input key "{}" to '
+ 'group "{}" in "{}".\n'
+ "Data is: {} ({})\n"
+ 'Received the following error: "{}"'.format(
+ key,group,self.pysam,value,type(value),e
+ )
+ )
+ logger.exception(msg)
+ raiseSAMInputError(msg)frome
+
+ @property
+ defpysam(self):
+"""Get the pysam object."""
+ returnself._pysam
+
+
+
+ @property
+ defattr_dict(self):
+"""Get the heirarchical PySAM object attribute dictionary.
+
+ Returns
+ -------
+ _attr_dict : dict
+ Dictionary with:
+ keys: variable groups
+ values: lowest level attribute/variable names
+ """
+ ifself._attr_dictisNone:
+ keys=self._get_pysam_attrs(self.pysam)
+ self._attr_dict={
+ k:self._get_pysam_attrs(getattr(self.pysam,k))forkinkeys
+ }
+
+ returnself._attr_dict
+
+ @property
+ definput_list(self):
+"""Get the list of lowest level input attribute/variable names.
+
+ Returns
+ -------
+ _inputs : list
+ List of lowest level input attributes.
+ """
+ ifnotany(self._inputs):
+ fork,vinself.attr_dict.items():
+ ifk.lower()!="outputs":
+ self._inputs+=v
+
+ returnself._inputs
+
+ def_get_group(self,key,outputs=True):
+"""Get the group that the input key belongs to.
+
+ Parameters
+ ----------
+ key : str
+ Lowest level PySAM attribute/variable name.
+ outputs : bool
+ Flag if this key might be in outputs group. False ignores the
+ outputs group (looks for inputs only).
+
+ Returns
+ -------
+ group : str | None
+ PySAM attribute group that key belongs to. None if not found.
+ """
+ group=None
+
+ temp=self.attr_dict
+ ifnotoutputs:
+ temp={k:vfor(k,v)intemp.items()ifk.lower()!="outputs"}
+
+ fork,vintemp.items():
+ ifkeyinv:
+ group=k
+ break
+
+ returngroup
+
+ def_get_pysam_attrs(self,obj):
+"""Get a list of attributes from obj with ignore logic.
+
+ Parameters
+ ----------
+ obj : PySAM object
+ PySAM object to get attribute list from.
+
+ Returns
+ -------
+ attrs : list
+ List of attrs belonging to obj with dunder attrs and IGNORE_ATTRS
+ not included.
+ """
+ attrs=[
+ a
+ foraindir(obj)
+ ifnota.startswith("__")andanotinself.IGNORE_ATTRS
+ ]
+ returnattrs
+
+
[docs]defexecute(self):
+"""Call the PySAM execute method. Raise SAMExecutionError if error."""
+ try:
+ self.pysam.execute()
+ exceptExceptionase:
+ msg='PySAM raised an error while executing: "{}"'.format(e)
+ logger.exception(msg)
+ raiseSAMExecutionError(msg)frome
+
+ @staticmethod
+ def_filter_inputs(key,value):
+"""Perform any necessary filtering of input keys and values for PySAM.
+
+ Parameters
+ ----------
+ key : str
+ SAM input key.
+ value : str | int | float | list | np.ndarray
+ Input value associated with key.
+
+ Returns
+ -------
+ key : str
+ Filtered SAM input key.
+ value : str | int | float | list | np.ndarray
+ Filtered Input value associated with key.
+ """
+
+ if"."inkey:
+ key=key.replace(".","_")
+
+ if":constant"inkeyand"adjust:"inkey:
+ key=key.replace("adjust:","")
+
+ ifisinstance(value,str)and"["invalueand"]"invalue:
+ try:
+ value=json.loads(value)
+ exceptjson.JSONDecodeError:
+ msg=(
+ 'Found a weird SAM config input for "{}" that looks '
+ "like a stringified-list but could not run through "
+ "json.loads() so skipping: {}".format(key,value)
+ )
+ logger.warning(msg)
+ warn(msg)
+
+ returnkey,value
+
+
[docs]defassign_inputs(self,inputs,raise_warning=False):
+"""Assign a flat dictionary of inputs to the PySAM object.
+
+ Parameters
+ ----------
+ inputs : dict
+ Flat (single-level) dictionary of PySAM inputs.
+ raise_warning : bool
+ Flag to raise a warning for inputs that are not set because they
+ are not found in the PySAM object.
+ """
+
+ fork,vininputs.items():
+ k,v=self._filter_inputs(k,v)
+ ifkinself.input_listandvisnotNone:
+ self[k]=v
+ elifraise_warning:
+ wmsg='Not setting input "{}" to: {}.'.format(k,v)
+ warn(wmsg,SAMInputWarning)
+ logger.warning(wmsg)
+
+
+
[docs]classRevPySam(Sam):
+"""Base class for reV-SAM simulations (generation and econ)."""
+
+ DIR=os.path.dirname(os.path.realpath(__file__))
+ MODULE=None
+
+ def__init__(
+ self,meta,sam_sys_inputs,output_request,site_sys_inputs=None
+ ):
+"""Initialize a SAM object.
+
+ Parameters
+ ----------
+ meta : pd.DataFrame | pd.Series | None
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone. Can be None for econ runs.
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ output_request : list
+ Requested SAM outputs (e.g., 'cf_mean', 'annual_energy',
+ , 'gen_profile', 'energy_yield', 'ppa_price',
+ 'lcoe_fcr').
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ """
+
+ super().__init__()
+ self._site=None
+ self.time_interval=1
+ self.outputs={}
+ self.sam_sys_inputs=sam_sys_inputs
+ self.site_sys_inputs=site_sys_inputs
+ self.output_request=output_request
+ ifself.output_requestisNone:
+ self.output_request=[]
+
+ self._meta=self._parse_meta(meta)
+ self._parse_site_sys_inputs(site_sys_inputs)
+ _add_cost_defaults(self.sam_sys_inputs)
+ _add_sys_capacity(self.sam_sys_inputs)
+
+ @property
+ defmeta(self):
+"""Get meta data property."""
+ returnself._meta
+
+ @property
+ defmodule(self):
+"""Get module property."""
+ returnself.MODULE
+
+ @property
+ defsite(self):
+"""Get the site number for this SAM simulation."""
+ returnself._site
+
+
[docs]@staticmethod
+ defget_sam_res(*args,**kwargs):
+"""Get the SAM resource iterator object (single year, single file)."""
+ returnSamResourceRetriever.get(*args,**kwargs)
+
+
[docs]@staticmethod
+ defdrop_leap(resource):
+"""Drop Feb 29th from resource df with time index.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Resource dataframe with an index containing a pandas
+ time index object with month and day attributes.
+
+ Returns
+ -------
+ resource : pd.DataFrame
+ Resource dataframe with all February 29th timesteps removed.
+ """
+
+ ifhasattr(resource,"index"):
+ ifhasattr(resource.index,"month")andhasattr(
+ resource.index,"day"
+ ):
+ leap_day=(resource.index.month==2)&(
+ resource.index.day==29
+ )
+ resource=resource.drop(resource.index[leap_day])
+
+ returnresource
+
+
[docs]@staticmethod
+ defensure_res_len(arr,time_index):
+"""
+ Ensure time_index has a constant time-step and only covers 365 days
+ (no leap days). If not remove last day
+
+ Parameters
+ ----------
+ arr : ndarray
+ Array to truncate if time_index has a leap day
+ time_index : pandas.DatatimeIndex
+ Time index associated with arr, used to check time-series
+ frequency and number of days
+
+ Returns
+ -------
+ arr : ndarray
+ Truncated array of data such that there are 365 days
+ """
+ msg=(
+ "A valid time_index must be supplied to ensure the proper "
+ "resource length! Instead {} was supplied".format(type(time_index))
+ )
+ assertisinstance(time_index,pd.DatetimeIndex)
+
+ msg="arr length {} does not match time_index length {}!".format(
+ len(arr),len(time_index)
+ )
+ assertlen(arr)==len(time_index)
+
+ iftime_index.is_leap_year.all():
+ mask=time_index.month==2
+ mask&=time_index.day==29
+ ifnotmask.any():
+ mask=time_index.month==2
+ mask&=time_index.day==28
+ s=np.where(mask)[0][-1]
+
+ freq=pd.infer_freq(time_index[:s])
+ msg="frequencies do not match before and after 2/29"
+ assertfreq==pd.infer_freq(time_index[s+1:]),msg
+ else:
+ freq=pd.infer_freq(time_index)
+ else:
+ freq=pd.infer_freq(time_index)
+
+ iffreqisNone:
+ msg=(
+ "Resource time_index does not have a consistent time-step "
+ "(frequency)!"
+ )
+ logger.error(msg)
+ raiseResourceError(msg)
+
+ doy=time_index.dayofyear
+ n_doy=len(doy.unique())
+
+ ifn_doy>365:
+ # Drop last day of year
+ doy_max=doy.max()
+ mask=doy!=doy_max
+ arr=arr[mask]
+
+ returnarr
+
+
[docs]@staticmethod
+ defmake_datetime(series):
+"""Ensure that pd series is a datetime series with dt accessor"""
+ ifnothasattr(series,"dt"):
+ series=pd.to_datetime(pd.Series(series))
+
+ returnseries
+
+
[docs]@classmethod
+ defget_time_interval(cls,time_index):
+"""Get the time interval.
+
+ Parameters
+ ----------
+ time_index : pd.series
+ Datetime series. Must have a dt attribute to access datetime
+ properties (added using make_datetime method).
+
+ Returns
+ -------
+ time_interval : int:
+ This value is the number of indices over which an hour is counted.
+ So if the timestep is 0.5 hours, time_interval is 2.
+ """
+
+ time_index=cls.make_datetime(time_index)
+ x=time_index.dt.hour.diff()
+ time_interval=0
+
+ # iterate through the hourly time diffs and count indices between flips
+ fortinx[1:]:
+ ift==1.0:
+ time_interval+=1
+ break
+ ift==0.0:
+ time_interval+=1
+
+ returnint(time_interval)
+
+ @staticmethod
+ def_parse_meta(meta):
+"""Make sure the meta data corresponds to a single location and convert
+ to pd.Series.
+
+ Parameters
+ ----------
+ meta : pd.DataFrame | pd.Series | None
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone. Can be None for econ runs.
+
+ Parameters
+ ----------
+ meta : pd.Series | None
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone. Can be None for econ runs.
+ """
+ ifisinstance(meta,pd.DataFrame):
+ msg=(
+ "Meta data must only be for a single site but received: "
+ f"{meta}"
+ )
+ assertlen(meta)==1,msg
+ meta=meta.iloc[0]
+
+ ifmetaisnotNone:
+ assertisinstance(meta,pd.Series)
+
+ returnmeta
+
+ def_parse_site_sys_inputs(self,site_sys_inputs):
+"""Parse site-specific parameters and add to parameter dict.
+
+ Parameters
+ ----------
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ """
+
+ ifsite_sys_inputsisnotNone:
+ fork,vinsite_sys_inputs.items():
+ ifisinstance(v,float)andnp.isnan(v):
+ pass
+ else:
+ self.sam_sys_inputs[k]=v
+
+ @staticmethod
+ def_is_arr_like(val):
+"""Returns true if SAM data is array-like. False if scalar."""
+ ifisinstance(val,(int,float,str)):
+ returnFalse
+ try:
+ len(val)
+ exceptTypeError:
+ returnFalse
+ else:
+ returnTrue
+
+ @classmethod
+ def_is_hourly(cls,val):
+"""Returns true if SAM data is hourly or sub-hourly. False otherise."""
+ ifnotcls._is_arr_like(val):
+ returnFalse
+ L=len(val)
+ returnL>=8760
+
+
[docs]defcollect_outputs(self,output_lookup):
+"""Collect SAM output_request, convert timeseries outputs to UTC, and
+ save outputs to self.outputs property.
+
+ Parameters
+ ----------
+ output_lookup : dict
+ Lookup dictionary mapping output keys to special output methods.
+ """
+ bad_requests=[]
+ forreqinself.output_request:
+ ifreqinoutput_lookup:
+ self.outputs[req]=output_lookup[req]()
+ elifreqinself.sam_sys_inputs:
+ self.outputs[req]=self.sam_sys_inputs[req]
+ else:
+ try:
+ self.outputs[req]=getattr(self.pysam.Outputs,req)
+ exceptAttributeError:
+ bad_requests.append(req)
+
+ ifany(bad_requests):
+ msg=('Could not retrieve outputs "{}" from PySAM object "{}".'
+ .format(bad_requests,self.pysam))
+ logger.error(msg)
+ raiseSAMExecutionError(msg)
+
+ self.outputs_to_utc_arr()
+
+
[docs]defassign_inputs(self):
+"""Assign the self.sam_sys_inputs attribute to the PySAM object."""
+ super().assign_inputs(copy.deepcopy(self.sam_sys_inputs))
+
+
[docs]defexecute(self):
+"""Call the PySAM execute method. Raise SAMExecutionError if error.
+ Include the site index if available.
+ """
+ try:
+ self.pysam.execute()
+ exceptExceptionase:
+ msg='PySAM raised an error while executing: "{}"'.format(
+ self.module
+ )
+ ifself.siteisnotNone:
+ msg+=" for site {}".format(self.site)
+ logger.exception(msg)
+ raiseSAMExecutionError(msg)frome
+
+
+def_add_cost_defaults(sam_inputs):
+"""Add default values for required cost outputs if they are missing. """
+ sam_inputs.setdefault("fixed_charge_rate",None)
+
+ reg_mult=sam_inputs.setdefault("capital_cost_multiplier",1)
+ capital_cost=sam_inputs.setdefault("capital_cost",None)
+ fixed_operating_cost=sam_inputs.setdefault("fixed_operating_cost",None)
+ variable_operating_cost=sam_inputs.setdefault(
+ "variable_operating_cost",None)
+
+ sam_inputs["base_capital_cost"]=capital_cost
+ sam_inputs["base_fixed_operating_cost"]=fixed_operating_cost
+ sam_inputs["base_variable_operating_cost"]=variable_operating_cost
+ ifcapital_costisnotNone:
+ sam_inputs["capital_cost"]=capital_cost*reg_mult
+ else:
+ sam_inputs["capital_cost"]=None
+
+
+def_add_sys_capacity(sam_inputs):
+"""Add system capacity SAM input if it is missing. """
+ cap=sam_inputs.get("system_capacity")
+ ifcapisNone:
+ cap=sam_inputs.get("turbine_capacity")
+
+ ifcapisNone:
+ cap=sam_inputs.get("wind_turbine_powercurve_powerout")
+ ifcapisnotNone:
+ cap=max(cap)
+
+ ifcapisNone:
+ cap=sam_inputs.get("nameplate")
+
+ sam_inputs["system_capacity"]=cap
+
+
+def_sam_config_contains_turbine_layout(sam_config):
+"""Detect wether SAM config contains multiple turbines in layout. """
+ returnlen(sam_config.get("wind_farm_xCoordinates",()))>1
+
[docs]classEconomic(RevPySam):
+"""Base class for SAM economic models."""
+
+ MODULE=None
+
+ def__init__(self,sam_sys_inputs,site_sys_inputs=None,
+ output_request='lcoe_fcr'):
+"""Initialize a SAM economic model object.
+
+ Parameters
+ ----------
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ output_request : list | tuple | str
+ Requested SAM output(s) (e.g., 'ppa_price', 'lcoe_fcr').
+ """
+
+ self._site=None
+
+ ifisinstance(output_request,(list,tuple)):
+ self.output_request=output_request
+ else:
+ self.output_request=(output_request,)
+
+ super().__init__(meta=None,sam_sys_inputs=sam_sys_inputs,
+ site_sys_inputs=site_sys_inputs,
+ output_request=output_request)
+
+ @staticmethod
+ def_parse_sys_cap(site,inputs,site_df):
+"""Find the system capacity variable in either inputs or df.
+
+ Parameters
+ ----------
+ site : int
+ Site gid.
+ inputs : dict
+ Generic system inputs (not site-specific).
+ site_df : pd.DataFrame
+ Site-specific inputs table with index = site gid's
+
+ Returns
+ -------
+ sys_cap : int | float
+ System nameplate capacity in native units (SAM is kW).
+ """
+
+ if('system_capacity'notininputs
+ and'turbine_capacity'notininputs
+ and'system_capacity'notinsite_df
+ and'turbine_capacity'notinsite_df):
+ raiseSAMExecutionError('Input parameter "system_capacity" '
+ 'or "turbine_capacity" '
+ 'must be included in the SAM config '
+ 'inputs or site-specific inputs in '
+ 'order to calculate annual energy '
+ 'yield for LCOE. Received the following '
+ 'inputs, site_df:\n{}\n{}'
+ .format(inputs,site_df.head()))
+
+ if'system_capacity'ininputs:
+ sys_cap=inputs['system_capacity']
+ elif'turbine_capacity'ininputs:
+ sys_cap=inputs['turbine_capacity']
+ elif'system_capacity'insite_df:
+ sys_cap=site_df.loc[site,'system_capacity']
+ elif'turbine_capacity'insite_df:
+ sys_cap=site_df.loc[site,'turbine_capacity']
+
+ returnsys_cap
+
+ @classmethod
+ def_get_annual_energy(cls,site,site_df,site_gids,cf_arr,inputs,
+ calc_aey):
+"""Get the single-site cf and annual energy and add to site_df.
+
+ Parameters
+ ----------
+ site : int
+ Site gid.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ site_gids : list
+ List of all site gid values from the cf_file.
+ cf_arr : np.ndarray
+ Array of cf_mean values for all sites in the cf_file for the
+ given year.
+ inputs : dict
+ Dictionary of SAM input parameters.
+ calc_aey : bool
+ Flag to add annual_energy to df.
+
+ Returns
+ -------
+ site_df : pd.DataFrame
+ Same as input but with added labels "capacity_factor" and
+ "annual_energy" (latter is dependent on calc_aey flag).
+ """
+
+ # get the index location of the site in question
+ isite=site_gids.index(site)
+
+ # calculate the capacity factor
+ cf=cf_arr[isite]
+ ifcf>1:
+ warn('Capacity factor > 1. Dividing by 100.')
+ cf/=100
+ site_df.loc[site,'capacity_factor']=cf
+
+ # calculate the annual energy yield if not input;
+ ifcalc_aey:
+ # get the system capacity
+ sys_cap=cls._parse_sys_cap(site,inputs,site_df)
+
+ # Calc annual energy, mult by 8760 to convert kW to kWh
+ aey=sys_cap*cf*8760
+
+ # add aey to site-specific inputs
+ site_df.loc[site,'annual_energy']=aey
+
+ returnsite_df
+
+ @staticmethod
+ def_get_cf_profiles(sites,cf_file,year):
+"""Get the multi-site capacity factor time series profiles.
+
+ Parameters
+ ----------
+ sites : list
+ List of all site GID's to get gen profiles for.
+ cf_file : str
+ reV generation capacity factor output file with path.
+ year : int | str | None
+ reV generation year to calculate econ for. Looks for cf_mean_{year}
+ or cf_profile_{year}. None will default to a non-year-specific cf
+ dataset (cf_mean, cf_profile).
+
+ Returns
+ -------
+ profiles : np.ndarray
+ 2D array (time, n_sites) of all capacity factor profiles for all
+ the requested sites.
+ """
+
+ # Retrieve the generation profile for single owner input
+ withOutputs(cf_file)ascfh:
+
+ # get the index location of the site in question
+ site_gids=list(cfh.get_meta_arr(ResourceMetaField.GID))
+ isites=[site_gids.index(s)forsinsites]
+
+ # look for the cf_profile dataset
+ if'cf_profile'incfh.datasets:
+ dset='cf_profile'
+ elif'cf_profile-{}'.format(year)incfh.datasets:
+ dset='cf_profile-{}'.format(year)
+ elif'cf_profile_{}'.format(year)incfh.datasets:
+ dset='cf_profile_{}'.format(year)
+ else:
+ msg=('Could not find cf_profile values for '
+ 'input to SingleOwner. Available datasets: {}'
+ .format(cfh.datasets))
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ profiles=cfh[dset,:,isites]
+
+ returnprofiles
+
+ @classmethod
+ def_make_gen_profile(cls,isite,site,profiles,site_df,inputs):
+"""Get the single-site generation time series and add to inputs dict.
+
+ Parameters
+ ----------
+ isite : int
+ Site index in the profiles array.
+ site : int
+ Site resource GID.
+ profiles : np.ndarray
+ 2D array (time, n_sites) of all capacity factor profiles for all
+ the requested sites.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ inputs : dict
+ Dictionary of SAM input parameters.
+
+ Returns
+ -------
+ inputs : dict
+ Dictionary of SAM input parameters with the generation profile
+ added.
+ """
+
+ sys_cap=cls._parse_sys_cap(site,inputs,site_df)
+ inputs['gen']=profiles[:,isite]*sys_cap
+
+ returninputs
+
+
[docs]defppa_price(self):
+"""Get PPA price ($/MWh).
+
+ Native units are cents/kWh, mult by 10 for $/MWh.
+ """
+ returnself['ppa']*10
+
+
[docs]defnpv(self):
+"""Get net present value (NPV) ($).
+
+ Native units are dollars.
+ """
+ returnself['project_return_aftertax_npv']
+
+
[docs]deflcoe_fcr(self):
+"""Get LCOE ($/MWh).
+
+ Native units are $/kWh, mult by 1000 for $/MWh.
+ """
+ if'lcoe_fcr'inself.outputs:
+ lcoe=self.outputs['lcoe_fcr']
+ else:
+ lcoe=self['lcoe_fcr']*1000
+ returnlcoe
+
+
[docs]deflcoe_nom(self):
+"""Get nominal LCOE ($/MWh) (from PPA/SingleOwner model).
+
+ Native units are cents/kWh, mult by 10 for $/MWh.
+ """
+ returnself['lcoe_nom']*10
+
+
[docs]deflcoe_real(self):
+"""Get real LCOE ($/MWh) (from PPA/SingleOwner model).
+
+ Native units are cents/kWh, mult by 10 for $/MWh.
+ """
+ returnself['lcoe_real']*10
+
+
[docs]defflip_actual_irr(self):
+"""Get actual IRR (from PPA/SingleOwner model).
+
+ Native units are %.
+ """
+ returnself['flip_actual_irr']
+
+
[docs]defgross_revenue(self):
+"""Get cash flow total revenue (from PPA/SingleOwner model).
+
+ Native units are $.
+ """
+ cf_tr=np.array(self['cf_total_revenue'],dtype=np.float32)
+ cf_tr=np.sum(cf_tr,axis=0)
+ returncf_tr
+
+
[docs]defcollect_outputs(self):
+"""Collect SAM output_request, convert timeseries outputs to UTC, and
+ save outputs to self.outputs property.
+ """
+
+ output_lookup={'ppa_price':self.ppa_price,
+ 'project_return_aftertax_npv':self.npv,
+ 'lcoe_fcr':self.lcoe_fcr,
+ 'lcoe_nom':self.lcoe_nom,
+ 'lcoe_real':self.lcoe_real,
+ 'flip_actual_irr':self.flip_actual_irr,
+ 'gross_revenue':self.gross_revenue,
+ }
+
+ super().collect_outputs(output_lookup)
+
+
[docs]@classmethod
+ defreV_run(cls,site,site_df,inputs,output_request):
+"""Run the SAM econ model for a single site.
+
+ Parameters
+ ----------
+ site : int
+ Site gid.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ inputs : dict
+ Dictionary of SAM system input parameters.
+ output_request : list | tuple | str
+ Requested SAM output(s) (e.g., 'ppa_price', 'lcoe_fcr').
+
+ Returns
+ -------
+ sim.outputs : dict
+ Dictionary keyed by SAM variable names with SAM numerical results.
+ """
+
+ # Create SAM econ instance and calculate requested output.
+ sim=cls(sam_sys_inputs=inputs,
+ site_sys_inputs=dict(site_df.loc[site,:]),
+ output_request=output_request)
+ sim._site=site
+
+ sim.assign_inputs()
+ sim.execute()
+ sim.collect_outputs()
+
+ returnsim.outputs
+
+
+
[docs]classLCOE(Economic):
+"""SAM LCOE model.
+ """
+
+ MODULE='lcoefcr'
+ PYSAM=PySamLCOE
+
+ def__init__(self,sam_sys_inputs,site_sys_inputs=None,
+ output_request=('lcoe_fcr',)):
+"""Initialize a SAM LCOE economic model object."""
+ super().__init__(sam_sys_inputs,site_sys_inputs=site_sys_inputs,
+ output_request=output_request)
+
+ @staticmethod
+ def_parse_lcoe_inputs(site_df,cf_file,year):
+"""Parse for non-site-specific LCOE inputs.
+
+ Parameters
+ ----------
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ cf_file : str
+ reV generation capacity factor output file with path.
+ year : int | str | None
+ reV generation year to calculate econ for. Looks for cf_mean_{year}
+ or cf_profile_{year}. None will default to a non-year-specific cf
+ dataset (cf_mean, cf_profile).
+
+ Returns
+ -------
+ site_gids : list
+ List of all site gid values from the cf_file.
+ calc_aey : bool
+ Flag to require calculation of the annual energy yield before
+ running LCOE.
+ cf_arr : np.ndarray
+ Array of cf_mean values for all sites in the cf_file for the
+ given year.
+ """
+
+ # get the cf_file meta data gid's to use as indexing tools
+ withOutputs(cf_file)ascfh:
+ site_gids=list(cfh.meta[ResourceMetaField.GID])
+
+ calc_aey=False
+ if'annual_energy'notinsite_df:
+ # annual energy yield has not been input, flag to calculate
+ site_df.loc[:,'annual_energy']=np.nan
+ calc_aey=True
+
+ # make sure capacity factor is present in site-specific data
+ if'capacity_factor'notinsite_df:
+ site_df.loc[:,'capacity_factor']=np.nan
+
+ # pull all cf mean values for LCOE calc
+ withOutputs(cf_file)ascfh:
+ if'cf_mean'incfh.datasets:
+ cf_arr=cfh['cf_mean']
+ elif'cf_mean-{}'.format(year)incfh.datasets:
+ cf_arr=cfh['cf_mean-{}'.format(year)]
+ elif'cf_mean_{}'.format(year)incfh.datasets:
+ cf_arr=cfh['cf_mean_{}'.format(year)]
+ elif'cf'incfh.datasets:
+ cf_arr=cfh['cf']
+ else:
+ raiseKeyError('Could not find cf_mean values for LCOE. '
+ 'Available datasets: {}'.format(cfh.datasets))
+ returnsite_gids,calc_aey,cf_arr
+
+
[docs]@classmethod
+ defreV_run(cls,points_control,site_df,cf_file,year,
+ output_request=('lcoe_fcr',)):
+"""Execute SAM LCOE simulations based on a reV points control instance.
+
+ Parameters
+ ----------
+ points_control : config.PointsControl
+ PointsControl instance containing project points site and SAM
+ config info.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ cf_file : str
+ reV generation capacity factor output file with path.
+ year : int | str | None
+ reV generation year to calculate econ for. Looks for cf_mean_{year}
+ or cf_profile_{year}. None will default to a non-year-specific cf
+ dataset (cf_mean, cf_profile).
+ output_request : list | tuple | str
+ Output(s) to retrieve from SAM.
+
+ Returns
+ -------
+ out : dict
+ Nested dictionaries where the top level key is the site index,
+ the second level key is the variable name, second level value is
+ the output variable value.
+ """
+
+ out={}
+
+ site_gids,calc_aey,cf_arr=cls._parse_lcoe_inputs(site_df,cf_file,
+ year)
+
+ forsiteinpoints_control.sites:
+ # get SAM inputs from project_points based on the current site
+ _,inputs=points_control.project_points[site]
+
+ site_df=cls._get_annual_energy(site,site_df,site_gids,cf_arr,
+ inputs,calc_aey)
+
+ out[site]=super().reV_run(site,site_df,inputs,output_request)
+
+ returnout
+
+
+
[docs]classSingleOwner(Economic):
+"""SAM single owner economic model.
+ """
+
+ MODULE='singleowner'
+ PYSAM=PySamSingleOwner
+
+ def__init__(self,sam_sys_inputs,site_sys_inputs=None,
+ output_request=('ppa_price',)):
+"""Initialize a SAM single owner economic model object.
+ """
+ super().__init__(sam_sys_inputs,site_sys_inputs=site_sys_inputs,
+ output_request=output_request)
+
+ # run balance of system cost model if required
+ self.sam_sys_inputs,self.windbos_outputs= \
+ self._windbos(self.sam_sys_inputs)
+
+ @staticmethod
+ def_windbos(inputs):
+"""Run SAM Wind Balance of System cost model if requested.
+
+ Parameters
+ ----------
+ inputs : dict
+ Dictionary of SAM key-value pair inputs.
+ "total_installed_cost": "windbos" will trigger the windbos method.
+
+ Returns
+ -------
+ inputs : dict
+ Dictionary of SAM key-value pair inputs with the total installed
+ cost replaced with WindBOS values if requested.
+ output : dict
+ Dictionary of windbos cost breakdowns.
+ """
+
+ outputs={}
+ if(inputsisnotNone
+ and'total_installed_cost'ininputs
+ andisinstance(inputs['total_installed_cost'],str)
+ andinputs['total_installed_cost'].lower()=='windbos'):
+ wb=WindBos(inputs)
+ inputs['total_installed_cost']=wb.total_installed_cost
+ outputs=wb.output
+ returninputs,outputs
+
+
[docs]defcollect_outputs(self):
+"""Collect SAM output_request, convert timeseries outputs to UTC, and
+ save outputs to self.outputs property. This includes windbos outputs.
+ """
+
+ windbos_out_vars=[vforvinself.output_request
+ ifvinself.windbos_outputs]
+ self.output_request=[vforvinself.output_request
+ ifvnotinwindbos_out_vars]
+
+ super().collect_outputs()
+
+ windbos_results={}
+ forrequestinwindbos_out_vars:
+ windbos_results[request]=self.windbos_outputs[request]
+
+ self.outputs.update(windbos_results)
+
+
[docs]@classmethod
+ defreV_run(cls,points_control,site_df,cf_file,year,
+ output_request=('ppa_price',)):
+"""Execute SAM SingleOwner simulations based on reV points control.
+
+ Parameters
+ ----------
+ points_control : config.PointsControl
+ PointsControl instance containing project points site and SAM
+ config info.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ cf_file : str
+ reV generation capacity factor output file with path.
+ year : int | str | None
+ reV generation year to calculate econ for. Looks for cf_mean_{year}
+ or cf_profile_{year}. None will default to a non-year-specific cf
+ dataset (cf_mean, cf_profile).
+ output_request : list | tuple | str
+ Output(s) to retrieve from SAM.
+
+ Returns
+ -------
+ out : dict
+ Nested dictionaries where the top level key is the site index,
+ the second level key is the variable name, second level value is
+ the output variable value.
+ """
+
+ out={}
+
+ profiles=cls._get_cf_profiles(points_control.sites,cf_file,year)
+
+ fori,siteinenumerate(points_control.sites):
+ # get SAM inputs from project_points based on the current site
+ _,inputs=points_control.project_points[site]
+
+ # ensure that site-specific data is not persisted to other sites
+ site_inputs=deepcopy(inputs)
+
+ # set the generation profile as an input.
+ site_inputs=cls._make_gen_profile(i,site,profiles,site_df,
+ site_inputs)
+
+ out[site]=super().reV_run(site,site_df,site_inputs,
+ output_request)
+
+ returnout
[docs]classAbstractSamGeneration(RevPySam,ScheduledLossesMixin,ABC):
+"""Base class for SAM generation simulations."""
+
+ def__init__(
+ self,
+ resource,
+ meta,
+ sam_sys_inputs,
+ site_sys_inputs=None,
+ output_request=None,
+ drop_leap=False,
+ ):
+"""Initialize a SAM generation object.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ meta : pd.DataFrame | pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ output_request : list
+ Requested SAM outputs (e.g., 'cf_mean', 'annual_energy',
+ 'cf_profile', 'gen_profile', 'energy_yield', 'ppa_price',
+ 'lcoe_fcr').
+ drop_leap : bool
+ Drops February 29th from the resource data. If False, December
+ 31st is dropped from leap years.
+ """
+
+ # drop the leap day
+ ifdrop_leap:
+ resource=self.drop_leap(resource)
+
+ # make sure timezone and elevation are in the meta data
+ meta=self.tz_elev_check(sam_sys_inputs,site_sys_inputs,meta)
+
+ # don't pass resource to base class,
+ # set in concrete generation classes instead
+ super().__init__(
+ meta,
+ sam_sys_inputs,
+ output_request,
+ site_sys_inputs=site_sys_inputs,
+ )
+
+ # Set the site number using resource
+ ifhasattr(resource,"name"):
+ self._site=resource.name
+ else:
+ self._site=None
+
+ # let children pass in None resource
+ ifresourceisnotNone:
+ self.check_resource_data(resource)
+ self.set_resource_data(resource,meta)
+
+ self.add_scheduled_losses(resource)
+
+ @classmethod
+ def_get_res(cls,res_df,output_request):
+"""Get the resource arrays and pass through for output (single site).
+
+ Parameters
+ ----------
+ res_df : pd.DataFrame
+ 2D table with resource data.
+ output_request : list
+ Outputs to retrieve from SAM.
+
+ Returns
+ -------
+ res_mean : dict | None
+ Dictionary object with variables for resource arrays.
+ out_req_cleaned : list
+ Output request list with the resource request entries removed.
+ """
+
+ out_req_cleaned=copy.deepcopy(output_request)
+ res_out=None
+
+ res_reqs=[]
+ ti=res_df.index
+ forreqinout_req_cleaned:
+ ifreqinres_df:
+ res_reqs.append(req)
+ ifres_outisNone:
+ res_out={}
+ res_out[req]=cls.ensure_res_len(res_df[req].values,ti)
+
+ forreqinres_reqs:
+ out_req_cleaned.remove(req)
+
+ returnres_out,out_req_cleaned
+
+ @staticmethod
+ def_get_res_mean(resource,res_gid,output_request):
+"""Get the resource annual means (single site).
+
+ Parameters
+ ----------
+ resource : rex.sam_resource.SAMResource
+ SAM resource object for WIND resource
+ res_gid : int
+ Site to extract means for
+ output_request : list
+ Outputs to retrieve from SAM.
+
+ Returns
+ -------
+ res_mean : dict | None
+ Dictionary object with variables for resource means.
+ out_req_nomeans : list
+ Output request list with the resource mean entries removed.
+ """
+
+ out_req_nomeans=copy.deepcopy(output_request)
+ res_mean=None
+ idx=resource.sites.index(res_gid)
+ irrad_means=(
+ "dni_mean",
+ "dhi_mean",
+ "ghi_mean",
+ "clearsky_dni_mean",
+ "clearsky_dhi_mean",
+ "clearsky_ghi_mean",
+ )
+
+ if"ws_mean"inout_req_nomeans:
+ out_req_nomeans.remove("ws_mean")
+ res_mean={}
+ res_mean["ws_mean"]=resource["mean_windspeed",idx]
+
+ else:
+ forvarinresource.var_list:
+ label_1="{}_mean".format(var)
+ label_2="mean_{}".format(var)
+ iflabel_1inout_req_nomeans:
+ out_req_nomeans.remove(label_1)
+ ifres_meanisNone:
+ res_mean={}
+ res_mean[label_1]=resource[label_2,idx]
+
+ iflabel_1inirrad_means:
+ # convert to kWh/m2/day
+ res_mean[label_1]/=1000
+ res_mean[label_1]*=24
+
+ returnres_mean,out_req_nomeans
+
+
[docs]defcheck_resource_data(self,resource):
+"""Check resource dataframe for NaN values
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ """
+ ifpd.isna(resource).any().any():
+ bad_vars=pd.isna(resource).any(axis=0)
+ bad_vars=resource.columns[bad_vars].values.tolist()
+ msg="Found NaN values for site {} in variables {}".format(
+ self.site,bad_vars
+ )
+ logger.error(msg)
+ raiseInputError(msg)
+
+ iflen(resource)<8760:
+ msg=(f"Detected resource time series of length {len(resource)}, "
+ "which is less than 8760. This may yield unexpected "
+ "results or fail altogether. If this is not intentional, "
+ "try setting 'time_index_step: 1' in your SAM config or "
+ "double check the resource input you're using.")
+ logger.warning(msg)
+ warn(msg)
+
+
[docs]@abstractmethod
+ defset_resource_data(self,resource,meta):
+"""Placeholder for resource data setting (nsrdb or wtk)"""
+
+
[docs]@staticmethod
+ deftz_elev_check(sam_sys_inputs,site_sys_inputs,meta):
+"""Check timezone+elevation input and use json config
+ timezone+elevation if not in resource meta.
+
+ Parameters
+ ----------
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ meta : pd.DataFrame | pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+
+ Returns
+ -------
+ meta : pd.DataFrame | pd.Series
+ Dataframe or series for a single site. Will include "timezone"
+ and "elevation" from the sam and site system inputs if found.
+ """
+
+ ifmetaisnotNone:
+ axis=0ifisinstance(meta,pd.core.series.Series)else1
+ meta=meta.rename(
+ SupplyCurveField.map_to(ResourceMetaField),axis=axis
+ )
+ ifsam_sys_inputsisnotNone:
+ ifResourceMetaField.ELEVATIONinsam_sys_inputs:
+ meta[ResourceMetaField.ELEVATION]=sam_sys_inputs[
+ ResourceMetaField.ELEVATION
+ ]
+ ifResourceMetaField.TIMEZONEinsam_sys_inputs:
+ meta[ResourceMetaField.TIMEZONE]=int(
+ sam_sys_inputs[ResourceMetaField.TIMEZONE]
+ )
+
+ # site-specific inputs take priority over generic system inputs
+ ifsite_sys_inputsisnotNone:
+ ifResourceMetaField.ELEVATIONinsite_sys_inputs:
+ meta[ResourceMetaField.ELEVATION]=site_sys_inputs[
+ ResourceMetaField.ELEVATION
+ ]
+ ifResourceMetaField.TIMEZONEinsite_sys_inputs:
+ meta[ResourceMetaField.TIMEZONE]=int(
+ site_sys_inputs[ResourceMetaField.TIMEZONE]
+ )
+
+ ifResourceMetaField.TIMEZONEnotinmeta:
+ msg=(
+ "Need timezone input to run SAM gen. Not found in "
+ "resource meta or technology json input config."
+ )
+ raiseSAMExecutionError(msg)
+
+ returnmeta
+
+ @property
+ defhas_timezone(self):
+"""Returns true if instance has a timezone set"""
+ ifself._metaisnotNoneandResourceMetaField.TIMEZONEinself.meta:
+ returnTrue
+
+ returnFalse
+
+
[docs]defcf_mean(self):
+"""Get mean capacity factor (fractional) from SAM.
+
+ Returns
+ -------
+ output : float
+ Mean capacity factor (fractional).
+ """
+ returnself["capacity_factor"]/100
+
+
[docs]defcf_profile(self):
+"""Get hourly capacity factor (frac) profile in local timezone.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ cf_profile : np.ndarray
+ 1D numpy array of capacity factor profile.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnself.gen_profile()/self.sam_sys_inputs["system_capacity"]
+
+
[docs]defannual_energy(self):
+"""Get annual energy generation value in kWh from SAM.
+
+ Returns
+ -------
+ output : float
+ Annual energy generation (kWh).
+ """
+ returnself["annual_energy"]
+
+
[docs]defenergy_yield(self):
+"""Get annual energy yield value in kwh/kw from SAM.
+
+ Returns
+ -------
+ output : float
+ Annual energy yield (kwh/kw).
+ """
+ returnself["kwh_per_kw"]
+
+
[docs]defgen_profile(self):
+"""Get power generation profile (local timezone) in kW.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ output : np.ndarray
+ 1D array of hourly power generation in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnnp.array(self["gen"],dtype=np.float32)
+
+
[docs]defcollect_outputs(self,output_lookup=None):
+"""Collect SAM output_request, convert timeseries outputs to UTC, and
+ save outputs to self.outputs property.
+
+
+ Parameters
+ ----------
+ output_lookup : dict | None
+ Lookup dictionary mapping output keys to special output methods.
+ None defaults to generation default outputs.
+ """
+
+ ifoutput_lookupisNone:
+ output_lookup={
+ "cf_mean":self.cf_mean,
+ "cf_profile":self.cf_profile,
+ "annual_energy":self.annual_energy,
+ "energy_yield":self.energy_yield,
+ "gen_profile":self.gen_profile,
+ }
+
+ super().collect_outputs(output_lookup=output_lookup)
[docs]defrun(self):
+"""Run a reV-SAM generation object by assigning inputs, executing the
+ SAM simulation, collecting outputs, and converting all arrays to UTC.
+ """
+ self.assign_inputs()
+ self.execute()
+ self.collect_outputs()
+
+
[docs]@classmethod
+ defreV_run(
+ cls,
+ points_control,
+ res_file,
+ site_df,
+ lr_res_file=None,
+ output_request=("cf_mean",),
+ drop_leap=False,
+ gid_map=None,
+ nn_map=None,
+ bias_correct=None,
+ ):
+"""Execute SAM generation based on a reV points control instance.
+
+ Parameters
+ ----------
+ points_control : config.PointsControl
+ PointsControl instance containing project points site and SAM
+ config info.
+ res_file : str
+ Resource file with full path.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ lr_res_file : str | None
+ Optional low resolution resource file that will be dynamically
+ mapped+interpolated to the nominal-resolution res_file. This
+ needs to be of the same format as resource_file, e.g. they both
+ need to be handled by the same rex Resource handler such as
+ WindResource
+ output_request : list | tuple
+ Outputs to retrieve from SAM.
+ drop_leap : bool
+ Drops February 29th from the resource data. If False, December
+ 31st is dropped from leap years.
+ gid_map : None | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This enables the user to input unique
+ generation gids in the project points that map to non-unique
+ resource gids. This can be None or a pre-extracted dict.
+ nn_map : np.ndarray
+ Optional 1D array of nearest neighbor mappings associated with the
+ res_file to lr_res_file spatial mapping. For details on this
+ argument, see the rex.MultiResolutionResource docstring.
+ bias_correct : None | pd.DataFrame
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+
+ Returns
+ -------
+ out : dict
+ Nested dictionaries where the top level key is the site index,
+ the second level key is the variable name, second level value is
+ the output variable value.
+ """
+ # initialize output dictionary
+ out={}
+
+ # Get the RevPySam resource object
+ resources=RevPySam.get_sam_res(
+ res_file,
+ points_control.project_points,
+ points_control.project_points.tech,
+ output_request=output_request,
+ gid_map=gid_map,
+ lr_res_file=lr_res_file,
+ nn_map=nn_map,
+ bias_correct=bias_correct,
+ )
+
+ # run resource through curtailment filter if applicable
+ curtailment=points_control.project_points.curtailment
+ ifcurtailmentisnotNone:
+ resources=curtail(
+ resources,curtailment,random_seed=curtailment.random_seed
+ )
+
+ # iterate through project_points gen_gid values
+ forgen_gidinpoints_control.project_points.sites:
+ # Lookup the resource gid if there's a mapping and get the resource
+ # data from the SAMResource object using the res_gid.
+ res_gid=gen_gidifgid_mapisNoneelsegid_map[gen_gid]
+ site_res_df,site_meta=resources._get_res_df(res_gid)
+
+ # drop the leap day
+ ifdrop_leap:
+ site_res_df=cls.drop_leap(site_res_df)
+
+ _,inputs=points_control.project_points[gen_gid]
+
+ # get resource data pass-throughs and resource means
+ res_outs,out_req_cleaned=cls._get_res(
+ site_res_df,output_request
+ )
+ res_mean,out_req_cleaned=cls._get_res_mean(
+ resources,res_gid,out_req_cleaned
+ )
+
+ # iterate through requested sites.
+ sim=cls(
+ resource=site_res_df,
+ meta=site_meta,
+ sam_sys_inputs=inputs,
+ output_request=out_req_cleaned,
+ site_sys_inputs=dict(site_df.loc[gen_gid,:]),
+ )
+ sim.run_gen_and_econ()
+
+ # collect outputs to dictout
+ out[gen_gid]=sim.outputs
+
+ ifres_outsisnotNone:
+ out[gen_gid].update(res_outs)
+
+ ifres_meanisnotNone:
+ out[gen_gid].update(res_mean)
+
+ returnout
+
+
+
[docs]classAbstractSamGenerationFromWeatherFile(AbstractSamGeneration,ABC):
+"""Base class for running sam generation with a weather file on disk."""
+
+ WF_META_DROP_COLS={
+ ResourceMetaField.LATITUDE,
+ ResourceMetaField.LONGITUDE,
+ ResourceMetaField.ELEVATION,
+ ResourceMetaField.TIMEZONE,
+ ResourceMetaField.COUNTRY,
+ ResourceMetaField.STATE,
+ ResourceMetaField.COUNTY,
+ "urban",
+ "population",
+ "landcover",
+ }
+
+ @property
+ @abstractmethod
+ defPYSAM_WEATHER_TAG(self):
+"""Name of the weather file input used by SAM generation module."""
+ raiseNotImplementedError
+
+
[docs]defset_resource_data(self,resource,meta):
+"""Generate the weather file and set the path as an input.
+
+ Some PySAM models require a data file, not raw data. This method
+ generates the weather data, writes it to a file on disk, and
+ then sets the file as an input to the generation module. The
+ function
+ :meth:`~AbstractSamGenerationFromWeatherFile.run_gen_and_econ`
+ deletes the file on disk after a run is complete.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Time series resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the
+ required variables to run the respective SAM simulation.
+ Remapping will be done to convert typical NSRDB/WTK names
+ into SAM names (e.g. DNI -> dn and wind_speed -> windspeed).
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude,
+ elevation, and timezone.
+ """
+ meta=self._parse_meta(meta)
+ self.time_interval=self.get_time_interval(resource.index.values)
+ pysam_w_fname=self._create_pysam_wfile(resource,meta)
+ self[self.PYSAM_WEATHER_TAG]=pysam_w_fname
+
+ def_create_pysam_wfile(self,resource,meta):
+"""Create PySAM weather input file.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Time series resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the
+ required variables to run the respective SAM simulation.
+ Remapping will be done to convert typical NSRDB/WTK names
+ into SAM names (e.g. DNI -> dn and wind_speed -> windspeed).
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude,
+ elevation, and timezone.
+
+ Returns
+ -------
+ fname : str
+ Name of weather csv file.
+
+ Notes
+ -----
+ PySAM will not accept data on Feb 29th. For leap years,
+ December 31st is dropped and time steps are shifted to relabel
+ Feb 29th as March 1st, March 1st as March 2nd, etc.
+ """
+ # pylint: disable=attribute-defined-outside-init,consider-using-with
+ self._temp_dir=TemporaryDirectory()
+ fname=os.path.join(self._temp_dir.name,"weather.csv")
+ logger.debug("Creating PySAM weather data file: {}".format(fname))
+
+ # ------- Process metadata
+ m=pd.DataFrame(meta).T
+ timezone=m[ResourceMetaField.TIMEZONE]
+ m["Source"]="NSRDB"
+ m["Location ID"]=meta.name
+ m["City"]="-"
+ m["State"]=m["state"].apply(lambdax:"-"ifx=="None"elsex)
+ m["Country"]=m["country"].apply(lambdax:"-"ifx=="None"elsex)
+ m["Latitude"]=m[ResourceMetaField.LATITUDE]
+ m["Longitude"]=m[ResourceMetaField.LONGITUDE]
+ m["Time Zone"]=timezone
+ m["Elevation"]=m[ResourceMetaField.ELEVATION]
+ m["Local Time Zone"]=timezone
+ m["Dew Point Units"]="c"
+ m["DHI Units"]="w/m2"
+ m["DNI Units"]="w/m2"
+ m["Temperature Units"]="c"
+ m["Pressure Units"]="mbar"
+ m["Wind Speed"]="m/s"
+ keep_cols=[cforcinm.columnsifcnotinself.WF_META_DROP_COLS]
+ m[keep_cols].to_csv(fname,index=False,mode="w")
+
+ # --------- Process data
+ var_map={
+ "dni":"DNI",
+ "dhi":"DHI",
+ "wind_speed":"Wind Speed",
+ "air_temperature":"Temperature",
+ "dew_point":"Dew Point",
+ "surface_pressure":"Pressure",
+ }
+ resource=resource.rename(mapper=var_map,axis="columns")
+
+ time_index=resource.index
+ # Adjust from UTC to local time
+ local=np.roll(
+ resource.values,int(timezone*self.time_interval),axis=0
+ )
+ resource=pd.DataFrame(
+ local,columns=resource.columns,index=time_index
+ )
+ mask=(time_index.month==2)&(time_index.day==29)
+ time_index=time_index[~mask]
+
+ df=pd.DataFrame(index=time_index)
+ df["Year"]=time_index.year
+ df["Month"]=time_index.month
+ df["Day"]=time_index.day
+ df["Hour"]=time_index.hour
+ df["Minute"]=time_index.minute
+ df=df.join(resource.loc[~mask])
+
+ df.to_csv(fname,index=False,mode="a")
+
+ returnfname
+
+
[docs]classAbstractSamSolar(AbstractSamGeneration,ABC):
+"""Base Class for Solar generation from SAM"""
+
+
[docs]@staticmethod
+ defagg_albedo(time_index,albedo):
+"""Aggregate a timeseries of albedo data to monthly values w len 12 as
+ required by pysam Pvsamv1
+
+ Tech spec from pysam docs:
+ https://nrel-pysam.readthedocs.io/en/master/modules/Pvsamv1.html
+ #PySAM.Pvsamv1.Pvsamv1.SolarResource.albedo
+
+ Parameters
+ ----------
+ time_index : pd.DatetimeIndex
+ Timeseries solar resource datetimeindex
+ albedo : list
+ Timeseries Albedo data to be aggregated. Should be 0-1 and likely
+ hourly or less.
+
+ Returns
+ -------
+ monthly_albedo : list
+ 1D list of monthly albedo values with length 12
+ """
+ monthly_albedo=np.zeros(12).tolist()
+ albedo=np.array(albedo)
+ formonthinrange(1,13):
+ m=np.where(time_index.month==month)[0]
+ monthly_albedo[int(month-1)]=albedo[m].mean()
+
+ returnmonthly_albedo
+
+
[docs]defset_resource_data(self,resource,meta):
+"""Set NSRDB resource data arrays.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ """
+
+ meta=self._parse_meta(meta)
+ time_index=resource.index
+ self.time_interval=self.get_time_interval(resource.index.values)
+
+ # map resource data names to SAM required data names
+ var_map={
+ "dni":"dn",
+ "dhi":"df",
+ "ghi":"gh",
+ "clearskydni":"dn",
+ "clearskydhi":"df",
+ "clearskyghi":"gh",
+ "windspeed":"wspd",
+ "airtemperature":"tdry",
+ "temperature":"tdry",
+ "temp":"tdry",
+ "dewpoint":"tdew",
+ "surfacepressure":"pres",
+ "pressure":"pres",
+ "surfacealbedo":"albedo",
+ }
+ lower_case={
+ k:k.lower().replace(" ","").replace("_","")
+ forkinresource.columns
+ }
+ irrad_vars=["dn","df","gh"]
+
+ resource=resource.rename(mapper=lower_case,axis="columns")
+ resource=resource.rename(mapper=var_map,axis="columns")
+ time_index=resource.index
+ resource={
+ k:np.array(v)
+ for(k,v)inresource.to_dict(orient="list").items()
+ }
+
+ # set resource variables
+ forvar,arrinresource.items():
+ ifvar!="time_index":
+ # ensure that resource array length is multiple of 8760
+ arr=self.ensure_res_len(arr,time_index)
+ n_roll=int(
+ self._meta[ResourceMetaField.TIMEZONE]*self.time_interval
+ )
+ arr=np.roll(arr,n_roll)
+
+ ifvarinirrad_varsandnp.min(arr)<0:
+ warn(
+ 'Solar irradiance variable "{}" has a minimum '
+ "value of {}. Truncating to zero.".format(
+ var,np.min(arr)
+ ),
+ SAMInputWarning,
+ )
+ arr=np.where(arr<0,0,arr)
+
+ resource[var]=arr.tolist()
+
+ resource["lat"]=meta[ResourceMetaField.LATITUDE]
+ resource["lon"]=meta[ResourceMetaField.LONGITUDE]
+ resource["tz"]=meta[ResourceMetaField.TIMEZONE]
+
+ resource["elev"]=meta.get(ResourceMetaField.ELEVATION,0.0)
+
+ time_index=self.ensure_res_len(time_index,time_index)
+ resource["minute"]=time_index.minute
+ resource["hour"]=time_index.hour
+ resource["month"]=time_index.month
+ resource["year"]=time_index.year
+ resource["day"]=time_index.day
+
+ if"albedo"inresource:
+ self["albedo"]=self.agg_albedo(
+ time_index,resource.pop("albedo")
+ )
+
+ self["solar_resource_data"]=resource
+
+
+
[docs]classAbstractSamPv(AbstractSamSolar,ABC):
+"""Photovoltaic (PV) generation with either pvwatts of detailed pv."""
+
+ # set these class attrs in concrete subclasses
+ MODULE=None
+ PYSAM=None
+
+ # pylint: disable=line-too-long
+ def__init__(
+ self,
+ resource,
+ meta,
+ sam_sys_inputs,
+ site_sys_inputs=None,
+ output_request=None,
+ drop_leap=False,
+ ):
+"""Initialize a SAM solar object.
+
+ See the PySAM :py:class:`~PySAM.Pvwattsv8.Pvwattsv8` (or older
+ version model) or :py:class:`~PySAM.Pvsamv1.Pvsamv1` documentation for
+ the configuration keys required in the `sam_sys_inputs` config for the
+ respective models. Some notable keys include the following to enable a
+ lifetime simulation (non-exhaustive):
+
+ - ``system_use_lifetime_output`` : Integer flag indicating whether
+ or not to run a full lifetime model (0 for off, 1 for on). If
+ running a lifetime model, the resource file will be repeated
+ for the number of years specified as the lifetime of the
+ plant and a performance degradation term will be used to
+ simulate reduced performance over time.
+ - ``analysis_period`` : Integer representing the number of years
+ to include in the lifetime of the model generator. Required if
+ ``system_use_lifetime_output`` is set to 1.
+ - ``dc_degradation`` : List of percentage values representing the
+ annual DC degradation of capacity factors. Maybe a single value
+ that will be compound each year or a vector of yearly rates.
+ Required if ``system_use_lifetime_output`` is set to 1.
+
+ You may also include the following ``reV``-specific keys:
+
+ - ``reV_outages`` : Specification for ``reV``-scheduled
+ stochastic outage losses. For example::
+
+ outage_info = [
+ {
+ 'count': 6,
+ 'duration': 24,
+ 'percentage_of_capacity_lost': 100,
+ 'allowed_months': ['January', 'March'],
+ 'allow_outage_overlap': True
+ },
+ {
+ 'count': 10,
+ 'duration': 1,
+ 'percentage_of_capacity_lost': 10,
+ 'allowed_months': ['January'],
+ 'allow_outage_overlap': False
+ },
+ ...
+ ]
+
+ See the description of
+ :meth:`~reV.losses.scheduled.ScheduledLossesMixin.add_scheduled_losses`
+ or the
+ `reV losses demo notebook <https://tinyurl.com/4d7uutt3/>`_
+ for detailed instructions on how to specify this input.
+ - ``reV_outages_seed`` : Integer value used to seed the RNG
+ used to compute stochastic outage losses.
+ - ``time_index_step`` : Integer representing the step size
+ used to sample the ``time_index`` in the resource data.
+ This can be used to reduce temporal resolution (i.e. for
+ 30 minute NSRDB input data, ``time_index_step=1`` yields
+ the full 30 minute time series as output, while
+ ``time_index_step=2`` yields hourly output, and so forth).
+
+ .. Note:: The reduced data shape (i.e. after applying a
+ step size of `time_index_step`) must still be an
+ integer multiple of 8760, or the execution will
+ fail.
+
+ - ``clearsky`` : Boolean flag value indicating wether
+ computation should use clearsky resource data to compute
+ generation data.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ meta : pd.DataFrame | pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ output_request : list
+ Requested SAM outputs (e.g., 'cf_mean', 'annual_energy',
+ 'cf_profile', 'gen_profile', 'energy_yield', 'ppa_price',
+ 'lcoe_fcr').
+ drop_leap : bool
+ Drops February 29th from the resource data. If False, December
+ 31st is dropped from leap years.
+ """
+
+ # need to check tilt=lat and azimuth for pv systems
+ meta=self._parse_meta(meta)
+ sam_sys_inputs=self.set_latitude_tilt_az(sam_sys_inputs,meta)
+
+ super().__init__(
+ resource,
+ meta,
+ sam_sys_inputs,
+ site_sys_inputs=site_sys_inputs,
+ output_request=output_request,
+ drop_leap=drop_leap,
+ )
+
+
[docs]defset_resource_data(self,resource,meta):
+"""Set NSRDB resource data arrays.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+
+ Raises
+ ------
+ ValueError : If lat/lon outside of -90 to 90 and -180 to 180,
+ respectively.
+
+ """
+ bad_location_input=(
+ (meta[ResourceMetaField.LATITUDE]<-90)
+ |(meta[ResourceMetaField.LATITUDE]>90)
+ |(meta[ResourceMetaField.LONGITUDE]<-180)
+ |(meta[ResourceMetaField.LONGITUDE]>180)
+ )
+ ifbad_location_input.any():
+ raiseValueError(
+ "Detected latitude/longitude values outside of "
+ "the range -90 to 90 and -180 to 180, "
+ "respectively. Please ensure input resource data"
+ "locations conform to these ranges. "
+ )
+ returnsuper().set_resource_data(resource,meta)
+
+
[docs]@staticmethod
+ defset_latitude_tilt_az(sam_sys_inputs,meta):
+"""Check if tilt is specified as latitude and set tilt=lat, az=180 or 0
+
+ Parameters
+ ----------
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+
+ Returns
+ -------
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ If for a pv simulation the "tilt" parameter was originally not
+ present or set to 'lat' or MetaKeyName.LATITUDE, the tilt will be
+ set to the absolute value of the latitude found in meta and the
+ azimuth will be 180 if lat>0, 0 if lat<0.
+ """
+
+ set_tilt=False
+ ifsam_sys_inputsisnotNoneandmetaisnotNone:
+ if"tilt"notinsam_sys_inputs:
+ warn(
+ "No tilt specified, setting at latitude.",SAMInputWarning
+ )
+ set_tilt=True
+ elif(
+ sam_sys_inputs["tilt"]=="lat"
+ orsam_sys_inputs["tilt"]==ResourceMetaField.LATITUDE
+ )or(
+ sam_sys_inputs["tilt"]=="lat"
+ orsam_sys_inputs["tilt"]==ResourceMetaField.LATITUDE
+ ):
+ set_tilt=True
+
+ ifset_tilt:
+ # set tilt to abs(latitude)
+ sam_sys_inputs["tilt"]=np.abs(meta[ResourceMetaField.LATITUDE])
+ ifmeta[ResourceMetaField.LATITUDE]>0:
+ # above the equator, az = 180
+ sam_sys_inputs["azimuth"]=180
+ else:
+ # below the equator, az = 0
+ sam_sys_inputs["azimuth"]=0
+
+ logger.debug(
+ 'Tilt specified at "latitude", setting tilt to: {}, '
+ "azimuth to: {}".format(
+ sam_sys_inputs["tilt"],sam_sys_inputs["azimuth"]
+ )
+ )
+ returnsam_sys_inputs
+
+
[docs]defsystem_capacity_ac(self):
+"""Get AC system capacity from SAM inputs.
+
+ NOTE: AC nameplate = DC nameplate / ILR
+
+ Returns
+ -------
+ cf_profile : float
+ AC nameplate = DC nameplate / ILR
+ """
+ return(
+ self.sam_sys_inputs["system_capacity"]
+ /self.sam_sys_inputs["dc_ac_ratio"]
+ )
+
+
[docs]defcf_mean(self):
+"""Get mean capacity factor (fractional) from SAM.
+
+ NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+ Returns
+ -------
+ output : float
+ Mean capacity factor (fractional).
+ PV CF is calculated as AC power / DC nameplate.
+ """
+ returnself["capacity_factor"]/100
+
+
[docs]defcf_mean_ac(self):
+"""Get mean AC capacity factor (fractional) from SAM.
+
+ NOTE: This value only available in PVWattsV8 and up.
+
+ Returns
+ -------
+ output : float
+ Mean AC capacity factor (fractional).
+ PV AC CF is calculated as AC power / AC nameplate.
+ """
+ returnself["capacity_factor_ac"]/100
+
+
[docs]defcf_profile(self):
+"""Get hourly capacity factor (frac) profile in local timezone.
+ See self.outputs attribute for collected output data in UTC.
+
+ NOTE: PV capacity factor is the AC power production / the DC nameplate
+
+ Returns
+ -------
+ cf_profile : np.ndarray
+ 1D numpy array of capacity factor profile.
+ Datatype is float32 and array length is 8760*time_interval.
+ PV CF is calculated as AC power / DC nameplate.
+ """
+ returnself.gen_profile()/self.sam_sys_inputs["system_capacity"]
+
+
[docs]defcf_profile_ac(self):
+"""Get hourly AC capacity factor (frac) profile in local timezone.
+ See self.outputs attribute for collected output data in UTC.
+
+ NOTE: PV AC capacity factor is the AC power production / the AC
+ nameplate. AC nameplate = DC nameplate / ILR
+
+ Returns
+ -------
+ cf_profile : np.ndarray
+ 1D numpy array of capacity factor profile.
+ Datatype is float32 and array length is 8760*time_interval.
+ PV AC CF is calculated as AC power / AC nameplate.
+ """
+ returnself.gen_profile()/self.system_capacity_ac()
+
+
[docs]defgen_profile(self):
+"""Get AC inverter power generation profile (local timezone) in kW.
+ This is an alias of the "ac" SAM output variable if PySAM version>=3.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ output : np.ndarray
+ 1D array of AC inverter power generation in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnnp.array(self["gen"],dtype=np.float32)
+
+
[docs]defac(self):
+"""Get AC inverter power generation profile (local timezone) in kW.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ output : np.ndarray
+ 1D array of AC inverter power generation in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnnp.array(self["ac"],dtype=np.float32)/1000
+
+
[docs]defdc(self):
+"""
+ Get DC array power generation profile (local timezone) in kW.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ output : np.ndarray
+ 1D array of DC array power generation in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnnp.array(self["dc"],dtype=np.float32)/1000
+
+
[docs]defclipped_power(self):
+"""
+ Get the clipped DC power generated behind the inverter
+ (local timezone) in kW.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ clipped : np.ndarray
+ 1D array of clipped DC power in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ ac=self.ac()
+ dc=self.dc()
+
+ returnnp.where(ac<ac.max(),0,dc-ac)
[docs]defac(self):
+"""Get AC inverter power generation profile (local timezone) in kW.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ output : np.ndarray
+ 1D array of AC inverter power generation in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnnp.array(self["gen"],dtype=np.float32)
+
+
[docs]defdc(self):
+"""
+ Get DC array power generation profile (local timezone) in kW.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ output : np.ndarray
+ 1D array of DC array power generation in kW.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnnp.array(self["dc_net"],dtype=np.float32)
[docs]classTcsMoltenSalt(AbstractSamSolar):
+"""Concentrated Solar Power (CSP) generation with tower molten salt"""
+
+ MODULE="tcsmolten_salt"
+ PYSAM=PySamCSP
+
+
[docs]defcf_profile(self):
+"""Get absolute value hourly capacity factor (frac) profile in
+ local timezone.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ cf_profile : np.ndarray
+ 1D numpy array of capacity factor profile.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ x=np.abs(self.gen_profile()/self.sam_sys_inputs["system_capacity"])
+ returnx
[docs]classGeothermal(AbstractSamGenerationFromWeatherFile):
+"""reV-SAM geothermal generation.
+
+ As of 12/20/2022, the resource potential input in SAM is only used
+ to calculate the number of well replacements during the lifetime of
+ a geothermal plant. It was decided that reV would not model well
+ replacements. Therefore, reV sets the resource potential to match
+ (or be just above) the gross potential so that SAM does not throw
+ any errors.
+
+ Also as of 12/20/2022, the SAM GETEM module requires a weather file,
+ but does not actually require any weather data to run. Therefore,
+ reV currently generates an empty weather file to pass to SAM. This
+ behavior can be easily updated in the future should the SAM GETEM
+ module start using weather data.
+
+ See the PySAM :py:class:`~PySAM.Geothermal.Geothermal` documentation
+ for the configuration keys required in the `sam_sys_inputs` config.
+ Some notable keys include (non-exhaustive):
+
+ - ``resource_type`` : Integer flag representing either
+ Hydrothermal (0) or EGS (1) resource. Only values of 0 or 1
+ allowed.
+ - ``resource_potential`` : Total resource potential at location
+ (in MW).
+
+ .. Important:: ``reV`` automatically sets the resource
+ potential to match the gross potential (see documentation
+ above), so this key should be left out of the config (it
+ will be overridden in any case).
+
+ - ``resource_temp`` : Temperature of resource (in C).
+
+ .. Important:: This value is set by ``reV`` based on the
+ user's geothermal resource data input. To override this
+ behavior, users *may* specify their own ``resource_temp``
+ value (either a single value for all sites in the SAM
+ geothermal config or a site-dependent value in the project
+ points CSV). In this case, the resource temperature from
+ the input data will be ignored completely, and the
+ temperature at each location will be determined solely from
+ this input.
+
+ - ``resource_depth`` : Depth to geothermal resource (in m).
+ - ``analysis_type`` : Integer flag representing the plant
+ configuration. If the ``nameplate`` input is to be used to
+ specify the plant capacity, then this flag should be set to 0
+ (this is the default ``reV`` assumption). Otherwise, if the
+ ``num_wells`` input is to be used to specify the plant site,
+ then this flag should be set to 1. Only values of 0 or 1
+ allowed.
+
+ - ``nameplate`` : Geothermal plant size (in kW). Only affects
+ the output if ``analysis_type=0``.
+
+ .. Important:: Unlike wind or solar, ``reV`` geothermal
+ dynamically sets the size of a geothermal plant. In
+ particular, the plant capacity is set to match the resource
+ potential (obtained from the input data) for each site. For
+ this to work, users **must** leave out the ``nameplate``
+ key from the SAM config.
+
+ Alternatively, users *may* specify their own ``nameplate``
+ capacity value (either a single value for all sites in the
+ SAM geothermal config or a site-dependent value in the
+ project points CSV). In this case, the resource potential
+ from the input data will be ignored completely, and the
+ capacity at each location will be determined solely from
+ this input.
+
+ - ``num_wells`` : Number of wells at each plant. This value is
+ used to determined plant capacity if ``analysis_type=1``.
+ Otherwise this input has no effect.
+ - ``num_wells_getem`` : Number of wells assumed at each plant
+ for power block calculations. Only affects power block outputs
+ if ``analysis_type=0`` (otherwise the ``num_wells`` input is
+ used in power block calculations).
+
+ .. Note:: ``reV`` does not currently adjust this value based
+ on the resource input (as it probably should). If any
+ power block outputs are required in the future, there may
+ need to be extra development to set this value based on
+ the dynamically calculated plant size.
+
+ - ``conversion_type`` : Integer flag representing the conversion
+ plant type. Either Binary (0) or Flash (1). Only values of 0
+ or 1 allowed.
+ - ``geotherm.cost.inj_prod_well_ratio`` : Fraction representing
+ the injection to production well ratio (0-1). SAM GUI defaults
+ to 0.5 for this value, but it is recommended to set this to
+ the GETEM default of 0.75.
+
+
+ You may also include the following ``reV``-specific keys:
+
+ - ``num_confirmation_wells`` : Number of confirmation wells that
+ can also be used as production wells. This number is used to
+ determined to total number of wells required at each plant,
+ and therefore the total drilling costs. This value defaults to
+ 2 (to match the SAM GUI as of 8/1/2023). However, the default
+ value can lead to negative costs if the plant size is small
+ (e.g. only 1 production well is needed, so the costs equal
+ -1 * ``drill_cost_per_well``). This is a limitation of the
+ SAM calculations (as of 8/1/2023), and it is therefore useful
+ to set ``num_confirmation_wells=0`` when performing ``reV``
+ runs for small plant sizes.
+ - ``capital_cost_per_kw`` : Capital cost values in $/kW. If
+ this value is specified in the config, reV calculates and
+ overrides the total ``capital_cost`` value based on the
+ geothermal plant size (capacity) at each location.
+ - ``fixed_operating_cost`` : Fixed operating cost values in
+ $/kW. If this value is specified in the config, reV calculates
+ and overrides the total ``fixed_operating_cost`` value based
+ on the geothermal plant size (capacity) at each location.
+ - ``drill_cost_per_well`` : Drilling cost per well, in $. If
+ this value is specified in the config, reV calculates the
+ total drilling costs based on the number of wells that need to
+ be drilled at each location. The drilling costs are added to
+ the total ``capital_cost`` at each location.
+ - ``reV_outages`` : Specification for ``reV``-scheduled
+ stochastic outage losses. For example::
+
+ outage_info = [
+ {
+ 'count': 6,
+ 'duration': 24,
+ 'percentage_of_capacity_lost': 100,
+ 'allowed_months': ['January', 'March'],
+ 'allow_outage_overlap': True
+ },
+ {
+ 'count': 10,
+ 'duration': 1,
+ 'percentage_of_capacity_lost': 10,
+ 'allowed_months': ['January'],
+ 'allow_outage_overlap': False
+ },
+ ...
+ ]
+
+ See the description of
+ :meth:`~reV.losses.scheduled.ScheduledLossesMixin.add_scheduled_losses`
+ or the
+ `reV losses demo notebook <https://tinyurl.com/4d7uutt3/>`_
+ for detailed instructions on how to specify this input.
+ - ``reV_outages_seed`` : Integer value used to seed the RNG
+ used to compute stochastic outage losses.
+ - ``time_index_step`` : Integer representing the step size
+ used to sample the ``time_index`` in the resource data.
+ This can be used to reduce temporal resolution (i.e. for
+ 30 minute NSRDB input data, ``time_index_step=1`` yields
+ the full 30 minute time series as output, while
+ ``time_index_step=2`` yields hourly output, and so forth).
+
+ """
+
+ MODULE="geothermal"
+ PYSAM=PySamGeothermal
+ PYSAM_WEATHER_TAG="file_name"
+ _RESOURCE_POTENTIAL_MULT=1.001
+ _DEFAULT_NUM_CONFIRMATION_WELLS=2# SAM GUI default as of 5/26/23
+
+
[docs]defcf_profile(self):
+"""Get hourly capacity factor (frac) profile in local timezone.
+ See self.outputs attribute for collected output data in UTC.
+
+ Returns
+ -------
+ cf_profile : np.ndarray
+ 1D numpy array of capacity factor profile.
+ Datatype is float32 and array length is 8760*time_interval.
+ """
+ returnself.gen_profile()/self.sam_sys_inputs["nameplate"]
+
+
[docs]defassign_inputs(self):
+"""Assign the self.sam_sys_inputs attribute to the PySAM object."""
+ ifself.sam_sys_inputs.get("ui_calculations_only"):
+ msg=(
+ "reV requires model run - cannot set "
+ '"ui_calculations_only" to `True` (1). Automatically '
+ "setting to `False` (0)!"
+ )
+ logger.warning(msg)
+ warn(msg)
+ self.sam_sys_inputs["ui_calculations_only"]=0
+ super().assign_inputs()
+
+
[docs]defset_resource_data(self,resource,meta):
+"""Generate the weather file and set the path as an input.
+
+ The Geothermal PySAM model requires a data file, not raw data.
+ This method generates the weather data, writes it to a file on
+ disk, and then sets the file as an input to the Geothermal
+ generation module. The function
+ :meth:`~AbstractSamGenerationFromWeatherFile.run_gen_and_econ`
+ deletes the file on disk after a run is complete.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Time series resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the
+ required variables to run the respective SAM simulation.
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude,
+ elevation, and timezone.
+ """
+ super().set_resource_data(resource,meta)
+ self._set_resource_temperature(resource)
+ self._set_egs_plant_design_temperature()
+ self._set_nameplate_to_match_resource_potential(resource)
+ self._set_resource_potential_to_match_gross_output()
+ self._set_costs()
+
+ def_set_resource_temperature(self,resource):
+"""Set resource temp from data if user did not specify it."""
+
+ if"resource_temp"inself.sam_sys_inputs:
+ logger.debug(
+ "Found 'resource_temp' value in SAM config: {:.2f}".format(
+ self.sam_sys_inputs["resource_temp"]
+ )
+ )
+ return
+
+ val=set(resource["temperature"].unique())
+ logger.debug(
+ "Found {} value(s) for 'temperature' in resource data".format(
+ len(val)
+ )
+ )
+ iflen(val)>1:
+ msg=(
+ "Found multiple values for 'temperature' for site "
+ "{}: {}".format(self.site,val)
+ )
+ logger.error(msg)
+ raiseInputError(msg)
+
+ val=val.pop()
+ logger.debug(
+ "Input 'resource_temp' not found in SAM config - setting "
+ "to {:.2f} based on input resource data.".format(val)
+ )
+ self.sam_sys_inputs["resource_temp"]=val
+
+ def_set_egs_plant_design_temperature(self):
+"""Set the EGS plant temp to match resource (avoids cf > 1)"""
+ ifself.sam_sys_inputs.get("resource_type")!=1:
+ return# Not EGS run
+
+ resource_temp=self.sam_sys_inputs["resource_temp"]
+ logger.debug("Setting EGS plant design temperature to match "
+ "resource temperature ({}C)".format(resource_temp))
+ self.sam_sys_inputs["design_temp"]=resource_temp
+
+ def_set_nameplate_to_match_resource_potential(self,resource):
+"""Set the nameplate capacity to match the resource potential."""
+
+ if"nameplate"inself.sam_sys_inputs:
+ msg=(
+ 'Found "nameplate" input in config! Resource potential '
+ "from input data will be ignored. Nameplate capacity is "
+ "{}".format(self.sam_sys_inputs["nameplate"])
+ )
+ logger.info(msg)
+ # required for downstream LCOE calcs
+ self.sam_sys_inputs["system_capacity"]=(
+ self.sam_sys_inputs["nameplate"]
+ )
+ return
+
+ val=set(resource["potential_MW"].unique())
+ iflen(val)>1:
+ msg=(
+ 'Found multiple values for "potential_MW" for site '
+ "{}: {}".format(self.site,val)
+ )
+ logger.error(msg)
+ raiseInputError(msg)
+
+ val=val.pop()*1000
+
+ logger.debug("Setting the nameplate to {}".format(val))
+ self.sam_sys_inputs["nameplate"]=val
+ # required for downstream LCOE calcs
+ self.sam_sys_inputs["system_capacity"]=val
+
+ def_set_resource_potential_to_match_gross_output(self):
+"""Set the resource potential input to match the gross generation.
+
+ If SAM throws an error during the UI calculation of the gross
+ output, the resource_potential is simply set to -1 since
+ SAM will error out for this point regardless of the
+ resource_potential input.
+ """
+
+ super().assign_inputs()
+ self["ui_calculations_only"]=1
+ try:
+ self.execute()
+ exceptSAMExecutionError:
+ self["ui_calculations_only"]=0
+ self.sam_sys_inputs["resource_potential"]=-1
+ return
+
+ gross_gen=(
+ self.pysam.Outputs.gross_output*self._RESOURCE_POTENTIAL_MULT
+ )
+ if"resource_potential"inself.sam_sys_inputs:
+ msg=(
+ 'Setting "resource_potential" is not allowed! Updating '
+ "user input of {} to match the gross generation: {}".format(
+ self.sam_sys_inputs["resource_potential"],gross_gen
+ )
+ )
+ logger.warning(msg)
+ warn(msg)
+
+ logger.debug(
+ "Setting the resource potential to {} MW".format(gross_gen)
+ )
+ self.sam_sys_inputs["resource_potential"]=gross_gen
+
+ ncw=self.sam_sys_inputs.pop(
+ "num_confirmation_wells",self._DEFAULT_NUM_CONFIRMATION_WELLS
+ )
+ self.sam_sys_inputs["prod_and_inj_wells_to_drill"]=(
+ self.pysam.Outputs.num_wells_getem_output
+ -ncw
+ +self.pysam.Outputs.num_wells_getem_inj
+ )
+ self["ui_calculations_only"]=0
+
+ def_set_costs(self):
+"""Set the costs based on gross plant generation."""
+ plant_size_kw=(
+ self.sam_sys_inputs["resource_potential"]
+ /self._RESOURCE_POTENTIAL_MULT
+ )*1000
+
+ cc_per_kw=self.sam_sys_inputs.pop("capital_cost_per_kw",None)
+ ifcc_per_kwisnotNone:
+ capital_cost=cc_per_kw*plant_size_kw
+ logger.debug(
+ "Setting the capital_cost to ${:,.2f}".format(capital_cost)
+ )
+ reg_mult=self.sam_sys_inputs.get("capital_cost_multiplier",1)
+ self.sam_sys_inputs["base_capital_cost"]=capital_cost
+ self.sam_sys_inputs["capital_cost"]=capital_cost*reg_mult
+
+ dc_per_well=self.sam_sys_inputs.pop("drill_cost_per_well",None)
+ num_wells=self.sam_sys_inputs.pop(
+ "prod_and_inj_wells_to_drill",None
+ )
+ ifdc_per_wellisnotNone:
+ ifnum_wellsisNone:
+ msg=(
+ "Could not determine number of wells to be drilled. "
+ "No drilling costs added!"
+ )
+ logger.warning(msg)
+ warn(msg)
+ else:
+ capital_cost=self.sam_sys_inputs["capital_cost"]
+ drill_cost=dc_per_well*num_wells
+ logger.debug(
+ "Setting the drilling cost to ${:,.2f} "
+ "({:.2f} wells at ${:,.2f} per well)".format(
+ drill_cost,num_wells,dc_per_well
+ )
+ )
+ reg_mult=self.sam_sys_inputs.get(
+ "capital_cost_multiplier",1
+ )
+ base_cc=capital_cost/reg_mult
+ new_base_cc=base_cc+drill_cost
+ self.sam_sys_inputs["base_capital_cost"]=new_base_cc
+ self.sam_sys_inputs["capital_cost"]=new_base_cc*reg_mult
+
+ foc_per_kw=self.sam_sys_inputs.pop(
+ "fixed_operating_cost_per_kw",None
+ )
+ iffoc_per_kwisnotNone:
+ foc=foc_per_kw*plant_size_kw
+ logger.debug(
+ "Setting the fixed_operating_cost to ${:,.2f}".format(foc)
+ )
+ self.sam_sys_inputs["base_fixed_operating_cost"]=foc
+ self.sam_sys_inputs["fixed_operating_cost"]=foc
+
+ voc_per_kw=self.sam_sys_inputs.pop(
+ "variable_operating_cost_per_kw",None
+ )
+ ifvoc_per_kwisnotNone:
+ voc=voc_per_kw*plant_size_kw
+ logger.debug(
+ "Setting the variable_operating_cost to ${:,.2f}".format(voc)
+ )
+ self.sam_sys_inputs["base_variable_operating_cost"]=voc
+ self.sam_sys_inputs["variable_operating_cost"]=voc
+
+ def_create_pysam_wfile(self,resource,meta):
+"""Create PySAM weather input file.
+
+ Geothermal module requires a weather file, but does not actually
+ require any weather data to run. Therefore, an empty file is
+ generated and passed through.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Time series resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the
+ required variables to run the respective SAM simulation.
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude,
+ and timezone.
+
+ Returns
+ -------
+ fname : str
+ Name of weather csv file.
+
+ Notes
+ -----
+ PySAM will not accept data on Feb 29th. For leap years,
+ December 31st is dropped and time steps are shifted to relabel
+ Feb 29th as March 1st, March 1st as March 2nd, etc.
+ """
+ # pylint: disable=attribute-defined-outside-init, consider-using-with
+ self._temp_dir=TemporaryDirectory()
+ fname=os.path.join(self._temp_dir.name,"weather.csv")
+ logger.debug("Creating PySAM weather data file: {}".format(fname))
+
+ # ------- Process metadata
+ m=pd.DataFrame(meta).T
+ m=m.rename(
+ {
+ "latitude":"Latitude",
+ "longitude":"Longitude",
+ "timezone":"Time Zone",
+ },
+ axis=1,
+ )
+
+ m[["Latitude","Longitude","Time Zone"]].to_csv(
+ fname,index=False,mode="w"
+ )
+
+ # --------- Process data, blank for geothermal
+ time_index=resource.index
+ mask=(time_index.month==2)&(time_index.day==29)
+ time_index=time_index[~mask]
+
+ df=pd.DataFrame(index=time_index)
+ df["Year"]=time_index.year
+ df["Month"]=time_index.month
+ df["Day"]=time_index.day
+ df["Hour"]=time_index.hour
+ df["Minute"]=time_index.minute
+ df.to_csv(fname,index=False,mode="a")
+
+ returnfname
+
+
[docs]defrun_gen_and_econ(self):
+"""Run SAM generation and possibility follow-on econ analysis."""
+ try:
+ super().run_gen_and_econ()
+ exceptSAMExecutionErrorase:
+ logger.error(
+ "Skipping site {}; received sam error: {}".format(
+ self._site,str(e)
+ )
+ )
+ self.outputs={}
+
+
+
[docs]classAbstractSamWind(AbstractSamGeneration,PowerCurveLossesMixin,ABC):
+"""AbstractSamWind"""
+
+ # pylint: disable=line-too-long
+ def__init__(self,*args,**kwargs):
+"""Wind generation from SAM.
+
+ See the PySAM :py:class:`~PySAM.Windpower.Windpower`
+ documentation for the configuration keys required in the
+ `sam_sys_inputs` config. You may also include the following
+ ``reV``-specific keys:
+
+ - ``reV_power_curve_losses`` : A dictionary that can be used
+ to initialize
+ :class:`~reV.losses.power_curve.PowerCurveLossesInput`.
+ For example::
+
+ reV_power_curve_losses = {
+ 'target_losses_percent': 9.8,
+ 'transformation': 'exponential_stretching'
+ }
+
+ See the description of the class mentioned above or the
+ `reV losses demo notebook <https://tinyurl.com/4d7uutt3/>`_
+ for detailed instructions on how to specify this input.
+ - ``reV_outages`` : Specification for ``reV``-scheduled
+ stochastic outage losses. For example::
+
+ outage_info = [
+ {
+ 'count': 6,
+ 'duration': 24,
+ 'percentage_of_capacity_lost': 100,
+ 'allowed_months': ['January', 'March'],
+ 'allow_outage_overlap': True
+ },
+ {
+ 'count': 10,
+ 'duration': 1,
+ 'percentage_of_capacity_lost': 10,
+ 'allowed_months': ['January'],
+ 'allow_outage_overlap': False
+ },
+ ...
+ ]
+
+ See the description of
+ :meth:`~reV.losses.scheduled.ScheduledLossesMixin.add_scheduled_losses`
+ or the
+ `reV losses demo notebook <https://tinyurl.com/4d7uutt3/>`_
+ for detailed instructions on how to specify this input.
+ - ``reV_outages_seed`` : Integer value used to seed the RNG
+ used to compute stochastic outage losses.
+ - ``time_index_step`` : Integer representing the step size
+ used to sample the ``time_index`` in the resource data.
+ This can be used to reduce temporal resolution (i.e. for
+ 30 minute input data, ``time_index_step=1`` yields the
+ full 30 minute time series as output, while
+ ``time_index_step=2`` yields hourly output, and so forth).
+
+ .. Note:: The reduced data shape (i.e. after applying a
+ step size of `time_index_step`) must still be
+ an integer multiple of 8760, or the execution
+ will fail.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ meta : pd.DataFrame | pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ output_request : list
+ Requested SAM outputs (e.g., 'cf_mean', 'annual_energy',
+ 'cf_profile', 'gen_profile', 'energy_yield', 'ppa_price',
+ 'lcoe_fcr').
+ drop_leap : bool
+ Drops February 29th from the resource data. If False, December
+ 31st is dropped from leap years.
+ """
+ super().__init__(*args,**kwargs)
+ self.add_power_curve_losses()
+
+
+
[docs]classWindPower(AbstractSamWind):
+"""Class for Wind generation from SAM"""
+
+ MODULE="windpower"
+ PYSAM=PySamWindPower
+
+
[docs]defset_resource_data(self,resource,meta):
+"""Set WTK resource data arrays.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries solar or wind resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation. Remapping will be
+ done to convert typical NSRDB/WTK names into SAM names (e.g. DNI ->
+ dn and wind_speed -> windspeed)
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ """
+
+ meta=self._parse_meta(meta)
+
+ # map resource data names to SAM required data names
+ var_map={
+ "speed":"windspeed",
+ "direction":"winddirection",
+ "airtemperature":"temperature",
+ "temp":"temperature",
+ "surfacepressure":"pressure",
+ "relativehumidity":"rh",
+ "humidity":"rh",
+ }
+ lower_case={
+ k:k.lower().replace(" ","").replace("_","")
+ forkinresource.columns
+ }
+ resource=resource.rename(mapper=lower_case,axis="columns")
+ resource=resource.rename(mapper=var_map,axis="columns")
+
+ data_dict={}
+ var_list=["temperature","pressure","windspeed","winddirection"]
+ if"winddirection"notinresource:
+ resource["winddirection"]=0.0
+
+ time_index=resource.index
+ self.time_interval=self.get_time_interval(resource.index.values)
+
+ data_dict["fields"]=[1,2,3,4]
+ data_dict["heights"]=4*[self.sam_sys_inputs["wind_turbine_hub_ht"]]
+
+ if"rh"inresource:
+ # set relative humidity for icing.
+ rh=self.ensure_res_len(resource["rh"].values,time_index)
+ n_roll=int(meta[ResourceMetaField.TIMEZONE]*self.time_interval)
+ rh=np.roll(rh,n_roll,axis=0)
+ data_dict["rh"]=rh.tolist()
+
+ # must be set as matrix in [temperature, pres, speed, direction] order
+ # ensure that resource array length is multiple of 8760
+ # roll the truncated resource array to local timezone
+ temp=self.ensure_res_len(resource[var_list].values,time_index)
+ n_roll=int(meta[ResourceMetaField.TIMEZONE]*self.time_interval)
+ temp=np.roll(temp,n_roll,axis=0)
+ data_dict["data"]=temp.tolist()
+
+ data_dict["lat"]=float(meta[ResourceMetaField.LATITUDE])
+ data_dict["lon"]=float(meta[ResourceMetaField.LONGITUDE])
+ data_dict["tz"]=int(meta[ResourceMetaField.TIMEZONE])
+ data_dict["elev"]=float(meta[ResourceMetaField.ELEVATION])
+
+ time_index=self.ensure_res_len(time_index,time_index)
+ data_dict["minute"]=time_index.minute.tolist()
+ data_dict["hour"]=time_index.hour.tolist()
+ data_dict["year"]=time_index.year.tolist()
+ data_dict["month"]=time_index.month.tolist()
+ data_dict["day"]=time_index.day.tolist()
+
+ # add resource data to self.data and clear
+ self["wind_resource_data"]=data_dict
+ self["wind_resource_model_choice"]=0
[docs]classWindPowerPD(AbstractSamGeneration,PowerCurveLossesMixin):
+"""WindPower analysis with wind speed/direction joint probabilty
+ distrubtion input"""
+
+ MODULE="windpower"
+ PYSAM=PySamWindPower
+
+ def__init__(
+ self,
+ ws_edges,
+ wd_edges,
+ wind_dist,
+ meta,
+ sam_sys_inputs,
+ site_sys_inputs=None,
+ output_request=None,
+ ):
+"""Initialize a SAM generation object for windpower with a
+ speed/direction joint probability distribution.
+
+ Parameters
+ ----------
+ ws_edges : np.ndarray
+ 1D array of windspeed (m/s) values that set the bin edges for the
+ wind probability distribution. Same len as wind_dist.shape[0] + 1
+ wd_edges : np.ndarray
+ 1D array of winddirections (deg) values that set the bin edges
+ for the wind probability dist. Same len as wind_dist.shape[1] + 1
+ wind_dist : np.ndarray
+ 2D array probability distribution of (windspeed, winddirection).
+ meta : pd.DataFrame | pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ sam_sys_inputs : dict
+ Site-agnostic SAM system model inputs arguments.
+ site_sys_inputs : dict
+ Optional set of site-specific SAM system inputs to complement the
+ site-agnostic inputs.
+ output_request : list
+ Requested SAM outputs (e.g., 'cf_mean', 'annual_energy',
+ 'cf_profile', 'gen_profile', 'energy_yield', 'ppa_price',
+ 'lcoe_fcr').
+ """
+
+ # make sure timezone and elevation are in the meta data
+ meta=self.tz_elev_check(sam_sys_inputs,site_sys_inputs,meta)
+
+ # don't pass resource to base class,
+ # set in concrete generation classes instead
+ super().__init__(
+ None,
+ meta,
+ sam_sys_inputs,
+ site_sys_inputs=site_sys_inputs,
+ output_request=output_request,
+ drop_leap=False,
+ )
+
+ # Set the site number using meta data
+ ifhasattr(meta,"name"):
+ self._site=meta.name
+ else:
+ self._site=None
+
+ self.set_resource_data(ws_edges,wd_edges,wind_dist)
+ self.add_power_curve_losses()
+
+
[docs]defset_resource_data(self,ws_edges,wd_edges,wind_dist):
+"""Send wind PD to pysam
+
+ Parameters
+ ----------
+ ws_edges : np.ndarray
+ 1D array of windspeed (m/s) values that set the bin edges for the
+ wind probability distribution. Same len as wind_dist.shape[0] + 1
+ wd_edges : np.ndarray
+ 1D array of winddirections (deg) values that set the bin edges
+ for the wind probability dist. Same len as wind_dist.shape[1] + 1
+ wind_dist : np.ndarray
+ 2D array probability distribution of (windspeed, winddirection).
+ """
+
+ assertlen(ws_edges)==wind_dist.shape[0]+1
+ assertlen(wd_edges)==wind_dist.shape[1]+1
+
+ wind_dist/=wind_dist.sum()
+
+ # SAM wants the midpoints of the sample bins
+ ws_points=ws_edges[:-1]+np.diff(ws_edges)/2
+ wd_points=wd_edges[:-1]+np.diff(wd_edges)/2
+
+ wd_points,ws_points=np.meshgrid(wd_points,ws_points)
+ vstack=(
+ ws_points.flatten(),
+ wd_points.flatten(),
+ wind_dist.flatten(),
+ )
+ wrd=np.vstack(vstack).T.tolist()
+
+ self["wind_resource_model_choice"]=2
+ self["wind_resource_distribution"]=wrd
+
+
+
[docs]classMhkWave(AbstractSamGeneration):
+"""Class for Wave generation from SAM"""
+
+ MODULE="mhkwave"
+ PYSAM=PySamMhkWave
+
+
[docs]defset_resource_data(self,resource,meta):
+"""Set Hindcast US Wave resource data arrays.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame
+ Timeseries resource data for a single location with a
+ pandas DatetimeIndex. There must be columns for all the required
+ variables to run the respective SAM simulation.
+ meta : pd.Series
+ Meta data corresponding to the resource input for the single
+ location. Should include values for latitude, longitude, elevation,
+ and timezone.
+ """
+
+ meta=self._parse_meta(meta)
+
+ # map resource data names to SAM required data names
+ var_map={
+ "significantwaveheight":"significant_wave_height",
+ "waveheight":"significant_wave_height",
+ "height":"significant_wave_height",
+ "swh":"significant_wave_height",
+ "energyperiod":"energy_period",
+ "waveperiod":"energy_period",
+ "period":"energy_period",
+ "ep":"energy_period",
+ }
+ lower_case={
+ k:k.lower().replace(" ","").replace("_","")
+ forkinresource.columns
+ }
+ resource=resource.rename(mapper=lower_case,axis="columns")
+ resource=resource.rename(mapper=var_map,axis="columns")
+
+ data_dict={}
+
+ time_index=resource.index
+ self.time_interval=self.get_time_interval(resource.index.values)
+
+ # must be set as matrix in [temperature, pres, speed, direction] order
+ # ensure that resource array length is multiple of 8760
+ # roll the truncated resource array to local timezone
+ forvarin["significant_wave_height","energy_period"]:
+ arr=self.ensure_res_len(resource[var].values,time_index)
+ n_roll=int(meta[ResourceMetaField.TIMEZONE]*self.time_interval)
+ data_dict[var]=np.roll(arr,n_roll,axis=0).tolist()
+
+ data_dict["lat"]=meta[ResourceMetaField.LATITUDE]
+ data_dict["lon"]=meta[ResourceMetaField.LONGITUDE]
+ data_dict["tz"]=meta[ResourceMetaField.TIMEZONE]
+
+ time_index=self.ensure_res_len(time_index,time_index)
+ data_dict["minute"]=time_index.minute
+ data_dict["hour"]=time_index.hour
+ data_dict["year"]=time_index.year
+ data_dict["month"]=time_index.month
+ data_dict["day"]=time_index.day
+
+ # add resource data to self.data and clear
+ self["wave_resource_data"]=data_dict
[docs]classWindBos:
+"""Wind Balance of System Cost Model."""
+
+ MODULE="windbos"
+
+ # keys for the windbos input data dictionary.
+ # Some keys may not be found explicitly in the SAM input.
+ KEYS=(
+ "tech_model",
+ "financial_model",
+ "machine_rating",
+ "rotor_diameter",
+ "hub_height",
+ "number_of_turbines",
+ "interconnect_voltage",
+ "distance_to_interconnect",
+ "site_terrain",
+ "turbine_layout",
+ "soil_condition",
+ "construction_time",
+ "om_building_size",
+ "quantity_test_met_towers",
+ "quantity_permanent_met_towers",
+ "weather_delay_days",
+ "crane_breakdowns",
+ "access_road_entrances",
+ "turbine_capital_cost",
+ "turbine_cost_per_kw",
+ "tower_top_mass",
+ "delivery_assist_required",
+ "pad_mount_transformer_required",
+ "new_switchyard_required",
+ "rock_trenching_required",
+ "mv_thermal_backfill",
+ "mv_overhead_collector",
+ "performance_bond",
+ "contingency",
+ "warranty_management",
+ "sales_and_use_tax",
+ "overhead",
+ "profit_margin",
+ "development_fee",
+ "turbine_transportation",
+ )
+
+ def__init__(self,inputs):
+"""
+ Parameters
+ ----------
+ inputs : dict
+ SAM key value pair inputs.
+ """
+
+ self._turbine_capital_cost=0.0
+ self._datadict={}
+
+ self._inputs=inputs
+ self._special={
+ "tech_model":"windbos",
+ "financial_model":"none",
+ "machine_rating":self.machine_rating,
+ "hub_height":self.hub_height,
+ "rotor_diameter":self.rotor_diameter,
+ "number_of_turbines":self.number_of_turbines,
+ "turbine_capital_cost":self.turbine_capital_cost,
+ }
+ self._parse_inputs()
+ self._out=ssc_sim_from_dict(self._datadict)
+
+ def_parse_inputs(self):
+"""Parse SAM inputs into a windbos input dict and perform any
+ required special operations."""
+
+ forkinself.KEYS:
+ ifkinself._special:
+ self._datadict[k]=self._special[k]
+ elifknotinself._inputs:
+ raiseSAMInputError(
+ 'Windbos requires input key: "{}"'.format(k)
+ )
+ else:
+ self._datadict[k]=self._inputs[k]
+
+ @property
+ defmachine_rating(self):
+"""Single turbine machine rating either from input or power curve."""
+ if"machine_rating"inself._inputs:
+ returnself._inputs["machine_rating"]
+ else:
+ returnnp.max(self._inputs["wind_turbine_powercurve_powerout"])
+
+ @property
+ defhub_height(self):
+"""Turbine hub height."""
+ if"wind_turbine_hub_ht"inself._inputs:
+ returnself._inputs["wind_turbine_hub_ht"]
+ else:
+ returnself._inputs["hub_height"]
+
+ @property
+ defrotor_diameter(self):
+"""Turbine rotor diameter."""
+ if"wind_turbine_rotor_diameter"inself._inputs:
+ returnself._inputs["wind_turbine_rotor_diameter"]
+ else:
+ returnself._inputs["rotor_diameter"]
+
+ @property
+ defnumber_of_turbines(self):
+"""Number of turbines either based on input or system (farm) capacity
+ and machine rating"""
+
+ if"number_of_turbines"inself._inputs:
+ returnself._inputs["number_of_turbines"]
+ else:
+ return(
+ self._inputs['system_capacity']/self.machine_rating
+ )
+
+ @property
+ defturbine_capital_cost(self):
+"""Returns zero (no turbine capital cost for WindBOS input,
+ and assigns any input turbine_capital_cost to an attr"""
+
+ if"turbine_capital_cost"inself._inputs:
+ self._turbine_capital_cost=self._inputs["turbine_capital_cost"]
+ else:
+ self._turbine_capital_cost=0.0
+ return0.0
+
+ @property
+ defbos_cost(self):
+"""Get the balance of system cost ($)."""
+ returnself._out["project_total_budgeted_cost"]
+
+ @property
+ defturbine_cost(self):
+"""Get the turbine cost ($)."""
+ tcost=(
+ self._inputs["turbine_cost_per_kw"]
+ *self.machine_rating
+ *self.number_of_turbines
+ )+(self._turbine_capital_cost*self.number_of_turbines)
+ returntcost
+
+ @property
+ defsales_tax_mult(self):
+"""Get a sales tax multiplier (frac of the total installed cost)."""
+ basis=self._inputs.get("sales_tax_basis",0)/100
+ tax=self._datadict.get("sales_and_use_tax",0)/100
+ returnbasis*tax
+
+ @property
+ defsales_tax_cost(self):
+"""Get the cost of sales tax ($)."""
+ return(self.bos_cost+self.turbine_cost)*self.sales_tax_mult
+
+ @property
+ deftotal_installed_cost(self):
+"""Get the total installed cost ($) (bos + turbine)."""
+ returnself.bos_cost+self.turbine_cost+self.sales_tax_cost
+
+ @property
+ defoutput(self):
+"""Get a dictionary containing the cost breakdown."""
+ output={
+ "total_installed_cost":self.total_installed_cost,
+ "turbine_cost":self.turbine_cost,
+ "sales_tax_cost":self.sales_tax_cost,
+ "bos_cost":self.bos_cost,
+ }
+ returnoutput
+
+ # pylint: disable-msg=W0613
+
[docs]@classmethod
+ defreV_run(
+ cls,
+ points_control,
+ site_df,
+ output_request=("total_installed_cost",),
+ **kwargs,
+ ):
+"""Execute SAM SingleOwner simulations based on reV points control.
+
+ Parameters
+ ----------
+ points_control : config.PointsControl
+ PointsControl instance containing project points site and SAM
+ config info.
+ site_df : pd.DataFrame
+ Dataframe of site-specific input variables. Row index corresponds
+ to site number/gid (via df.loc not df.iloc), column labels are the
+ variable keys that will be passed forward as SAM parameters.
+ output_request : list | tuple | str
+ Output(s) to retrieve from SAM.
+ kwargs : dict
+ Not used but maintained for polymorphic calls with other
+ SAM econ reV_run() methods (lcoe and single owner).
+ Breaks pylint error W0613: unused argument.
+
+ Returns
+ -------
+ out : dict
+ Nested dictionaries where the top level key is the site index,
+ the second level key is the variable name, second level value is
+ the output variable value.
+ """
+ out={}
+
+ forsiteinpoints_control.sites:
+ # get SAM inputs from project_points based on the current site
+ _,inputs=points_control.project_points[site]
+
+ # ensure that site-specific data is not persisted to other sites
+ site_inputs=deepcopy(inputs)
+
+ site_inputs.update(dict(site_df.loc[site,:]))
+
+ wb=cls(site_inputs)
+
+ out[site]={
+ k:vfork,vinwb.output.items()ifkinoutput_request
+ }
+
+ returnout
[docs]classBespokeMultiPlantData:
+"""Multi-plant preloaded data.
+
+ This object is intended to facilitate the use of pre-loaded data for
+ running :class:`BespokeWindPlants` on systems with slow parallel
+ reads to a single HDF5 file.
+ """
+
+ def__init__(self,res_fpath,sc_gid_to_hh,sc_gid_to_res_gid,
+ pre_load_humidity=False):
+"""Initialize BespokeMultiPlantData
+
+ Parameters
+ ----------
+ res_fpath : str | list
+ Unix shell style path (potentially containing wildcard (*)
+ patterns) to a single or multi-file resource file set(s).
+ Can also be an explicit list of resource file paths, which
+ themselves can contain wildcards. This input must be
+ readable by
+ :py:class:`rex.multi_year_resource.MultiYearWindResource`.
+ sc_gid_to_hh : dict
+ Dictionary mapping SC GID values to hub-heights. Data for
+ each SC GID will be pulled for the corresponding hub-height
+ given in this dictionary.
+ sc_gid_to_res_gid : dict
+ Dictionary mapping SC GID values to an iterable oif resource
+ GID values. Resource GID values should correspond to GID
+ values in the HDF5 file, so any GID map must be applied
+ before initializing :class`BespokeMultiPlantData`.
+ pre_load_humidity : optional, default=False
+ Option to pre-load relative humidity data (useful for icing
+ runs). If ``False``, relative humidities are not loaded.
+ """
+ self.res_fpath=res_fpath
+ self.sc_gid_to_hh=sc_gid_to_hh
+ self.sc_gid_to_res_gid=sc_gid_to_res_gid
+ self.hh_to_res_gids={}
+ self._wind_dirs=None
+ self._wind_speeds=None
+ self._temps=None
+ self._pressures=None
+ self._relative_humidities=None
+ self._pre_load_humidity=pre_load_humidity
+ self._time_index=None
+ self._pre_load_data()
+
+ def_pre_load_data(self):
+"""Pre-load the resource data."""
+
+ forsc_gid,gidsinself.sc_gid_to_res_gid.items():
+ hh=self.sc_gid_to_hh[sc_gid]
+ self.hh_to_res_gids.setdefault(hh,set()).update(gids)
+
+ self.hh_to_res_gids={
+ hh:sorted(gids)forhh,gidsinself.hh_to_res_gids.items()
+ }
+
+ start_time=time.time()
+ withMultiYearWindResource(self.res_fpath)asres:
+ self._wind_dirs={
+ hh:res[f"winddirection_{hh}m",:,gids]
+ forhh,gidsinself.hh_to_res_gids.items()
+ }
+ self._wind_speeds={
+ hh:res[f"windspeed_{hh}m",:,gids]
+ forhh,gidsinself.hh_to_res_gids.items()
+ }
+ self._temps={
+ hh:res[f"temperature_{hh}m",:,gids]
+ forhh,gidsinself.hh_to_res_gids.items()
+ }
+ self._pressures={
+ hh:res[f"pressure_{hh}m",:,gids]
+ forhh,gidsinself.hh_to_res_gids.items()
+ }
+ self._time_index=res.time_index
+ ifself._pre_load_humidity:
+ self._relative_humidities={
+ hh:res["relativehumidity_2m",:,gids]
+ forhh,gidsinself.hh_to_res_gids.items()
+ }
+
+ logger.debug(
+ f"Data took {(time.time()-start_time)/60:.2f} "f"min to load"
+ )
+
+
[docs]defget_preloaded_data_for_gid(self,sc_gid):
+"""Get the pre-loaded data for a single SC GID.
+
+ Parameters
+ ----------
+ sc_gid : int
+ SC GID to load resource data for.
+
+ Returns
+ -------
+ BespokeSinglePlantData
+ A loaded ``BespokeSinglePlantData`` object that can act as
+ an HDF5 handler stand-in *for this SC GID only*.
+ """
+ hh=self.sc_gid_to_hh[sc_gid]
+ sc_point_res_gids=sorted(self.sc_gid_to_res_gid[sc_gid])
+ data_inds=np.searchsorted(self.hh_to_res_gids[hh],sc_point_res_gids)
+
+ rh=(Noneifnotself._pre_load_humidity
+ elseself._relative_humidities[hh][:,data_inds])
+ returnBespokeSinglePlantData(
+ sc_point_res_gids,
+ self._wind_dirs[hh][:,data_inds],
+ self._wind_speeds[hh][:,data_inds],
+ self._temps[hh][:,data_inds],
+ self._pressures[hh][:,data_inds],
+ self._time_index,
+ rh,
+ )
+
+
+
[docs]classBespokeSinglePlantData:
+"""Single-plant preloaded data.
+
+ This object is intended to facilitate the use of pre-loaded data for
+ running :class:`BespokeSinglePlant` on systems with slow parallel
+ reads to a single HDF5 file.
+ """
+
+ def__init__(
+ self,data_inds,wind_dirs,wind_speeds,temps,pressures,time_index,
+ relative_humidities=None,
+ ):
+"""Initialize BespokeSinglePlantData
+
+ Parameters
+ ----------
+ data_inds : 1D np.array
+ Array of res GIDs. This array should be the same length as
+ the second dimension of `wind_dirs`, `wind_speeds`, `temps`,
+ and `pressures`. The GID value of data_inds[0] should
+ correspond to the `wind_dirs[:, 0]` data, etc.
+ wind_dirs : 2D np.array
+ Array of wind directions. Dimensions should be correspond to
+ [time, location]. See documentation for `data_inds` for
+ required spatial mapping of GID values.
+ wind_speeds : 2D np.array
+ Array of wind speeds. Dimensions should be correspond to
+ [time, location]. See documentation for `data_inds` for
+ required spatial mapping of GID values.
+ temps : 2D np.array
+ Array oftemperatures. Dimensions should be correspond to
+ [time, location]. See documentation for `data_inds` for
+ required spatial mapping of GID values.
+ pressures : 2D np.array
+ Array of pressures. Dimensions should be correspond to
+ pressures, respectively. Dimensions should be correspond to
+ [time, location]. See documentation for `data_inds` for
+ required spatial mapping of GID values.
+ time_index : 1D np.array
+ Time index array corresponding to the temporal dimension of
+ the 2D data. Will be exposed directly to user.
+ relative_humidities : 2D np.array, optional
+ Array of relative humidities. Dimensions should be
+ correspond to [time, location]. See documentation for
+ `data_inds` for required spatial mapping of GID values.
+ If ``None``, relative_humidities cannot be queried.
+ """
+
+ self.data_inds=data_inds
+ self.wind_dirs=wind_dirs
+ self.wind_speeds=wind_speeds
+ self.temps=temps
+ self.pressures=pressures
+ self.time_index=time_index
+ self.relative_humidities=relative_humidities
+ self._humidities_exist=relative_humiditiesisnotNone
+
+ def__getitem__(self,key):
+ dset_name,t_idx,gids=key
+ data_inds=np.searchsorted(self.data_inds,gids)
+ if"winddirection"indset_name:
+ returnself.wind_dirs[t_idx,data_inds]
+ if"windspeed"indset_name:
+ returnself.wind_speeds[t_idx,data_inds]
+ if"temperature"indset_name:
+ returnself.temps[t_idx,data_inds]
+ if"pressure"indset_name:
+ returnself.pressures[t_idx,data_inds]
+ ifself._humidities_existand"relativehumidity"indset_name:
+ returnself.relative_humidities[t_idx,data_inds]
+ msg=f"Unknown dataset name: {dset_name!r}"
+ logger.error(msg)
+ raiseValueError(msg)
+
+
+
[docs]classBespokeSinglePlant:
+"""Framework for analyzing and optimizing a wind plant layout specific to
+ the local wind resource and exclusions for a single reV supply curve point.
+ """
+
+ DEPENDENCIES=("shapely",)
+ OUT_ATTRS=copy.deepcopy(Gen.OUT_ATTRS)
+
+ def__init__(
+ self,
+ gid,
+ excl,
+ res,
+ tm_dset,
+ sam_sys_inputs,
+ objective_function,
+ capital_cost_function,
+ fixed_operating_cost_function,
+ variable_operating_cost_function,
+ balance_of_system_cost_function,
+ min_spacing="5x",
+ wake_loss_multiplier=1,
+ ga_kwargs=None,
+ output_request=("system_capacity","cf_mean"),
+ ws_bins=(0.0,20.0,5.0),
+ wd_bins=(0.0,360.0,45.0),
+ excl_dict=None,
+ inclusion_mask=None,
+ data_layers=None,
+ resolution=64,
+ excl_area=None,
+ exclusion_shape=None,
+ eos_mult_baseline_cap_mw=200,
+ prior_meta=None,
+ gid_map=None,
+ bias_correct=None,
+ pre_loaded_data=None,
+ close=True,
+ ):
+"""
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | ExclusionMask
+ Filepath to exclusions h5 or ExclusionMask file handler.
+ res : str | Resource
+ Filepath to .h5 wind resource file or pre-initialized Resource
+ handler
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ sam_sys_inputs : dict
+ SAM windpower compute module system inputs not including the
+ wind resource data.
+ objective_function : str
+ The objective function of the optimization as a string, should
+ return the objective to be minimized during layout optimization.
+ Variables available are:
+
+ - ``n_turbines``: the number of turbines
+ - ``system_capacity``: wind plant capacity
+ - ``aep``: annual energy production
+ - ``avg_sl_dist_to_center_m``: Average straight-line
+ distance to the supply curve point center from all
+ turbine locations (in m). Useful for computing plant
+ BOS costs.
+ - ``avg_sl_dist_to_medoid_m``: Average straight-line
+ distance to the medoid of all turbine locations
+ (in m). Useful for computing plant BOS costs.
+ - ``nn_conn_dist_m``: Total BOS connection distance
+ using nearest-neighbor connections. This variable is
+ only available for the
+ ``balance_of_system_cost_function`` equation.
+ - ``fixed_charge_rate``: user input fixed_charge_rate if
+ included as part of the sam system config.
+ - ``capital_cost``: plant capital cost as evaluated
+ by `capital_cost_function`
+ - ``fixed_operating_cost``: plant fixed annual operating
+ cost as evaluated by `fixed_operating_cost_function`
+ - ``variable_operating_cost``: plant variable annual
+ operating cost as evaluated by
+ `variable_operating_cost_function`
+ - ``balance_of_system_cost``: plant balance of system
+ cost as evaluated by `balance_of_system_cost_function`
+ - ``self.wind_plant``: the SAM wind plant object,
+ through which all SAM variables can be accessed
+
+ capital_cost_function : str
+ The plant capital cost function as a string, must return the total
+ capital cost in $. Has access to the same variables as the
+ objective_function.
+ fixed_operating_cost_function : str
+ The plant annual fixed operating cost function as a string, must
+ return the fixed operating cost in $/year. Has access to the same
+ variables as the objective_function.
+ variable_operating_cost_function : str
+ The plant annual variable operating cost function as a string, must
+ return the variable operating cost in $/kWh. Has access to the same
+ variables as the objective_function. You can set this to "0"
+ to effectively ignore variable operating costs.
+ balance_of_system_cost_function : str
+ The plant balance-of-system cost function as a string, must
+ return the variable operating cost in $. Has access to the
+ same variables as the objective_function. You can set this
+ to "0" to effectively ignore balance-of-system costs.
+ balance_of_system_cost_function : str
+ The plant balance-of-system cost function as a string, must
+ return the variable operating cost in $. Has access to the same
+ variables as the objective_function.
+ min_spacing : float | int | str
+ Minimum spacing between turbines in meters. Can also be a string
+ like "5x" (default) which is interpreted as 5 times the turbine
+ rotor diameter.
+ wake_loss_multiplier : float, optional
+ A multiplier used to scale the annual energy lost due to
+ wake losses.
+ .. WARNING:: This multiplier will ONLY be applied during the
+ optimization process and will NOT be come through in output
+ values such as the hourly profiles,
+ aep, any of the cost functions, or even the output objective.
+ ga_kwargs : dict | None
+ Dictionary of keyword arguments to pass to GA initialization.
+ If `None`, default initialization values are used.
+ See :class:`~reV.bespoke.gradient_free.GeneticAlgorithm` for
+ a description of the allowed keyword arguments.
+ output_request : list | tuple
+ Outputs requested from the SAM windpower simulation after the
+ bespoke plant layout optimization. Can also request resource means
+ like ws_mean, windspeed_mean, temperature_mean, pressure_mean.
+ ws_bins : tuple
+ 3-entry tuple with (start, stop, step) for the windspeed binning of
+ the wind joint probability distribution. The stop value is
+ inclusive, so ws_bins=(0, 20, 5) would result in four bins with bin
+ edges (0, 5, 10, 15, 20).
+ wd_bins : tuple
+ 3-entry tuple with (start, stop, step) for the winddirection
+ binning of the wind joint probability distribution. The stop value
+ is inclusive, so ws_bins=(0, 360, 90) would result in four bins
+ with bin edges (0, 90, 180, 270, 360).
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ inclusion_mask : np.ndarray
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. The shape of this will be checked against the input
+ resolution.
+ data_layers : None | dict
+ Aggregation data layers. Must be a dictionary keyed by data label
+ name. Each value must be another dictionary with "dset", "method",
+ and "fpath".
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols). Inputing this
+ will speed things up considerably.
+ eos_mult_baseline_cap_mw : int | float, optional
+ Baseline plant capacity (MW) used to calculate economies of
+ scale (EOS) multiplier from the `capital_cost_function`. EOS
+ multiplier is calculated as the $-per-kW of the wind plant
+ divided by the $-per-kW of a plant with this baseline
+ capacity. By default, `200` (MW), which aligns the baseline
+ with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
+ prior_meta : pd.DataFrame | None
+ Optional meta dataframe belonging to a prior run. This will only
+ run the timeseries power generation step and assume that all of the
+ wind plant layouts are fixed given the prior run. The meta data
+ needs columns "capacity", "turbine_x_coords", and
+ "turbine_y_coords".
+ gid_map : None | str | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This can be None, a pre-extracted dict, or
+ a filepath to json or csv. If this is a csv, it must have the
+ columns "gid" (which matches the techmap) and "gid_map" (gids to
+ extract from the resource input). This is useful if you're running
+ forecasted resource data (e.g., ECMWF) to complement historical
+ meteorology (e.g., WTK).
+ bias_correct : str | pd.DataFrame, optional
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+ pre_loaded_data : BespokeSinglePlantData, optional
+ A pre-loaded :class:`BespokeSinglePlantData` object, or
+ ``None``. Can be useful to speed up execution on file
+ systems with slow parallel reads.
+ close : bool
+ Flag to close object file handlers on exit.
+ """
+
+ logger.debug(
+ "Initializing BespokeSinglePlant for gid {}...".format(gid)
+ )
+ logger.debug("Resource filepath: {}".format(res))
+ logger.debug("Exclusion filepath: {}".format(excl))
+ logger.debug("Exclusion dict: {}".format(excl_dict))
+ logger.debug(
+ "Bespoke objective function: {}".format(objective_function)
+ )
+ logger.debug("Bespoke cost function: {}".format(objective_function))
+ logger.debug(
+ "Bespoke wake loss multiplier: {}".format(wake_loss_multiplier)
+ )
+ logger.debug("Bespoke GA initialization kwargs: {}".format(ga_kwargs))
+ logger.debug(
+ "Bespoke EOS multiplier baseline capacity: {:,} MW".format(
+ eos_mult_baseline_cap_mw
+ )
+ )
+
+ ifisinstance(min_spacing,str)andmin_spacing.endswith("x"):
+ rotor_diameter=sam_sys_inputs["wind_turbine_rotor_diameter"]
+ min_spacing=float(min_spacing.strip("x"))*rotor_diameter
+
+ ifnotisinstance(min_spacing,(int,float)):
+ try:
+ min_spacing=float(min_spacing)
+ exceptExceptionase:
+ msg=(
+ "min_spacing must be numeric but received: {}, {}".format(
+ min_spacing,type(min_spacing)
+ )
+ )
+ logger.error(msg)
+ raiseTypeError(msg)frome
+
+ self.objective_function=objective_function
+ self.capital_cost_function=capital_cost_function
+ self.fixed_operating_cost_function=fixed_operating_cost_function
+ self.variable_operating_cost_function=(
+ variable_operating_cost_function
+ )
+ self.balance_of_system_cost_function=balance_of_system_cost_function
+ self.min_spacing=min_spacing
+ self.wake_loss_multiplier=wake_loss_multiplier
+ self.ga_kwargs=ga_kwargsor{}
+
+ self._sam_sys_inputs=sam_sys_inputs
+ self._out_req=list(output_request)
+ self._ws_bins=ws_bins
+ self._wd_bins=wd_bins
+ self._baseline_cap_mw=eos_mult_baseline_cap_mw
+
+ self._res_df=None
+ self._prior_meta=prior_metaisnotNone
+ self._meta=prior_meta
+ self._wind_dist=None
+ self._ws_edges=None
+ self._wd_edges=None
+ self._wind_plant_pd=None
+ self._wind_plant_ts=None
+ self._plant_optm=None
+ self._gid_map=self._parse_gid_map(gid_map)
+ self._bias_correct=Gen._parse_bc(bias_correct)
+ self._pre_loaded_data=pre_loaded_data
+ self._outputs={}
+
+ res=resifnotisinstance(res,str)elseMultiYearWindResource(res)
+
+ self._sc_point=AggSCPoint(
+ gid,
+ excl,
+ res,
+ tm_dset,
+ excl_dict=excl_dict,
+ inclusion_mask=inclusion_mask,
+ resolution=resolution,
+ excl_area=excl_area,
+ exclusion_shape=exclusion_shape,
+ close=close,
+ )
+
+ self._parse_output_req()
+ self._data_layers=data_layers
+ self._parse_prior_run()
+
+ def__str__(self):
+ s="BespokeSinglePlant for reV SC gid {} with resolution {}".format(
+ self.sc_point.gid,self.sc_point.resolution
+ )
+ returns
+
+ def__repr__(self):
+ s="BespokeSinglePlant for reV SC gid {} with resolution {}".format(
+ self.sc_point.gid,self.sc_point.resolution
+ )
+ returns
+
+ def__enter__(self):
+ returnself
+
+ def__exit__(self,type,value,traceback):
+ self.close()
+ iftypeisnotNone:
+ raise
+
+ def_parse_output_req(self):
+"""Make sure that the output request has basic important parameters
+ (cf_mean, annual_energy) and process mean wind resource datasets
+ (ws_mean, *_mean) if requested.
+ """
+
+ required=("cf_mean","annual_energy")
+ forreqinrequired:
+ ifreqnotinself._out_req:
+ self._out_req.append(req)
+
+ if"ws_mean"inself._out_req:
+ self._out_req.remove("ws_mean")
+ self._outputs["ws_mean"]=self.res_df["windspeed"].mean()
+
+ forreqincopy.deepcopy(self._out_req):
+ ifreqinself.res_df:
+ self._out_req.remove(req)
+ forannual_tiinself.annual_time_indexes:
+ year=annual_ti.year[0]
+ mask=self.res_df.index.isin(annual_ti)
+ arr=self.res_df.loc[mask,req].values.flatten()
+ self._outputs[req+f"-{year}"]=arr
+
+ elifreq.replace("_mean","")inself.res_df:
+ self._out_req.remove(req)
+ dset=req.replace("_mean","")
+ self._outputs[req]=self.res_df[dset].mean()
+
+ if"lcoe_fcr"inself._out_reqand(
+ "fixed_charge_rate"notinself.original_sam_sys_inputs
+ ):
+ msg=(
+ 'User requested "lcoe_fcr" but did not input '
+ '"fixed_charge_rate" in the SAM system config.'
+ )
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ def_parse_prior_run(self):
+"""Parse prior bespoke wind plant optimization run meta data and make
+ sure the SAM system inputs are set accordingly."""
+
+ # {meta_column: sam_sys_input_key}
+ required={
+ SupplyCurveField.CAPACITY_AC_MW:"system_capacity",
+ SupplyCurveField.TURBINE_X_COORDS:"wind_farm_xCoordinates",
+ SupplyCurveField.TURBINE_Y_COORDS:"wind_farm_yCoordinates",
+ }
+
+ ifself._prior_meta:
+ missing=[kforkinrequiredifknotinself.meta]
+ msg=(
+ "Prior bespoke run meta data is missing the following "
+ "required columns: {}".format(missing)
+ )
+ assertnotany(missing),msg
+
+ formeta_col,sam_sys_keyinrequired.items():
+ prior_value=self.meta[meta_col].values[0]
+ self._sam_sys_inputs[sam_sys_key]=prior_value
+
+ # convert reV supply curve cap in MW to SAM capacity in kW
+ self._sam_sys_inputs["system_capacity"]*=1e3
+
+ @staticmethod
+ def_parse_gid_map(gid_map):
+"""Parse the gid map and return the extracted dictionary or None if not
+ provided
+
+ Parameters
+ ----------
+ gid_map : None | str | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This can be None, a pre-extracted dict, or
+ a filepath to json or csv. If this is a csv, it must have the
+ columns "gid" (which matches the techmap) and "gid_map" (gids to
+ extract from the resource input). This is useful if you're running
+ forecasted resource data (e.g., ECMWF) to complement historical
+ meteorology (e.g., WTK).
+
+ Returns
+ -------
+ gid_map : dict | None
+ Pre-extracted gid_map dictionary if provided or None if not.
+ """
+
+ ifisinstance(gid_map,str):
+ ifgid_map.endswith(".csv"):
+ gid_map=(
+ pd.read_csv(gid_map)
+ .rename(SupplyCurveField.map_to(ResourceMetaField),axis=1)
+ .to_dict()
+ )
+ err_msg=f"Need {ResourceMetaField.GID} in gid_map column"
+ assertResourceMetaField.GIDingid_map,err_msg
+ assert"gid_map"ingid_map,'Need "gid_map" in gid_map column'
+ gid_map={
+ gid_map[ResourceMetaField.GID][i]:gid_map["gid_map"][i]
+ foriingid_map[ResourceMetaField.GID]
+ }
+
+ elifgid_map.endswith(".json"):
+ withopen(gid_map)asf:
+ gid_map=json.load(f)
+
+ returngid_map
+
+
[docs]defclose(self):
+"""Close any open file handlers via the sc point attribute. If this
+ class was initialized with close=False, this will not close any
+ handlers."""
+ self.sc_point.close()
+
+
[docs]defbias_correct_ws(self,ws,dset,h5_gids):
+"""Bias correct windspeed data if the ``bias_correct`` input was
+ provided.
+
+ Parameters
+ ----------
+ ws : np.ndarray
+ Windspeed data in shape (time, space)
+ dset : str
+ Resource dataset name e.g., "windspeed_100m", "temperature_100m",
+ "pressure_100m", or something similar
+ h5_gids : list | np.ndarray
+ Array of integer gids (spatial indices) from the source h5 file.
+ This is used to get the correct bias correction parameters from
+ ``bias_correct`` table based on its ``gid`` column
+
+ Returns
+ -------
+ ws : np.ndarray
+ Bias corrected windspeed data in same shape as input
+ """
+
+ ifself._bias_correctisnotNoneanddset.startswith("windspeed_"):
+ out=parse_bc_table(self._bias_correct,h5_gids)
+ bc_fun,bc_fun_kwargs,bool_bc=out
+
+ ifbool_bc.any():
+ logger.debug(
+ "Bias correcting windspeed with function {} "
+ "for h5 gids: {}".format(bc_fun,h5_gids)
+ )
+
+ bc_fun_kwargs["ws"]=ws[:,bool_bc]
+ sig=signature(bc_fun)
+ bc_fun_kwargs={
+ k:v
+ fork,vinbc_fun_kwargs.items()
+ ifkinsig.parameters
+ }
+
+ ws[:,bool_bc]=bc_fun(**bc_fun_kwargs)
+
+ returnws
+
+
[docs]defget_weighted_res_ts(self,dset):
+"""Special method for calculating the exclusion-weighted mean resource
+ timeseries data for the BespokeSinglePlant.
+
+ Parameters
+ ----------
+ dset : str
+ Resource dataset name e.g., "windspeed_100m", "temperature_100m",
+ "pressure_100m", or something similar
+
+ Returns
+ -------
+ data : np.ndarray
+ Timeseries data of shape (n_time,) for the wind plant weighted by
+ the plant inclusions mask.
+ """
+ gids=self.sc_point.h5_gid_set
+ h5_gids=copy.deepcopy(gids)
+ ifself._gid_mapisnotNone:
+ h5_gids=[self._gid_map[g]forgingids]
+
+ ifself._pre_loaded_dataisNone:
+ data=self.sc_point.h5[dset,:,h5_gids]
+ else:
+ data=self._pre_loaded_data[dset,:,h5_gids]
+
+ data=self.bias_correct_ws(data,dset,h5_gids)
+
+ weights=np.zeros(len(gids))
+ fori,gidinenumerate(gids):
+ mask=self.sc_point._h5_gids==gid
+ weights[i]=self.sc_point.include_mask_flat[mask].sum()
+
+ weights/=weights.sum()
+ data=data.astype(np.float32)
+ data*=weights
+ data=np.sum(data,axis=1)
+
+ returndata
+
+
[docs]defget_weighted_res_dir(self):
+"""Special method for calculating the exclusion-weighted mean wind
+ direction for the BespokeSinglePlant
+
+ Returns
+ -------
+ mean_wind_dirs : np.ndarray
+ Timeseries array of winddirection data in shape (n_time,) in units
+ of degrees from north.
+ """
+
+ dset=f"winddirection_{self.hub_height}m"
+ gids=self.sc_point.h5_gid_set
+ h5_gids=copy.deepcopy(gids)
+ ifself._gid_mapisnotNone:
+ h5_gids=[self._gid_map[g]forgingids]
+
+ ifself._pre_loaded_dataisNone:
+ dirs=self.sc_point.h5[dset,:,h5_gids]
+ else:
+ dirs=self._pre_loaded_data[dset,:,h5_gids]
+ angles=np.radians(dirs,dtype=np.float32)
+
+ weights=np.zeros(len(gids))
+ fori,gidinenumerate(gids):
+ mask=self.sc_point._h5_gids==gid
+ weights[i]=self.sc_point.include_mask_flat[mask].sum()
+
+ weights/=weights.sum()
+ sin=np.sum(np.sin(angles)*weights,axis=1)
+ cos=np.sum(np.cos(angles)*weights,axis=1)
+
+ mean_wind_dirs=np.degrees(np.arctan2(sin,cos))
+ mean_wind_dirs[(mean_wind_dirs<0)]+=360
+
+ returnmean_wind_dirs
+
+ @property
+ defgid(self):
+"""SC point gid for this bespoke plant.
+
+ Returns
+ -------
+ int
+ """
+ returnself.sc_point.gid
+
+ @property
+ definclude_mask(self):
+"""Get the supply curve point 2D inclusion mask (included is 1,
+ excluded is 0)
+
+ Returns
+ -------
+ np.ndarray
+ """
+ returnself.sc_point.include_mask
+
+ @property
+ defpixel_side_length(self):
+"""Get the length of a single exclusion pixel side (meters)
+
+ Returns
+ -------
+ float
+ """
+ returnnp.sqrt(self.sc_point.pixel_area)*1000.0
+
+ @property
+ deforiginal_sam_sys_inputs(self):
+"""Get the original (pre-optimized) SAM windpower system inputs.
+
+ Returns
+ -------
+ dict
+ """
+ returnself._sam_sys_inputs
+
+ @property
+ defsam_sys_inputs(self):
+"""Get the SAM windpower system inputs. If the wind plant has not yet
+ been optimized, this returns the initial SAM config. If the wind plant
+ has been optimized using the wind_plant_pd object, this returns the
+ final optimized SAM plant config.
+
+ Returns
+ -------
+ dict
+ """
+ config=copy.deepcopy(self._sam_sys_inputs)
+ ifself._wind_plant_pdisNone:
+ returnconfig
+
+ config.update(self._wind_plant_pd.sam_sys_inputs)
+ returnconfig
+
+ @property
+ defsc_point(self):
+"""Get the reV supply curve point object.
+
+ Returns
+ -------
+ AggSCPoint
+ """
+ returnself._sc_point
+
+ @property
+ defmeta(self):
+"""Get the basic supply curve point meta data
+
+ Returns
+ -------
+ pd.DataFrame
+ """
+ ifself._metaisNone:
+ res_gids=json.dumps([int(g)forginself.sc_point.h5_gid_set])
+ gid_counts=json.dumps(
+ [float(np.round(n,1))forninself.sc_point.gid_counts]
+ )
+
+ self._meta=pd.DataFrame(
+ {
+ "gid":self.sc_point.gid,# needed for collection
+ SupplyCurveField.LATITUDE:self.sc_point.latitude,
+ SupplyCurveField.LONGITUDE:self.sc_point.longitude,
+ SupplyCurveField.COUNTRY:self.sc_point.country,
+ SupplyCurveField.STATE:self.sc_point.state,
+ SupplyCurveField.COUNTY:self.sc_point.county,
+ SupplyCurveField.ELEVATION:self.sc_point.elevation,
+ SupplyCurveField.TIMEZONE:self.sc_point.timezone,
+ SupplyCurveField.SC_POINT_GID:self.sc_point.sc_point_gid,
+ SupplyCurveField.SC_ROW_IND:self.sc_point.sc_row_ind,
+ SupplyCurveField.SC_COL_IND:self.sc_point.sc_col_ind,
+ SupplyCurveField.RES_GIDS:res_gids,
+ SupplyCurveField.GID_COUNTS:gid_counts,
+ SupplyCurveField.N_GIDS:self.sc_point.n_gids,
+ SupplyCurveField.OFFSHORE:self.sc_point.offshore,
+ SupplyCurveField.AREA_SQ_KM:self.sc_point.area,
+ },
+ index=[self.sc_point.gid],
+ )
+
+ returnself._meta
+
+ @property
+ defhub_height(self):
+"""Get the integer SAM system config turbine hub height (meters)
+
+ Returns
+ -------
+ int
+ """
+ returnint(self.sam_sys_inputs["wind_turbine_hub_ht"])
+
+ @property
+ defres_df(self):
+"""Get the reV compliant wind resource dataframe representing the
+ aggregated and included wind resource in the current reV supply curve
+ point at the turbine hub height. Includes a DatetimeIndex and columns
+ for temperature, pressure, windspeed, and winddirection.
+
+ Returns
+ -------
+ pd.DataFrame
+ """
+ ifself._res_dfisNone:
+ ifself._pre_loaded_dataisNone:
+ ti=self.sc_point.h5.time_index
+ else:
+ ti=self._pre_loaded_data.time_index
+
+ wd=self.get_weighted_res_dir()
+ ws=self.get_weighted_res_ts(f"windspeed_{self.hub_height}m")
+ temp=self.get_weighted_res_ts(f"temperature_{self.hub_height}m")
+ pres=self.get_weighted_res_ts(f"pressure_{self.hub_height}m")
+
+ # convert mbar to atm
+ ifnp.nanmax(pres)>1000:
+ pres*=9.86923e-6
+
+ data={
+ "temperature":temp,
+ "pressure":pres,
+ "windspeed":ws,
+ "winddirection":wd,
+ }
+
+ ifself.sam_sys_inputs.get("en_icing_cutoff"):
+ rh=self.get_weighted_res_ts("relativehumidity_2m")
+ data["relativehumidity"]=rh
+
+ self._res_df=pd.DataFrame(data,index=ti)
+
+ if"time_index_step"inself.original_sam_sys_inputs:
+ ti_step=self.original_sam_sys_inputs["time_index_step"]
+ self._res_df=self._res_df.iloc[::ti_step]
+
+ returnself._res_df
+
+ @property
+ defyears(self):
+"""Get the sorted list of analysis years.
+
+ Returns
+ -------
+ list
+ """
+ returnsorted(list(self.res_df.index.year.unique()))
+
+ @property
+ defannual_time_indexes(self):
+"""Get an ordered list of single-year time index objects that matches
+ the profile outputs from the wind_plant_ts object.
+
+ Returns
+ -------
+ list
+ """
+ tis=[]
+ foryearinself.years:
+ ti=self.res_df.index[(self.res_df.index.year==year)]
+ tis.append(WindPower.ensure_res_len(ti,ti))
+ returntis
+
+ @property
+ defwind_dist(self):
+"""Get the wind joint probability distribution and corresonding bin
+ edges
+
+ Returns
+ -------
+ wind_dist : np.ndarray
+ 2D array probability distribution of (windspeed, winddirection)
+ normalized so the sum of all values = 1.
+ ws_edges : np.ndarray
+ 1D array of windspeed (m/s) values that set the bin edges for the
+ wind probability distribution. Same len as wind_dist.shape[0] + 1
+ wd_edges : np.ndarray
+ 1D array of winddirections (deg) values that set the bin edges
+ for the wind probability dist. Same len as wind_dist.shape[1] + 1
+ """
+ ifself._wind_distisNone:
+ ws_bins=JointPD._make_bins(*self._ws_bins)
+ wd_bins=JointPD._make_bins(*self._wd_bins)
+
+ hist_out=np.histogram2d(
+ self.res_df["windspeed"],
+ self.res_df["winddirection"],
+ bins=(ws_bins,wd_bins),
+ )
+ self._wind_dist,self._ws_edges,self._wd_edges=hist_out
+ self._wind_dist/=self._wind_dist.sum()
+
+ returnself._wind_dist,self._ws_edges,self._wd_edges
+
+
[docs]definitialize_wind_plant_ts(self):
+"""Initialize the annual wind plant timeseries analysis object(s) using
+ the annual resource data and the sam system inputs from the optimized
+ plant.
+
+ Returns
+ -------
+ wind_plant_ts : dict
+ Annual reV.SAM.generation.WindPower object(s) keyed by year.
+ """
+ wind_plant_ts={}
+ foryearinself.years:
+ res_df=self.res_df[(self.res_df.index.year==year)]
+ sam_inputs=copy.deepcopy(self.sam_sys_inputs)
+
+ if"lcoe_fcr"inself._out_req:
+ lcoe_kwargs=self.get_lcoe_kwargs()
+ sam_inputs.update(lcoe_kwargs)
+
+ i_wp=WindPower(
+ res_df,self.meta,sam_inputs,output_request=self._out_req
+ )
+ wind_plant_ts[year]=i_wp
+
+ returnwind_plant_ts
+
+ @property
+ defwind_plant_pd(self):
+"""ReV WindPowerPD compute object for plant layout optimization based
+ on wind joint probability distribution
+
+ Returns
+ -------
+ reV.SAM.generation.WindPowerPD
+ """
+
+ ifself._wind_plant_pdisNone:
+ wind_dist,ws_edges,wd_edges=self.wind_dist
+ self._wind_plant_pd=WindPowerPD(
+ ws_edges,
+ wd_edges,
+ wind_dist,
+ self.meta,
+ self.sam_sys_inputs,
+ output_request=self._out_req,
+ )
+ returnself._wind_plant_pd
+
+ @property
+ defwind_plant_ts(self):
+"""ReV WindPower compute object(s) based on wind resource timeseries
+ data keyed by year
+
+ Returns
+ -------
+ dict
+ """
+ returnself._wind_plant_ts
+
+ @property
+ defplant_optimizer(self):
+"""Bespoke plant turbine placement optimizer object.
+
+ Returns
+ -------
+ PlaceTurbines
+ """
+ ifself._plant_optmisNone:
+ # put import here to delay breaking due to special dependencies
+ fromreV.bespoke.place_turbinesimportPlaceTurbines
+
+ self._plant_optm=PlaceTurbines(
+ self.wind_plant_pd,
+ self.objective_function,
+ self.capital_cost_function,
+ self.fixed_operating_cost_function,
+ self.variable_operating_cost_function,
+ self.balance_of_system_cost_function,
+ self.include_mask,
+ self.pixel_side_length,
+ self.min_spacing,
+ self.wake_loss_multiplier,
+ )
+
+ returnself._plant_optm
+
+
[docs]defrecalc_lcoe(self):
+"""Recalculate the multi-year mean LCOE based on the multi-year mean
+ annual energy production (AEP)"""
+
+ if"lcoe_fcr-means"inself.outputs:
+ lcoe_kwargs=self.get_lcoe_kwargs()
+
+ logger.debug(
+ "Recalulating multi-year mean LCOE using "
+ "multi-year mean AEP."
+ )
+
+ fcr=lcoe_kwargs['fixed_charge_rate']
+ cc=lcoe_kwargs['capital_cost']
+ foc=lcoe_kwargs['fixed_operating_cost']
+ voc=lcoe_kwargs['variable_operating_cost']
+ aep=self.outputs['annual_energy-means']
+
+ my_mean_lcoe=lcoe_fcr(fcr,cc,foc,aep,voc)
+
+ self._outputs["lcoe_fcr-means"]=my_mean_lcoe
+ self._meta[SupplyCurveField.MEAN_LCOE]=my_mean_lcoe
+
+
[docs]defget_lcoe_kwargs(self):
+"""Get a namespace of arguments for calculating LCOE based on the
+ bespoke optimized wind plant capacity
+
+ Returns
+ -------
+ lcoe_kwargs : dict
+ kwargs for the SAM lcoe model. These are based on the original
+ sam_sys_inputs, normalized to the original system_capacity, and
+ updated based on the bespoke optimized system_capacity, includes
+ fixed_charge_rate, system_capacity (kW), capital_cost ($),
+ fixed_operating_cos ($), variable_operating_cost ($/kWh),
+ balance_of_system_cost ($). Data source priority: outputs,
+ plant_optimizer, original_sam_sys_inputs, meta
+ """
+
+ kwargs_map={
+ "fixed_charge_rate":SupplyCurveField.FIXED_CHARGE_RATE,
+ "system_capacity":SupplyCurveField.CAPACITY_AC_MW,
+ "capital_cost":SupplyCurveField.BESPOKE_CAPITAL_COST,
+ "fixed_operating_cost":(
+ SupplyCurveField.BESPOKE_FIXED_OPERATING_COST
+ ),
+ "variable_operating_cost":(
+ SupplyCurveField.BESPOKE_VARIABLE_OPERATING_COST
+ ),
+ "balance_of_system_cost":(
+ SupplyCurveField.BESPOKE_BALANCE_OF_SYSTEM_COST
+ ),
+ }
+ lcoe_kwargs={}
+
+ forkwarg,meta_fieldinkwargs_map.items():
+ ifkwarginself.outputs:
+ lcoe_kwargs[kwarg]=self.outputs[kwarg]
+ elifgetattr(self.plant_optimizer,kwarg,None)isnotNone:
+ lcoe_kwargs[kwarg]=getattr(self.plant_optimizer,kwarg)
+ elifkwarginself.original_sam_sys_inputs:
+ lcoe_kwargs[kwarg]=self.original_sam_sys_inputs[kwarg]
+ elifkwarginself.meta:
+ value=float(self.meta[kwarg].values[0])
+ lcoe_kwargs[kwarg]=value
+ elifmeta_fieldinself.meta:
+ value=float(self.meta[meta_field].values[0])
+ ifmeta_field==SupplyCurveField.CAPACITY_AC_MW:
+ value*=1000# MW to kW
+ lcoe_kwargs[kwarg]=value
+
+ missing=[kforkinkwargs_mapifknotinlcoe_kwargs]
+ ifany(missing):
+ msg=(
+ "Could not find these LCOE kwargs in outputs, "
+ "plant_optimizer, original_sam_sys_inputs, or meta: {}".format(
+ missing
+ )
+ )
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ bos=lcoe_kwargs.pop("balance_of_system_cost")
+ lcoe_kwargs["capital_cost"]=lcoe_kwargs["capital_cost"]+bos
+ returnlcoe_kwargs
+
+
[docs]@classmethod
+ defcheck_dependencies(cls):
+"""Check special dependencies for bespoke"""
+
+ missing=[]
+ fornameincls.DEPENDENCIES:
+ try:
+ import_module(name)
+ exceptModuleNotFoundError:
+ missing.append(name)
+
+ ifany(missing):
+ msg=(
+ "The reV bespoke module depends on the following special "
+ "dependencies that were not found in the active "
+ "environment: {}".format(missing)
+ )
+ logger.error(msg)
+ raiseModuleNotFoundError(msg)
[docs]defagg_data_layers(self):
+"""Aggregate optional data layers if requested and save to self.meta"""
+ ifself._data_layersisnotNone:
+ logger.debug(
+ "Aggregating {} extra data layers.".format(
+ len(self._data_layers)
+ )
+ )
+ point_summary=self.meta.to_dict()
+ point_summary=self.sc_point.agg_data_layers(
+ point_summary,self._data_layers
+ )
+ self._meta=pd.DataFrame(point_summary)
+ logger.debug("Finished aggregating extra data layers.")
+
+ @property
+ defoutputs(self):
+"""Saved outputs for the single wind plant bespoke optimization.
+
+ Returns
+ -------
+ dict
+ """
+ returnself._outputs
+
+
[docs]@classmethod
+ defrun(cls,*args,**kwargs):
+"""Run the bespoke optimization for a single wind plant.
+
+ Parameters
+ ----------
+ See the class initialization parameters.
+
+ Returns
+ -------
+ bsp : dict
+ Bespoke single plant outputs namespace keyed by dataset name
+ including a dataset "meta" for the BespokeSinglePlant meta data.
+ """
+
+ withcls(*args,**kwargs)asbsp:
+ ifbsp._prior_meta:
+ logger.debug(
+ "Skipping bespoke plant optimization for gid {}. "
+ "Received prior meta data for this point.".format(bsp.gid)
+ )
+ else:
+ _=bsp.run_plant_optimization()
+
+ _=bsp.run_wind_plant_ts()
+ bsp.agg_data_layers()
+
+ meta=bsp.meta
+ out=bsp.outputs
+ out["meta"]=meta
+ foryear,tiinzip(bsp.years,bsp.annual_time_indexes):
+ out["time_index-{}".format(year)]=ti
+
+ returnout
+
+
+
[docs]classBespokeWindPlants(BaseAggregation):
+"""BespokeWindPlants"""
+
+ def__init__(self,excl_fpath,res_fpath,tm_dset,objective_function,
+ capital_cost_function,fixed_operating_cost_function,
+ variable_operating_cost_function,
+ balance_of_system_cost_function,project_points,
+ sam_files,min_spacing='5x',wake_loss_multiplier=1,
+ ga_kwargs=None,output_request=('system_capacity','cf_mean'),
+ ws_bins=(0.0,20.0,5.0),wd_bins=(0.0,360.0,45.0),
+ excl_dict=None,area_filter_kernel='queen',min_area=None,
+ resolution=64,excl_area=None,data_layers=None,
+ pre_extract_inclusions=False,eos_mult_baseline_cap_mw=200,
+ prior_run=None,gid_map=None,bias_correct=None,
+ pre_load_data=False):
+"""reV bespoke analysis class.
+
+ Much like generation, ``reV`` bespoke analysis runs SAM
+ simulations by piping in renewable energy resource data (usually
+ from the WTK), loading the SAM config, and then executing the
+ :py:class:`PySAM.Windpower.Windpower` compute module.
+ However, unlike ``reV`` generation, bespoke analysis is
+ performed on the supply-curve grid resolution, and the plant
+ layout is optimized for every supply-curve point based on an
+ optimization objective specified by the user. See the NREL
+ publication on the bespoke methodology for more information.
+
+ See the documentation for the ``reV`` SAM class (e.g.
+ :class:`reV.SAM.generation.WindPower`,
+ :class:`reV.SAM.generation.PvWattsv8`,
+ :class:`reV.SAM.generation.Geothermal`, etc.) for info on the
+ allowed and/or required SAM config file inputs.
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions data HDF5 file. The exclusions HDF5
+ file should contain the layers specified in `excl_dict`
+ and `data_layers`. These layers may also be spread out
+ across multiple HDF5 files, in which case this input should
+ be a list or tuple of filepaths pointing to the files
+ containing the layers. Note that each data layer must be
+ uniquely defined (i.e.only appear once and in a single
+ input file).
+ res_fpath : str
+ Unix shell style path to wind resource HDF5 file in NREL WTK
+ format. Can also be a path including a wildcard input like
+ ``/h5_dir/prefix*suffix`` to run bespoke on multiple years
+ of resource data. Can also be an explicit list of resource
+ HDF5 file paths, which themselves can contain wildcards. If
+ multiple files are specified in this way, they must have the
+ same coordinates but can have different time indices (i.e.
+ different years). This input must be readable by
+ :py:class:`rex.multi_year_resource.MultiYearWindResource`
+ (i.e. the resource data conform to the
+ `rex data format <https://tinyurl.com/3fy7v5kx>`_). This
+ means the data file(s) must contain a 1D ``time_index``
+ dataset indicating the UTC time of observation, a 1D
+ ``meta`` dataset represented by a DataFrame with
+ site-specific columns, and 2D resource datasets that match
+ the dimensions of (time_index, meta). The time index must
+ start at 00:00 of January 1st of the year under
+ consideration, and its shape must be a multiple of 8760.
+ tm_dset : str
+ Dataset name in the `excl_fpath` file containing the
+ techmap (exclusions-to-resource mapping data). This data
+ layer links the supply curve GID's to the generation GID's
+ that are used to evaluate the performance metrics of each
+ wind plant. By default, the generation GID's are assumed to
+ match the resource GID's, but this mapping can be customized
+ via the `gid_map` input (see the documentation for `gid_map`
+ for more details).
+
+ .. Important:: This dataset uniquely couples the (typically
+ high-resolution) exclusion layers to the (typically
+ lower-resolution) resource data. Therefore, a separate
+ techmap must be used for every unique combination of
+ resource and exclusion coordinates.
+
+ objective_function : str
+ The objective function of the optimization written out as a
+ string. This expression should compute the objective to be
+ minimized during layout optimization. Variables available
+ for computation are:
+
+ - ``n_turbines``: the number of turbines
+ - ``system_capacity``: wind plant capacity
+ - ``aep``: annual energy production
+ - ``avg_sl_dist_to_center_m``: Average straight-line
+ distance to the supply curve point center from all
+ turbine locations (in m). Useful for computing plant
+ BOS costs.
+ - ``avg_sl_dist_to_medoid_m``: Average straight-line
+ distance to the medoid of all turbine locations
+ (in m). Useful for computing plant BOS costs.
+ - ``nn_conn_dist_m``: Total BOS connection distance
+ using nearest-neighbor connections. This variable is
+ only available for the
+ ``balance_of_system_cost_function`` equation.
+ - ``fixed_charge_rate``: user input fixed_charge_rate if
+ included as part of the sam system config.
+ - ``capital_cost``: plant capital cost as evaluated
+ by `capital_cost_function`
+ - ``fixed_operating_cost``: plant fixed annual operating
+ cost as evaluated by `fixed_operating_cost_function`
+ - ``variable_operating_cost``: plant variable annual
+ operating cost as evaluated by
+ `variable_operating_cost_function`
+ - ``balance_of_system_cost``: plant balance of system
+ cost as evaluated by `balance_of_system_cost_function`
+ - ``self.wind_plant``: the SAM wind plant object,
+ through which all SAM variables can be accessed
+
+ capital_cost_function : str
+ The plant capital cost function written out as a string.
+ This expression must return the total plant capital cost in
+ $. This expression has access to the same variables as the
+ `objective_function` argument above.
+ fixed_operating_cost_function : str
+ The plant annual fixed operating cost function written out
+ as a string. This expression must return the fixed operating
+ cost in $/year. This expression has access to the same
+ variables as the `objective_function` argument above.
+ variable_operating_cost_function : str
+ The plant annual variable operating cost function written
+ out as a string. This expression must return the variable
+ operating cost in $/kWh. This expression has access to the
+ same variables as the `objective_function` argument above.
+ You can set this to "0" to effectively ignore variable
+ operating costs.
+ balance_of_system_cost_function : str
+ The plant balance-of-system cost function as a string, must
+ return the variable operating cost in $. Has access to the
+ same variables as the objective_function. You can set this
+ to "0" to effectively ignore balance-of-system costs.
+ project_points : int | list | tuple | str | dict | pd.DataFrame | slice
+ Input specifying which sites to process. A single integer
+ representing the supply curve GID of a site may be specified
+ to evaluate ``reV`` at a supply curve point. A list or tuple
+ of integers (or slice) representing the supply curve GIDs of
+ multiple sites can be specified to evaluate ``reV`` at
+ multiple specific locations. A string pointing to a project
+ points CSV file may also be specified. Typically, the CSV
+ contains the following columns:
+
+ - ``gid``: Integer specifying the supply curve GID of
+ each site.
+ - ``config``: Key in the `sam_files` input dictionary
+ (see below) corresponding to the SAM configuration to
+ use for each particular site. This value can also be
+ ``None`` (or left out completely) if you specify only
+ a single SAM configuration file as the `sam_files`
+ input.
+
+ The CSV file may also contain site-specific inputs by
+ including a column named after a config keyword (e.g. a
+ column called ``capital_cost`` may be included to specify a
+ site-specific capital cost value for each location). Columns
+ that do not correspond to a config key may also be included,
+ but they will be ignored. The CSV file input can also have
+ these extra, optional columns:
+
+ - ``capital_cost_multiplier``
+ - ``fixed_operating_cost_multiplier``
+ - ``variable_operating_cost_multiplier``
+ - ``balance_of_system_cost_multiplier``
+
+ These particular inputs are treated as multipliers to be
+ applied to the respective cost curves
+ (`capital_cost_function`, `fixed_operating_cost_function`,
+ `variable_operating_cost_function`, and
+ `balance_of_system_cost_function`) both during and
+ after the optimization. A DataFrame following the same
+ guidelines as the CSV input (or a dictionary that can be
+ used to initialize such a DataFrame) may be used for this
+ input as well. If you would like to obtain all available
+ ``reV`` supply curve points to run, you can use the
+ :class:`reV.supply_curve.extent.SupplyCurveExtent` class
+ like so::
+
+ import pandas as pd
+ from reV.supply_curve.extent import SupplyCurveExtent
+
+ excl_fpath = "..."
+ resolution = ...
+ with SupplyCurveExtent(excl_fpath, resolution) as sc:
+ points = sc.valid_sc_points(tm_dset).tolist()
+ points = pd.DataFrame({"gid": points})
+ points["config"] = "default" # or a list of config choices
+
+ # Use the points directly or save them to csv for CLI usage
+ points.to_csv("project_points.csv", index=False)
+
+ sam_files : dict | str
+ A dictionary mapping SAM input configuration ID(s) to SAM
+ configuration(s). Keys are the SAM config ID(s) which
+ correspond to the ``config`` column in the project points
+ CSV. Values for each key are either a path to a
+ corresponding SAM config file or a full dictionary
+ of SAM config inputs. For example::
+
+ sam_files = {
+ "default": "/path/to/default/sam.json",
+ "onshore": "/path/to/onshore/sam_config.yaml",
+ "offshore": {
+ "sam_key_1": "sam_value_1",
+ "sam_key_2": "sam_value_2",
+ ...
+ },
+ ...
+ }
+
+ This input can also be a string pointing to a single SAM
+ config file. In this case, the ``config`` column of the
+ CSV points input should be set to ``None`` or left out
+ completely. See the documentation for the ``reV`` SAM class
+ (e.g. :class:`reV.SAM.generation.WindPower`,
+ :class:`reV.SAM.generation.PvWattsv8`,
+ :class:`reV.SAM.generation.Geothermal`, etc.) for
+ info on the allowed and/or required SAM config file inputs.
+ min_spacing : float | int | str, optional
+ Minimum spacing between turbines (in meters). This input can
+ also be a string like "5x", which is interpreted as 5 times
+ the turbine rotor diameter. By default, ``"5x"``.
+ wake_loss_multiplier : float, optional
+ A multiplier used to scale the annual energy lost due to
+ wake losses.
+
+ .. WARNING:: This multiplier will ONLY be applied during the
+ optimization process and will NOT come through in output
+ values such as the hourly profiles, aep, any of the cost
+ functions, or even the output objective.
+
+ By default, ``1``.
+ ga_kwargs : dict, optional
+ Dictionary of keyword arguments to pass to GA
+ initialization. If ``None``, default initialization values
+ are used. See
+ :class:`~reV.bespoke.gradient_free.GeneticAlgorithm` for
+ a description of the allowed keyword arguments.
+ By default, ``None``.
+ output_request : list | tuple, optional
+ Outputs requested from the SAM windpower simulation after
+ the bespoke plant layout optimization. Can be any of the
+ parameters in the "Outputs" group of the PySAM module
+ :py:class:`PySAM.Windpower.Windpower.Outputs`, PySAM module.
+ This list can also include a select number of SAM
+ config/resource parameters to include in the output:
+ any key in any of the
+ `output attribute JSON files <https://tinyurl.com/4bmrpe3j/>`_
+ may be requested. Time-series profiles requested via this
+ input are output in UTC. This input can also be used to
+ request resource means like ``"ws_mean"``,
+ ``"windspeed_mean"``, ``"temperature_mean"``, and
+ ``"pressure_mean"``. By default,
+ ``('system_capacity', 'cf_mean')``.
+ ws_bins : tuple, optional
+ A 3-entry tuple with ``(start, stop, step)`` for the
+ windspeed binning of the wind joint probability
+ distribution. The stop value is inclusive, so
+ ``ws_bins=(0, 20, 5)`` would result in four bins with bin
+ edges (0, 5, 10, 15, 20). By default, ``(0.0, 20.0, 5.0)``.
+ wd_bins : tuple, optional
+ A 3-entry tuple with ``(start, stop, step)`` for the wind
+ direction binning of the wind joint probability
+ distribution. The stop value is inclusive, so
+ ``wd_bins=(0, 360, 90)`` would result in four bins with bin
+ edges (0, 90, 180, 270, 360).
+ By default, ``(0.0, 360.0, 45.0)``.
+ excl_dict : dict, optional
+ Dictionary of exclusion keyword arguments of the format
+ ``{layer_dset_name: {kwarg: value}}``, where
+ ``layer_dset_name`` is a dataset in the exclusion h5 file
+ and the ``kwarg: value`` pair is a keyword argument to
+ the :class:`reV.supply_curve.exclusions.LayerMask` class.
+ For example::
+
+ excl_dict = {
+ "typical_exclusion": {
+ "exclude_values": 255,
+ },
+ "another_exclusion": {
+ "exclude_values": [2, 3],
+ "weight": 0.5
+ },
+ "exclusion_with_nodata": {
+ "exclude_range": [10, 100],
+ "exclude_nodata": True,
+ "nodata_value": -1
+ },
+ "partial_setback": {
+ "use_as_weights": True
+ },
+ "height_limit": {
+ "exclude_range": [0, 200]
+ },
+ "slope": {
+ "include_range": [0, 20]
+ },
+ "developable_land": {
+ "force_include_values": 42
+ },
+ "more_developable_land": {
+ "force_include_range": [5, 10]
+ },
+ ...
+ }
+
+ Note that all the keys given in this dictionary should be
+ datasets of the `excl_fpath` file. If ``None`` or empty
+ dictionary, no exclusions are applied. By default, ``None``.
+ area_filter_kernel : {"queen", "rook"}, optional
+ Contiguous area filter method to use on final exclusions
+ mask. The filters are defined as::
+
+ # Queen: # Rook:
+ [[1,1,1], [[0,1,0],
+ [1,1,1], [1,1,1],
+ [1,1,1]] [0,1,0]]
+
+ These filters define how neighboring pixels are "connected".
+ Once pixels in the final exclusion layer are connected, the
+ area of each resulting cluster is computed and compared
+ against the `min_area` input. Any cluster with an area
+ less than `min_area` is excluded from the final mask.
+ This argument has no effect if `min_area` is ``None``.
+ By default, ``"queen"``.
+ min_area : float, optional
+ Minimum area (in km\ :sup:`2`) required to keep an isolated
+ cluster of (included) land within the resulting exclusions
+ mask. Any clusters of land with areas less than this value
+ will be marked as exclusions. See the documentation for
+ `area_filter_kernel` for an explanation of how the area of
+ each land cluster is computed. If ``None``, no area
+ filtering is performed. By default, ``None``.
+ resolution : int, optional
+ Supply Curve resolution. This value defines how many pixels
+ are in a single side of a supply curve cell. For example,
+ a value of ``64`` would generate a supply curve where the
+ side of each supply curve cell is ``64x64`` exclusion
+ pixels. By default, ``64``.
+ excl_area : float, optional
+ Area of a single exclusion mask pixel (in km\ :sup:`2`).
+ If ``None``, this value will be inferred from the profile
+ transform attribute in `excl_fpath`. By default, ``None``.
+ data_layers : dict, optional
+ Dictionary of aggregation data layers of the format::
+
+ data_layers = {
+ "output_layer_name": {
+ "dset": "layer_name",
+ "method": "mean",
+ "fpath": "/path/to/data.h5"
+ },
+ "another_output_layer_name": {
+ "dset": "input_layer_name",
+ "method": "mode",
+ # optional "fpath" key omitted
+ },
+ ...
+ }
+
+ The ``"output_layer_name"`` is the column name under which
+ the aggregated data will appear in the meta DataFrame of the
+ output file. The ``"output_layer_name"`` does not have to
+ match the ``dset`` input value. The latter should match
+ the layer name in the HDF5 from which the data to aggregate
+ should be pulled. The ``method`` should be one of
+ ``{"mode", "mean", "min", "max", "sum", "category"}``,
+ describing how the high-resolution data should be aggregated
+ for each supply curve point. ``fpath`` is an optional key
+ that can point to an HDF5 file containing the layer data. If
+ left out, the data is assumed to exist in the file(s)
+ specified by the `excl_fpath` input. If ``None``, no data
+ layer aggregation is performed. By default, ``None``.
+ pre_extract_inclusions : bool, optional
+ Optional flag to pre-extract/compute the inclusion mask from
+ the `excl_dict` input. It is typically faster to compute
+ the inclusion mask on the fly with parallel workers.
+ By default, ``False``.
+ eos_mult_baseline_cap_mw : int | float, optional
+ Baseline plant capacity (MW) used to calculate economies of
+ scale (EOS) multiplier from the `capital_cost_function`. EOS
+ multiplier is calculated as the $-per-kW of the wind plant
+ divided by the $-per-kW of a plant with this baseline
+ capacity. By default, `200` (MW), which aligns the baseline
+ with ATB assumptions. See here: https://tinyurl.com/y85hnu6h.
+ prior_run : str, optional
+ Optional filepath to a bespoke output HDF5 file belonging to
+ a prior run. If specified, this module will only run the
+ timeseries power generation step and assume that all of the
+ wind plant layouts are fixed from the prior run. The meta
+ data of this file must contain the following columns
+ (automatically satisfied if the HDF5 file was generated by
+ ``reV`` bespoke):
+
+ - ``capacity`` : Capacity of the plant, in MW.
+ - ``turbine_x_coords``: A string representation of a
+ python list containing the X coordinates (in m; origin
+ of cell at bottom left) of the turbines within the
+ plant (supply curve cell).
+ - ``turbine_y_coords`` : A string representation of a
+ python list containing the Y coordinates (in m; origin
+ of cell at bottom left) of the turbines within the
+ plant (supply curve cell).
+
+ If ``None``, no previous run data is considered.
+ By default, ``None``
+ gid_map : str | dict, optional
+ Mapping of unique integer generation gids (keys) to single
+ integer resource gids (values). This enables unique
+ generation gids in the project points to map to non-unique
+ resource gids, which can be useful when evaluating multiple
+ resource datasets in ``reV`` (e.g., forecasted ECMWF
+ resource data to complement historical WTK meteorology).
+ This input can be a pre-extracted dictionary or a path to a
+ JSON or CSV file. If this input points to a CSV file, the
+ file must have the columns ``gid`` (which matches the
+ project points) and ``gid_map`` (gids to extract from the
+ resource input). If ``None``, the GID values in the project
+ points are assumed to match the resource GID values.
+ By default, ``None``.
+ bias_correct : str | pd.DataFrame, optional
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+ pre_load_data : bool, optional
+ Option to pre-load resource data. This step can be
+ time-consuming up front, but it drastically reduces the
+ number of parallel reads to the `res_fpath` HDF5 file(s),
+ and can have a significant overall speedup on systems with
+ slow parallel I/O capabilities. Pre-loaded data can use a
+ significant amount of RAM, so be sure to split execution
+ across many nodes (e.g. 100 nodes, 36 workers each for
+ CONUS) or request large amounts of memory for a smaller
+ number of nodes. By default, ``False``.
+ """
+
+ log_versions(logger)
+ logger.info('Initializing BespokeWindPlants...')
+ logger.info('Resource filepath: {}'.format(res_fpath))
+ logger.info('Exclusion filepath: {}'.format(excl_fpath))
+ logger.debug('Exclusion dict: {}'.format(excl_dict))
+ logger.info('Bespoke objective function: {}'
+ .format(objective_function))
+ logger.info('Bespoke capital cost function: {}'
+ .format(capital_cost_function))
+ logger.info('Bespoke fixed operating cost function: {}'
+ .format(fixed_operating_cost_function))
+ logger.info('Bespoke variable operating cost function: {}'
+ .format(variable_operating_cost_function))
+ logger.info('Bespoke balance of system cost function: {}'
+ .format(balance_of_system_cost_function))
+ logger.info('Bespoke wake loss multiplier: {}'
+ .format(wake_loss_multiplier))
+ logger.info('Bespoke GA initialization kwargs: {}'.format(ga_kwargs))
+
+ logger.info(
+ "Bespoke pre-extracting exclusions: {}".format(
+ pre_extract_inclusions
+ )
+ )
+ logger.info(
+ "Bespoke pre-extracting resource data: {}".format(pre_load_data)
+ )
+ logger.info("Bespoke prior run: {}".format(prior_run))
+ logger.info("Bespoke GID map: {}".format(gid_map))
+ logger.info("Bespoke bias correction table: {}".format(bias_correct))
+
+ BespokeSinglePlant.check_dependencies()
+
+ self._project_points=self._parse_points(project_points,sam_files)
+
+ super().__init__(
+ excl_fpath,
+ tm_dset,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ resolution=resolution,
+ excl_area=excl_area,
+ gids=self._project_points.gids,
+ pre_extract_inclusions=pre_extract_inclusions,
+ )
+
+ self._res_fpath=res_fpath
+ self._obj_fun=objective_function
+ self._cap_cost_fun=capital_cost_function
+ self._foc_fun=fixed_operating_cost_function
+ self._voc_fun=variable_operating_cost_function
+ self._bos_fun=balance_of_system_cost_function
+ self._min_spacing=min_spacing
+ self._wake_loss_multiplier=wake_loss_multiplier
+ self._ga_kwargs=ga_kwargsor{}
+ self._output_request=SAMOutputRequest(output_request)
+ self._ws_bins=ws_bins
+ self._wd_bins=wd_bins
+ self._data_layers=data_layers
+ self._eos_mult_baseline_cap_mw=eos_mult_baseline_cap_mw
+ self._prior_meta=self._parse_prior_run(prior_run)
+ self._gid_map=BespokeSinglePlant._parse_gid_map(gid_map)
+ self._bias_correct=Gen._parse_bc(bias_correct)
+ self._outputs={}
+ self._check_files()
+
+ self._pre_loaded_data=None
+ self._pre_load_data(pre_load_data)
+
+ self._slice_lookup=None
+
+ logger.info(
+ "Initialized BespokeWindPlants with project points: {}".format(
+ self._project_points
+ )
+ )
+
+ @staticmethod
+ def_parse_points(points,sam_configs):
+"""Parse a project points object using a project points file
+
+ Parameters
+ ----------
+ points : int | slice | list | str | PointsControl | None
+ Slice or list specifying project points, string pointing to a
+ project points csv, or a fully instantiated PointsControl object.
+ Can also be a single site integer value. Points csv should have
+ `SiteDataField.GID` and 'config' column, the config maps to the
+ sam_configs dict keys.
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+
+ Returns
+ -------
+ ProjectPoints : ~reV.config.project_points.ProjectPoints
+ Project points object laying out the supply curve gids to
+ analyze.
+ """
+ pc=Gen.get_pc(
+ points,
+ points_range=None,
+ sam_configs=sam_configs,
+ tech="windpower",
+ sites_per_worker=1,
+ )
+
+ returnpc.project_points
+
+ @staticmethod
+ def_parse_prior_run(prior_run):
+"""Extract bespoke meta data from prior run and verify that the run is
+ compatible with the new job specs.
+
+ Parameters
+ ----------
+ prior_run : str | None
+ Optional filepath to a bespoke output .h5 file belonging to a prior
+ run. This will only run the timeseries power generation step and
+ assume that all of the wind plant layouts are fixed given the prior
+ run. The meta data of this file needs columns "capacity",
+ "turbine_x_coords", and "turbine_y_coords".
+
+ Returns
+ -------
+ meta : pd.DataFrame | None
+ Meta data from the previous bespoke run. This includes the
+ previously optimized wind farm layouts. All of the nested list
+ columns will be json loaded.
+ """
+
+ meta=None
+
+ ifprior_runisnotNone:
+ assertos.path.isfile(prior_run)
+ assertprior_run.endswith(".h5")
+
+ withOutputs(prior_run,mode="r")asf:
+ meta=f.meta
+ meta=meta.rename(columns=SupplyCurveField.map_from_legacy())
+
+ # pylint: disable=no-member
+ forcolinmeta.columns:
+ val=meta[col].values[0]
+ ifisinstance(val,str)andval[0]=="["andval[-1]=="]":
+ meta[col]=meta[col].apply(json.loads)
+
+ returnmeta
+
+ def_get_prior_meta(self,gid):
+"""Get the meta data for a given gid from the prior run (if available)
+
+ Parameters
+ ----------
+ gid : int
+ SC point gid for site to pull prior meta for.
+
+ Returns
+ -------
+ meta : pd.DataFrame
+ Prior meta data for just the requested gid.
+ """
+ meta=None
+
+ ifself._prior_metaisnotNone:
+ mask=self._prior_meta[SupplyCurveField.SC_POINT_GID]==gid
+ ifany(mask):
+ meta=self._prior_meta[mask]
+
+ returnmeta
+
+ def_check_files(self):
+"""Do a preflight check on input files"""
+
+ paths=self._excl_fpath
+ ifisinstance(self._excl_fpath,str):
+ paths=[self._excl_fpath]
+
+ forpathinpaths:
+ ifnotos.path.exists(path):
+ raiseFileNotFoundError(
+ "Could not find required exclusions file: ""{}".format(
+ path
+ )
+ )
+
+ withExclusionLayers(paths)asexcl:
+ ifself._tm_dsetnotinexcl:
+ raiseFileInputError(
+ 'Could not find techmap dataset "{}" '
+ "in the exclusions file(s): {}".format(
+ self._tm_dset,paths
+ )
+ )
+
+ # just check that this file exists, cannot check res_fpath if *glob
+ withMultiYearWindResource(self._res_fpath)asf:
+ assertany(f.dsets)
+
+ def_pre_load_data(self,pre_load_data):
+"""Pre-load resource data, if requested."""
+ ifnotpre_load_data:
+ return
+
+ sc_gid_to_hh={
+ gid:self._hh_for_sc_gid(gid)
+ forgidinself._project_points.df[ResourceMetaField.GID]
+ }
+
+ withExclusionLayers(self._excl_fpath)asexcl:
+ tm=excl[self._tm_dset]
+
+ scp_kwargs={"shape":self.shape,"resolution":self._resolution}
+ slices={
+ gid:SupplyCurvePoint.get_agg_slices(gid=gid,**scp_kwargs)
+ forgidinself._project_points.df[ResourceMetaField.GID]
+ }
+
+ sc_gid_to_res_gid={
+ gid:sorted(set(tm[slx,sly].flatten()))
+ forgid,(slx,sly)inslices.items()
+ }
+
+ forsc_gid,res_gidsinsc_gid_to_res_gid.items():
+ ifres_gids[0]<0:
+ sc_gid_to_res_gid[sc_gid]=res_gids[1:]
+
+ ifself._gid_mapisnotNone:
+ forsc_gid,res_gidsinsc_gid_to_res_gid.items():
+ sc_gid_to_res_gid[sc_gid]=sorted(
+ self._gid_map[g]forginres_gids
+ )
+
+ logger.info("Pre-loading resource data for Bespoke run... ")
+ self._pre_loaded_data=BespokeMultiPlantData(
+ self._res_fpath,
+ sc_gid_to_hh,
+ sc_gid_to_res_gid,
+ pre_load_humidity=self._project_points.sam_config_obj.icing,
+ )
+
+ def_hh_for_sc_gid(self,sc_gid):
+"""Fetch the hh for a given sc_gid"""
+ config=self.sam_sys_inputs_with_site_data(sc_gid)
+ returnint(config["wind_turbine_hub_ht"])
+
+ def_pre_loaded_data_for_sc_gid(self,sc_gid):
+"""Pre-load data for a given SC GID, if requested."""
+ ifself._pre_loaded_dataisNone:
+ returnNone
+
+ returnself._pre_loaded_data.get_preloaded_data_for_gid(sc_gid)
+
+ def_get_bc_for_gid(self,gid):
+"""Get the bias correction table trimmed down just for the resource
+ pixels corresponding to a single supply curve GID. This can help
+ prevent excess memory usage when doing complex bias correction
+ distributed to parallel workers.
+
+ Parameters
+ ----------
+ gid : int
+ SC point gid for site to pull bias correction data for
+
+ Returns
+ -------
+ out : pd.DataFrame | None
+ If bias_correct was input, this is just the rows from the larger
+ bias correction table that correspond to the SC point gid
+ """
+ out=self._bias_correct
+
+ ifself._bias_correctisnotNone:
+ h5_gids=[]
+ try:
+ scp_kwargs=dict(
+ gid=gid,
+ excl=self._excl_fpath,
+ tm_dset=self._tm_dset,
+ resolution=self._resolution,
+ )
+ withSupplyCurvePoint(**scp_kwargs)asscp:
+ h5_gids=scp.h5_gid_set
+ exceptEmptySupplyCurvePointError:
+ pass
+
+ ifself._gid_mapisnotNone:
+ h5_gids=[self._gid_map[g]forginh5_gids]
+
+ mask=self._bias_correct.index.isin(h5_gids)
+ out=self._bias_correct[mask]
+
+ returnout
+
+ @property
+ defoutputs(self):
+"""Saved outputs for the multi wind plant bespoke optimization. Keys
+ are reV supply curve gids and values are BespokeSinglePlant.outputs
+ dictionaries.
+
+ Returns
+ -------
+ dict
+ """
+ returnself._outputs
+
+ @property
+ defcompleted_gids(self):
+"""Get a sorted list of completed BespokeSinglePlant gids
+
+ Returns
+ -------
+ list
+ """
+ returnsorted(list(self.outputs.keys()))
+
+ @property
+ defmeta(self):
+"""Meta data for all completed BespokeSinglePlant objects.
+
+ Returns
+ -------
+ pd.DataFrame
+ """
+ meta=[self.outputs[g]["meta"]forginself.completed_gids]
+ iflen(self.completed_gids)>1:
+ meta=pd.concat(meta,axis=0)
+ else:
+ meta=meta[0]
+ returnmeta
+
+ @property
+ defslice_lookup(self):
+"""Dict | None: Lookup mapping sc_point_gid to exclusion slice."""
+ ifself._slice_lookupisNoneandself._inclusion_maskisnotNone:
+ withSupplyCurveExtent(
+ self._excl_fpath,resolution=self._resolution
+ )assc:
+ assertself.shape==self._inclusion_mask.shape
+ self._slice_lookup=sc.get_slice_lookup(self.gids)
+
+ returnself._slice_lookup
+
+
[docs]defsam_sys_inputs_with_site_data(self,gid):
+"""Update the sam_sys_inputs with site data for the given GID.
+
+ Site data is extracted from the project points DataFrame. Every
+ column in the project DataFrame becomes a key in the site_data
+ output dictionary.
+
+ Parameters
+ ----------
+ gid : int
+ SC point gid for site to pull site data for.
+
+ Returns
+ -------
+ dictionary : dict
+ SAM system config with extra keys from the project points
+ DataFrame.
+ """
+
+ gid_idx=self._project_points.index(gid)
+ site_data=self._project_points.df.iloc[gid_idx]
+
+ site_sys_inputs=self._project_points[gid][1]
+ site_sys_inputs.update(
+ {
+ k:v
+ fork,vinsite_data.to_dict().items()
+ ifnot(isinstance(v,float)andnp.isnan(v))
+ }
+ )
+ returnsite_sys_inputs
+
+ def_init_fout(self,out_fpath,sample):
+"""Initialize the bespoke output h5 file with meta and time index dsets
+
+ Parameters
+ ----------
+ out_fpath : str
+ Full filepath to an output .h5 file to save Bespoke data to. The
+ parent directories will be created if they do not already exist.
+ sample : dict
+ A single sample BespokeSinglePlant output dict that has been run
+ and has output data.
+ """
+ out_dir=os.path.dirname(out_fpath)
+ ifnotos.path.exists(out_dir):
+ create_dirs(out_dir)
+
+ withOutputs(out_fpath,mode="w")asf:
+ f._set_meta("meta",self.meta,attrs={})
+ ti_dsets=[
+ dfordinsample.keys()ifd.startswith("time_index-")
+ ]
+ fordsetinti_dsets:
+ f._set_time_index(dset,sample[dset],attrs={})
+ f._set_time_index("time_index",sample[dset],attrs={})
+
+ def_collect_out_arr(self,dset,sample):
+"""Collect single-plant data arrays into complete arrays with data from
+ all BespokeSinglePlant objects.
+
+ Parameters
+ ----------
+ dset : str
+ Dataset to collect, this should be an output dataset present in
+ BespokeSinglePlant.outputs
+ sample : dict
+ A single sample BespokeSinglePlant output dict that has been run
+ and has output data.
+
+ Returns
+ -------
+ full_arr : np.ndarray
+ Full data array either 1D for scalar data or 2D for timeseries
+ data (n_time, n_plant) for all BespokeSinglePlant objects
+ """
+
+ single_arr=sample[dset]
+
+ ifisinstance(single_arr,Number):
+ shape=(len(self.completed_gids),)
+ sample_num=single_arr
+ elifisinstance(single_arr,(list,tuple,np.ndarray)):
+ shape=(len(single_arr),len(self.completed_gids))
+ sample_num=single_arr[0]
+ else:
+ msg='Not writing dataset "{}" of type "{}" to disk.'.format(
+ dset,type(single_arr)
+ )
+ logger.info(msg)
+ returnNone
+
+ ifisinstance(sample_num,float):
+ dtype=np.float32
+ else:
+ dtype=type(sample_num)
+ full_arr=np.zeros(shape,dtype=dtype)
+
+ # collect data from all wind plants
+ logger.info(
+ 'Collecting dataset "{}" with final shape {}'.format(dset,shape)
+ )
+ fori,gidinenumerate(self.completed_gids):
+ iflen(full_arr.shape)==1:
+ full_arr[i]=self.outputs[gid][dset]
+ else:
+ full_arr[:,i]=self.outputs[gid][dset]
+
+ returnfull_arr
+
+
[docs]defsave_outputs(self,out_fpath):
+"""Save Bespoke Wind Plant optimization outputs to disk.
+
+ Parameters
+ ----------
+ out_fpath : str
+ Full filepath to an output .h5 file to save Bespoke data to. The
+ parent directories will be created if they do not already exist.
+
+ Returns
+ -------
+ out_fpath : str
+ Full filepath to desired .h5 output file, the .h5 extension has
+ been added if it was not already present.
+ """
+ ifnotout_fpath.endswith(".h5"):
+ out_fpath+=".h5"
+
+ ifModuleName.BESPOKEnotinout_fpath:
+ extension_with_module="_{}.h5".format(ModuleName.BESPOKE)
+ out_fpath=out_fpath.replace(".h5",extension_with_module)
+
+ ifnotself.completed_gids:
+ msg=(
+ "No output data found! It is likely that all requested "
+ "points are excluded."
+ )
+ logger.warning(msg)
+ warn(msg)
+ returnout_fpath
+
+ sample=self.outputs[self.completed_gids[0]]
+ self._init_fout(out_fpath,sample)
+
+ dsets=[
+ d
+ fordinsample.keys()
+ ifnotd.startswith("time_index-")andd!="meta"
+ ]
+ withOutputs(out_fpath,mode="a")asf:
+ fordsetindsets:
+ full_arr=self._collect_out_arr(dset,sample)
+ iffull_arrisnotNone:
+ dset_no_year=dset
+ ifparse_year(dset,option="boolean"):
+ year=parse_year(dset)
+ dset_no_year=dset.replace("-{}".format(year),"")
+
+ attrs=BespokeSinglePlant.OUT_ATTRS.get(dset_no_year,{})
+ attrs=copy.deepcopy(attrs)
+ dtype=attrs.pop("dtype",np.float32)
+ chunks=attrs.pop("chunks",None)
+ try:
+ f.write_dataset(
+ dset,full_arr,dtype,chunks=chunks,attrs=attrs
+ )
+ exceptExceptionase:
+ msg='Failed to write "{}" to disk.'.format(dset)
+ logger.exception(msg)
+ raiseOSError(msg)frome
+
+ logger.info("Saved output data to: {}".format(out_fpath))
+ returnout_fpath
+
+ # pylint: disable=arguments-renamed
+
[docs]@classmethod
+ defrun_serial(cls,excl_fpath,res_fpath,tm_dset,
+ sam_sys_inputs,objective_function,
+ capital_cost_function,
+ fixed_operating_cost_function,
+ variable_operating_cost_function,
+ balance_of_system_cost_function,
+ min_spacing='5x',wake_loss_multiplier=1,ga_kwargs=None,
+ output_request=('system_capacity','cf_mean'),
+ ws_bins=(0.0,20.0,5.0),wd_bins=(0.0,360.0,45.0),
+ excl_dict=None,inclusion_mask=None,
+ area_filter_kernel='queen',min_area=None,
+ resolution=64,excl_area=0.0081,data_layers=None,
+ gids=None,exclusion_shape=None,slice_lookup=None,
+ eos_mult_baseline_cap_mw=200,prior_meta=None,
+ gid_map=None,bias_correct=None,pre_loaded_data=None):
+"""
+ Standalone serial method to run bespoke optimization.
+ See BespokeWindPlants docstring for parameter description.
+
+ This method can only take a single sam_sys_inputs... For a spatially
+ variant gid-to-config mapping, see the BespokeWindPlants class methods.
+
+ Returns
+ -------
+ out : dict
+ Bespoke outputs keyed by sc point gid
+ """
+
+ out={}
+ withSupplyCurveExtent(excl_fpath,resolution=resolution)assc:
+ ifgidsisNone:
+ gids=sc.valid_sc_points(tm_dset)
+ elifnp.issubdtype(type(gids),np.number):
+ gids=[gids]
+ ifslice_lookupisNone:
+ slice_lookup=sc.get_slice_lookup(gids)
+ ifexclusion_shapeisNone:
+ exclusion_shape=sc.exclusions.shape
+
+ cls._check_inclusion_mask(inclusion_mask,gids,exclusion_shape)
+
+ # pre-extract handlers so they are not repeatedly initialized
+ file_kwargs={
+ "excl_dict":excl_dict,
+ "area_filter_kernel":area_filter_kernel,
+ "min_area":min_area,
+ "h5_handler":MultiYearWindResource,
+ }
+
+ withAggFileHandler(excl_fpath,res_fpath,**file_kwargs)asfh:
+ n_finished=0
+ forgidingids:
+ gid_inclusions=cls._get_gid_inclusion_mask(
+ inclusion_mask,gid,slice_lookup,resolution=resolution
+ )
+ try:
+ bsp_plant_out=BespokeSinglePlant.run(
+ gid,
+ fh.exclusions,
+ fh.h5,
+ tm_dset,
+ sam_sys_inputs,
+ objective_function,
+ capital_cost_function,
+ fixed_operating_cost_function,
+ variable_operating_cost_function,
+ balance_of_system_cost_function,
+ min_spacing=min_spacing,
+ wake_loss_multiplier=wake_loss_multiplier,
+ ga_kwargs=ga_kwargs,
+ output_request=output_request,
+ ws_bins=ws_bins,
+ wd_bins=wd_bins,
+ excl_dict=excl_dict,
+ inclusion_mask=gid_inclusions,
+ resolution=resolution,
+ excl_area=excl_area,
+ data_layers=data_layers,
+ exclusion_shape=exclusion_shape,
+ eos_mult_baseline_cap_mw=eos_mult_baseline_cap_mw,
+ prior_meta=prior_meta,
+ gid_map=gid_map,
+ bias_correct=bias_correct,
+ pre_loaded_data=pre_loaded_data,
+ close=False,
+ )
+
+ exceptEmptySupplyCurvePointError:
+ logger.debug(
+ "SC gid {} is fully excluded or does not "
+ "have any valid source data!".format(gid)
+ )
+ exceptExceptionase:
+ msg="SC gid {} failed!".format(gid)
+ logger.exception(msg)
+ raiseRuntimeError(msg)frome
+ else:
+ n_finished+=1
+ logger.debug(
+ "Serial bespoke: "
+ "{} out of {} points complete".format(
+ n_finished,len(gids)
+ )
+ )
+ log_mem(logger)
+ out[gid]=bsp_plant_out
+
+ returnout
+
+
[docs]defrun_parallel(self,max_workers=None):
+"""Run the bespoke optimization for many supply curve points in
+ parallel.
+
+ Parameters
+ ----------
+ max_workers : int | None, optional
+ Number of cores to run summary on. None is all
+ available cpus, by default None
+
+ Returns
+ -------
+ out : dict
+ Bespoke outputs keyed by sc point gid
+ """
+
+ logger.info(
+ "Running bespoke optimization for points {} through {} "
+ "at a resolution of {} on {} cores.".format(
+ self.gids[0],self.gids[-1],self._resolution,max_workers
+ )
+ )
+
+ futures=[]
+ out={}
+ n_finished=0
+ loggers=[__name__,"reV.supply_curve.point_summary","reV"]
+ withSpawnProcessPool(max_workers=max_workers,loggers=loggers)asexe:
+ # iterate through split executions, submitting each to worker
+ forgidinself.gids:
+ # submit executions and append to futures list
+ gid_incl_mask=None
+ ifself._inclusion_maskisnotNone:
+ rs,cs=self.slice_lookup[gid]
+ gid_incl_mask=self._inclusion_mask[rs,cs]
+
+ futures.append(exe.submit(
+ self.run_serial,
+ self._excl_fpath,
+ self._res_fpath,
+ self._tm_dset,
+ self.sam_sys_inputs_with_site_data(gid),
+ self._obj_fun,
+ self._cap_cost_fun,
+ self._foc_fun,
+ self._voc_fun,
+ self._bos_fun,
+ self._min_spacing,
+ wake_loss_multiplier=self._wake_loss_multiplier,
+ ga_kwargs=self._ga_kwargs,
+ output_request=self._output_request,
+ ws_bins=self._ws_bins,
+ wd_bins=self._wd_bins,
+ excl_dict=self._excl_dict,
+ inclusion_mask=gid_incl_mask,
+ area_filter_kernel=self._area_filter_kernel,
+ min_area=self._min_area,
+ resolution=self._resolution,
+ excl_area=self._excl_area,
+ data_layers=self._data_layers,
+ gids=gid,
+ exclusion_shape=self.shape,
+ slice_lookup=copy.deepcopy(self.slice_lookup),
+ eos_mult_baseline_cap_mw=self._eos_mult_baseline_cap_mw,
+ prior_meta=self._get_prior_meta(gid),
+ gid_map=self._gid_map,
+ bias_correct=self._get_bc_for_gid(gid),
+ pre_loaded_data=self._pre_loaded_data_for_sc_gid(gid)))
+
+ # gather results
+ forfutureinas_completed(futures):
+ n_finished+=1
+ out.update(future.result())
+ ifn_finished%10==0:
+ mem=psutil.virtual_memory()
+ logger.info(
+ "Parallel bespoke futures collected: "
+ "{} out of {}. Memory usage is {:.3f} GB out "
+ "of {:.3f} GB ({:.2f}% utilized).".format(
+ n_finished,
+ len(futures),
+ mem.used/1e9,
+ mem.total/1e9,
+ 100*mem.used/mem.total,
+ )
+ )
+
+ returnout
+
+
[docs]defrun(self,out_fpath=None,max_workers=None):
+"""Run the bespoke wind plant optimization in serial or parallel.
+
+ Parameters
+ ----------
+ out_fpath : str, optional
+ Path to output file. If ``None``, no output file will
+ be written. If the filepath is specified but the module name
+ (bespoke) is not included, the module name will get added to
+ the output file name. By default, ``None``.
+ max_workers : int, optional
+ Number of local workers to run on. If ``None``, uses all
+ available cores (typically 36). By default, ``None``.
+
+ Returns
+ -------
+ str | None
+ Path to output HDF5 file, or ``None`` if results were not
+ written to disk.
+ """
+
+ # parallel job distribution test.
+ ifself._obj_fun=="test":
+ returnTrue
+
+ ifmax_workers==1:
+ slice_lookup=copy.deepcopy(self.slice_lookup)
+ forgidinself.gids:
+ gid_incl_mask=None
+ ifself._inclusion_maskisnotNone:
+ rs,cs=slice_lookup[gid]
+ gid_incl_mask=self._inclusion_mask[rs,cs]
+
+ sam_inputs=self.sam_sys_inputs_with_site_data(gid)
+ prior_meta=self._get_prior_meta(gid)
+ pre_loaded_data=self._pre_loaded_data_for_sc_gid(gid)
+ afk=self._area_filter_kernel
+ wlm=self._wake_loss_multiplier
+ i_bc=self._get_bc_for_gid(gid)
+ ebc=self._eos_mult_baseline_cap_mw
+
+ si=self.run_serial(self._excl_fpath,
+ self._res_fpath,
+ self._tm_dset,
+ sam_inputs,
+ self._obj_fun,
+ self._cap_cost_fun,
+ self._foc_fun,
+ self._voc_fun,
+ self._bos_fun,
+ min_spacing=self._min_spacing,
+ wake_loss_multiplier=wlm,
+ ga_kwargs=self._ga_kwargs,
+ output_request=self._output_request,
+ ws_bins=self._ws_bins,
+ wd_bins=self._wd_bins,
+ excl_dict=self._excl_dict,
+ inclusion_mask=gid_incl_mask,
+ area_filter_kernel=afk,
+ min_area=self._min_area,
+ resolution=self._resolution,
+ excl_area=self._excl_area,
+ data_layers=self._data_layers,
+ slice_lookup=slice_lookup,
+ eos_mult_baseline_cap_mw=ebc,
+ prior_meta=prior_meta,
+ gid_map=self._gid_map,
+ bias_correct=i_bc,
+ gids=gid,
+ pre_loaded_data=pre_loaded_data)
+ self._outputs.update(si)
+ else:
+ self._outputs=self.run_parallel(max_workers=max_workers)
+
+ ifout_fpathisnotNone:
+ out_fpath=self.save_outputs(out_fpath)
+
+ returnout_fpath
[docs]classGeneticAlgorithm():
+"""a simple genetic algorithm used to select bespoke turbine locations
+ """
+
+ def__init__(self,bits,bounds,variable_type,objective_function,
+ max_generation=100,population_size=0,crossover_rate=0.1,
+ mutation_rate=0.01,tol=1E-6,convergence_iters=5,
+ max_time=3600):
+"""
+ Parameters
+ ----------
+ bits : array of ints
+ The number of bits assigned to each of the design variables.
+ The number of discretizations for each design variables will be
+ 2^n where n is the number of bits assigned to that variable.
+ bounds : array of tuples
+ The bounds for each design variable. This parameter looks like:
+ np.array([(lower, upper), (lower, upper)...])
+ variable_type : array of strings ('int' or 'float')
+ The type of each design variable (int or float).
+ objective_function : function handle for the objective that is to be
+ minimized. Should take a single variable as an input which is a
+ list/array of the design variables.
+ max_generation : int, optional
+ The maximum number of generations that will be run in the genetic
+ algorithm.
+ population_size : int, optional
+ The population size in the genetic algorithm.
+ crossover_rate : float, optional
+ The probability of crossover for a single bit during the crossover
+ phase of the genetic algorithm.
+ mutation_rate : float, optional
+ The probability of a single bit mutating during the mutation phase
+ of the genetic algorithm.
+ tol : float, optional
+ The absolute tolerance to determine convergence.
+ convergence_iters : int, optional
+ The number of generations to determine convergence.
+ max_time : float
+ The maximum time (in seconds) to run the genetic algorithm.
+ """
+
+ logger.debug('Initializing GeneticAlgorithm...')
+ logger.debug('Minimum convergence iterations: {}'
+ .format(convergence_iters))
+ logger.debug('Max iterations (generations): {}'.format(max_generation))
+ logger.debug('Population size: {}'.format(population_size))
+ logger.debug('Crossover rate: {}'.format(crossover_rate))
+ logger.debug('Mutation rate: {}'.format(mutation_rate))
+ logger.debug('Convergence tolerance: {}'.format(tol))
+ logger.debug('Maximum runtime (in seconds): {}'.format(max_time))
+
+ # inputs
+ self.bits=bits
+ self.bounds=bounds
+ self.variable_type=variable_type
+ self.objective_function=objective_function
+ self.max_generation=max_generation
+ self.population_size=population_size
+ self.crossover_rate=crossover_rate
+ self.mutation_rate=mutation_rate
+ self.tol=tol
+ self.convergence_iters=convergence_iters
+ self.max_time=max_time
+
+ # internal variables, you could output some of this info if you wanted
+ self.design_variables=np.array([])# the desgin variables as they
+ # are passed into self.objective function
+ self.nbits=0# the total number of bits in each chromosome
+ self.nvars=0# the total number of design variables
+ self.parent_population=np.array([])# 2D array containing all of the
+ # parent individuals
+ self.offspring_population=np.array([])# 2D array containing all of
+ # the offspring individuals
+ self.parent_fitness=np.array([])# array containing all of the
+ # parent fitnesses
+ self.offspring_fitness=np.array([])# array containing all of the
+ # offspring fitnesses
+ self.discretized_variables={}# a dict of arrays containing all of
+ # the discretized design variable
+
+ # outputs
+ self.solution_history=np.array([])
+ self.optimized_function_value=0.0
+ self.optimized_design_variables=np.array([])
+
+ self.initialize_design_variables()
+ self.initialize_bits()
+ ifself.population_size%2==1:
+ self.population_size+=1
+ self.initialize_population()
+ self.initialize_fitness()
+
+ ifself.population_size>5:
+ n=5
+ else:
+ n=self.population_size
+ logger.debug('The first few parent individuals are: {}'
+ .format(self.parent_population[0:n]))
+ logger.debug('The first few parent fitness values are: {}'
+ .format(self.parent_fitness[0:n]))
+
+
[docs]definitialize_design_variables(self):
+"""initialize the design variables from the randomly initialized
+ population
+ """
+ # determine the number of design variables and initialize
+ self.nvars=len(self.variable_type)
+ self.design_variables=np.zeros(self.nvars)
+ float_ind=0
+ foriinrange(self.nvars):
+ ifself.variable_type[i]=="float":
+ ndiscretizations=2**self.bits[i]
+ self.discretized_variables["float_var%s"%float_ind]= \
+ np.linspace(self.bounds[i][0],self.bounds[i][1],
+ ndiscretizations)
+ float_ind+=1
+
+
[docs]definitialize_bits(self):
+"""determine the total number of bits"""
+ # determine the total number of bits
+ foriinrange(self.nvars):
+ ifself.variable_type[i]=="int":
+ int_range=self.bounds[i][1]-self.bounds[i][0]
+ int_bits=int(np.ceil(log(int_range,2)))
+ self.bits[i]=int_bits
+ self.nbits+=self.bits[i]
+
+
[docs]definitialize_population(self):
+"""randomly initialize the parent and offspring populations"""
+ all_bits_on=np.ones((1,self.nbits))
+ random_bits_on=np.random.randint(
+ 0,high=2,size=(self.population_size-1,self.nbits)
+ )
+ self.parent_population=np.r_[all_bits_on,random_bits_on]
+ self.offspring_population=np.zeros_like(self.parent_population)
+
+
[docs]definitialize_fitness(self):
+"""initialize the fitness of member of the parent population"""
+ # initialize the fitness arrays
+ self.parent_fitness=np.zeros(self.population_size)
+ self.offspring_fitness=np.zeros(self.population_size)
+
+ # initialize fitness of the parent population
+ foriinrange(self.population_size):
+ self.chromosome_2_variables(self.parent_population[i])
+ self.parent_fitness[i]= \
+ self.objective_function(self.design_variables)
[docs]defcrossover(self):
+"""perform crossover between individual parents"""
+ self.offspring_population[:,:]=self.parent_population[:,:]
+
+ # mate conscutive pairs of parents (0, 1), (2, 3), ...
+ # The population is shuffled so this does not need to be randomized
+ foriinrange(int(self.population_size/2)):
+ # trade bits in the offspring
+ crossover_arr=np.random.rand(self.nbits)
+ forjinrange(self.nbits):
+ ifcrossover_arr[j]<self.crossover_rate:
+ self.offspring_population[2*i][j], \
+ self.offspring_population[2*i+1][j]= \
+ self.offspring_population[2*i+1][j], \
+ self.offspring_population[2*i][j]
+
+
[docs]defmutate(self):
+"""randomly mutate bits of each chromosome"""
+ foriinrange(int(self.population_size)):
+ # mutate bits in the offspring
+ mutate_arr=np.random.rand(self.nbits)
+ forjinrange(self.nbits):
+ ifmutate_arr[j]<self.mutation_rate:
+ self.offspring_population[i][j]= \
+ (self.offspring_population[i][j]+1)%2
+
+
[docs]defoptimize_ga(self):
+"""run the genetic algorithm"""
+
+ converged=False
+ ngens=1
+ generation=1
+ difference=self.tol*10000.0
+ self.solution_history=np.zeros(self.max_generation+1)
+ self.solution_history[0]=np.min(self.parent_fitness)
+
+ run_time=0.0
+ start_time=time.time()
+ whileconvergedisFalseandngens<self.max_generationand \
+ run_time<self.max_time:
+ self.crossover()
+ self.mutate()
+ # determine fitness of offspring
+ foriinrange(self.population_size):
+ self.chromosome_2_variables(self.offspring_population[i])
+ self.offspring_fitness[i]= \
+ self.objective_function(self.design_variables)
+
+ # rank the total population from best to worst
+ total_fitness=np.append(self.parent_fitness,
+ self.offspring_fitness)
+ ranked_fitness= \
+ np.argsort(total_fitness)[0:int(self.population_size)]
+
+ total_population= \
+ np.vstack([self.parent_population,self.offspring_population])
+ self.parent_population[:,:]=total_population[ranked_fitness,:]
+ self.parent_fitness[:]=total_fitness[ranked_fitness]
+
+ # store solution history and wrap up generation
+ self.solution_history[generation]=np.min(self.parent_fitness)
+
+ ifgeneration>self.convergence_iters:
+ difference= \
+ self.solution_history[generation-self.convergence_iters]\
+ -self.solution_history[generation]
+ else:
+ difference=1000
+ ifabs(difference)<=self.tol:
+ converged=True
+
+ # shuffle up the order of the population
+ shuffle_order=np.arange(1,self.population_size)
+ np.random.shuffle(shuffle_order)
+ shuffle_order=np.append([0],shuffle_order)
+ self.parent_population=self.parent_population[shuffle_order]
+ self.parent_fitness=self.parent_fitness[shuffle_order]
+
+ generation+=1
+ ngens+=1
+
+ run_time=time.time()-start_time
+
+ # Assign final outputs
+ self.solution_history=self.solution_history[0:ngens]
+ self.optimized_function_value=np.min(self.parent_fitness)
+ self.chromosome_2_variables(
+ self.parent_population[np.argmin(self.parent_fitness)])
+ self.optimized_design_variables=self.design_variables
+
+ logger.debug('The GA ran for this many generations: {}'
+ .format(ngens))
+ logger.debug('The GA ran for this many seconds: {:.3f}'
+ .format(run_time))
+ logger.debug('The optimized function value was: {:.3e}'
+ .format(self.optimized_function_value))
+ logger.debug('The optimal design variables were: {}'
+ .format(self.optimized_design_variables))
[docs]defclear(self):
+"""Reset the packing algorithm by clearing the x and y turbine arrays
+ """
+ self.turbine_x=np.array([])
+ self.turbine_y=np.array([])
+
+
+
[docs]defsmallest_area_with_tiebreakers(g):
+"""_summary_
+
+ This function helps break ties in the area of two different
+ geometries using their exterior coordinate values.
+
+ Parameters
+ ----------
+ g : _type_
+ A geometry object with an `area` and an
+ `exterior.coords` coords attribute.
+
+ Returns
+ -------
+ tuple
+ Tuple with the following elements:
+ - area of the geometry
+ - minimum exterior coordinate (southwest)
+ - maximum exterior coordinate (northeast)
+ """
+ returng.area,min(g.exterior.coords),max(g.exterior.coords)
[docs]defnone_until_optimized(func):
+"""Decorator that returns None until `PlaceTurbines` is optimized.
+
+ Meant for exclusive use in `PlaceTurbines` and its subclasses.
+ `PlaceTurbines` is considered optimized when its
+ `optimized_design_variables` attribute is not `None`.
+
+ Parameters
+ ----------
+ func : callable
+ A callable function that should return `None` until
+ `PlaceTurbines` is optimized.
+
+ Returns
+ -------
+ callable
+ New function that returns `None` until `PlaceTurbines` is
+ optimized.
+ """
+
+ def_func(pt):
+"""Wrapper to return `None` if `PlaceTurbines` is not optimized"""
+ ifpt.optimized_design_variablesisNone:
+ return
+ returnfunc(pt)
+ return_func
+
+
+
[docs]classPlaceTurbines:
+"""Framework for optimizing turbine locations for site specific
+ exclusions, wind resources, and objective
+ """
+
+ def__init__(self,wind_plant,objective_function,
+ capital_cost_function,
+ fixed_operating_cost_function,
+ variable_operating_cost_function,
+ balance_of_system_cost_function,
+ include_mask,pixel_side_length,min_spacing,
+ wake_loss_multiplier=1):
+"""
+ Parameters
+ ----------
+ wind_plant : WindPowerPD
+ wind plant object to analyze wind plant performance. This
+ object should have everything in the plant defined, such
+ that only the turbine coordinates and plant capacity need to
+ be defined during the optimization.
+ objective_function : str
+ The objective function of the optimization as a string,
+ should return the objective to be minimized during layout
+ optimization. Variables available are:
+
+ - ``n_turbines``: the number of turbines
+ - ``system_capacity``: wind plant capacity
+ - ``aep``: annual energy production
+ - ``avg_sl_dist_to_center_m``: Average straight-line
+ distance to the supply curve point center from all
+ turbine locations (in m). Useful for computing plant
+ BOS costs.
+ - ``avg_sl_dist_to_medoid_m``: Average straight-line
+ distance to the medoid of all turbine locations
+ (in m). Useful for computing plant BOS costs.
+ - ``nn_conn_dist_m``: Total BOS connection distance
+ using nearest-neighbor connections. This variable is
+ only available for the
+ ``balance_of_system_cost_function`` equation.
+ - ``fixed_charge_rate``: user input fixed_charge_rate if
+ included as part of the sam system config.
+ - ``capital_cost``: plant capital cost as evaluated
+ by `capital_cost_function`
+ - ``fixed_operating_cost``: plant fixed annual operating
+ cost as evaluated by `fixed_operating_cost_function`
+ - ``variable_operating_cost``: plant variable annual
+ operating cost as evaluated by
+ `variable_operating_cost_function`
+ - ``balance_of_system_cost``: plant balance of system
+ cost as evaluated by `balance_of_system_cost_function`
+ - ``self.wind_plant``: the SAM wind plant object,
+ through which all SAM variables can be accessed
+
+ capital_cost_function : str
+ The plant capital cost function as a string, must return the
+ total capital cost in $. Has access to the same variables as
+ the objective_function.
+ fixed_operating_cost_function : str
+ The plant annual fixed operating cost function as a string,
+ must return the fixed operating cost in $/year. Has access
+ to the same variables as the objective_function.
+ variable_operating_cost_function : str
+ The plant annual variable operating cost function as a
+ string, must return the variable operating cost in $/kWh.
+ Has access to the same variables as the objective_function.
+ You can set this to "0" to effectively ignore variable
+ operating costs.
+ balance_of_system_cost_function : str
+ The plant balance-of-system cost function as a string, must
+ return the variable operating cost in $. Has access to the
+ same variables as the objective_function. You can set this
+ to "0" to effectively ignore balance-of-system costs.
+ include_mask : np.ndarray
+ Supply curve point 2D inclusion mask where included pixels
+ are set to 1 and excluded pixels are set to 0.
+ pixel_side_length : int
+ Side length (m) of a single pixel of the `include_mask`.
+ min_spacing : float
+ The minimum spacing between turbines (in meters).
+ wake_loss_multiplier : float, optional
+ A multiplier used to scale the annual energy lost due to
+ wake losses. **IMPORTANT**: This multiplier will ONLY be
+ applied during the optimization process and will NOT be
+ come through in output values such as aep, any of the cost
+ functions, or even the output objective.
+ """
+
+ # inputs
+ self.wind_plant=wind_plant
+
+ self.capital_cost_function=capital_cost_function
+ self.fixed_operating_cost_function=fixed_operating_cost_function
+ self.variable_operating_cost_function= \
+ variable_operating_cost_function
+ self.balance_of_system_cost_function=balance_of_system_cost_function
+
+ self.objective_function=objective_function
+ self.include_mask=include_mask
+ self.pixel_side_length=pixel_side_length
+ self.min_spacing=min_spacing
+ self.wake_loss_multiplier=wake_loss_multiplier
+
+ # internal variables
+ self.nrows,self.ncols=np.shape(include_mask)
+ self.x_locations=np.array([])
+ self.y_locations=np.array([])
+ self.turbine_capacity= \
+ np.max(self.wind_plant.
+ sam_sys_inputs["wind_turbine_powercurve_powerout"])
+ self.full_polygons=None
+ self.packing_polygons=None
+ self.optimized_design_variables=None
+ self.safe_polygons=None
+ self._optimized_nn_conn_dist_m=None
+
+ self.ILLEGAL=('import ','os.','sys.','.__','__.','eval','exec')
+ self._preflight(self.objective_function)
+ self._preflight(self.capital_cost_function)
+ self._preflight(self.fixed_operating_cost_function)
+ self._preflight(self.variable_operating_cost_function)
+ self._preflight(self.balance_of_system_cost_function)
+
+ def_preflight(self,eqn):
+"""Run preflight checks on the equation string."""
+ forsubstrinself.ILLEGAL:
+ ifsubstrinstr(eqn):
+ msg=('Will not evaluate string which contains "{}": {}'
+ .format(substr,eqn))
+ raiseValueError(msg)
+
+
[docs]defplace_turbines(self,**kwargs):
+"""Define bespoke wind plant turbine layouts.
+
+ Run all functions to define bespoke wind plant turbine layouts.
+
+ Parameters
+ ----------
+ **kwargs
+ Keyword arguments to pass to GA initialization.
+
+ See Also
+ --------
+ :class:`~reV.bespoke.gradient_free.GeneticAlgorithm` : GA Algorithm.
+ """
+ self.define_exclusions()
+ self.initialize_packing()
+ self.optimize(**kwargs)
+
+
[docs]defcapital_cost_per_kw(self,capacity_mw):
+"""Capital cost function ($ per kW) evaluated for a given capacity.
+
+ The capacity will be adjusted to be an exact multiple of the
+ turbine rating in order to yield an integer number of
+ turbines.
+
+ Parameters
+ ----------
+ capacity_mw : float
+ The desired capacity (MW) to sample the cost curve at. Note
+ as mentioned above, the capacity will be adjusted to be an
+ exact multiple of the turbine rating in order to yield an
+ integer number of turbines. For best results, set this
+ value to be an integer multiple of the turbine rating.
+
+ Returns
+ -------
+ capital_cost : float
+ Capital cost ($ per kW) for the (adjusted) plant capacity.
+ """
+
+ fixed_charge_rate=self.fixed_charge_rate
+ avg_sl_dist_to_center_m=self.avg_sl_dist_to_center_m
+ n_turbines=int(round(capacity_mw*1e3/self.turbine_capacity))
+ system_capacity=n_turbines*self.turbine_capacity
+ mult=self.wind_plant.sam_sys_inputs.get(
+ 'capital_cost_multiplier',1)/system_capacity
+ returneval(self.capital_cost_function,globals(),locals())*mult
+
+ @property
+ deffixed_charge_rate(self):
+"""Fixed charge rate if input to the SAM WindPowerPD object, None if
+ not found in inputs."""
+ returnself.wind_plant.sam_sys_inputs.get("fixed_charge_rate",None)
+
+ @property
+ @none_until_optimized
+ defturbine_x(self):
+"""This is the final optimized turbine x locations (m)"""
+ returnself.x_locations[self.optimized_design_variables]
+
+ @property
+ @none_until_optimized
+ defturbine_y(self):
+"""This is the final optimized turbine y locations (m)"""
+ returnself.y_locations[self.optimized_design_variables]
+
+ @property
+ @none_until_optimized
+ defavg_sl_dist_to_center_m(self):
+"""This is the final avg straight line distance to center (m)"""
+ returnself._avg_sl_dist_to_cent(self.turbine_x,self.turbine_y)
+
+ @property
+ @none_until_optimized
+ defavg_sl_dist_to_medoid_m(self):
+"""This is the final avg straight line distance to turb medoid (m)"""
+ returnself._avg_sl_dist_to_med(self.turbine_x,self.turbine_y)
+
+ @property
+ @none_until_optimized
+ defnn_conn_dist_m(self):
+"""This is the final avg straight line distance to turb medoid (m)"""
+ ifself._optimized_nn_conn_dist_misNone:
+ self._optimized_nn_conn_dist_m=_compute_nn_conn_dist(
+ self.turbine_x,self.turbine_y
+ )
+ returnself._optimized_nn_conn_dist_m
+
+ @property
+ @none_until_optimized
+ defnturbs(self):
+"""This is the final optimized number of turbines"""
+ returnnp.sum(self.optimized_design_variables)
+
+ @property
+ @none_until_optimized
+ defcapacity(self):
+"""This is the final optimized plant nameplate capacity (kW)"""
+ returnself.turbine_capacity*self.nturbs
+
+ @property
+ @none_until_optimized
+ defconvex_hull(self):
+"""This is the convex hull of the turbine locations"""
+ turbines=MultiPoint([Point(x,y)
+ forx,yinzip(self.turbine_x,
+ self.turbine_y)])
+ returnturbines.convex_hull
+
+ @property
+ @none_until_optimized
+ defarea(self):
+"""This is the area available for wind turbine placement (km^2)"""
+ returnself.full_polygons.area/1e6
+
+ @property
+ @none_until_optimized
+ defconvex_hull_area(self):
+"""This is the area of the convex hull of the turbines (km^2)"""
+ returnself.convex_hull.area/1e6
+
+ @property
+ @none_until_optimized
+ deffull_cell_area(self):
+"""This is the full non-excluded area available for wind turbine
+ placement (km^2)"""
+ nx,ny=np.shape(self.include_mask)
+ side_x=nx*self.pixel_side_length
+ side_y=ny*self.pixel_side_length
+ returnside_x*side_y/1e6
+
+ @property
+ @none_until_optimized
+ defcapacity_density(self):
+"""This is the optimized capacity density of the wind plant
+ defined with the area available after removing the exclusions
+ (MW/km2)"""
+ ifself.full_polygonsisNoneorself.capacityisNone:
+ return
+
+ ifself.area!=0.0:
+ returnself.capacity/self.area/1E3
+
+ return0.0
+
+ @property
+ @none_until_optimized
+ defconvex_hull_capacity_density(self):
+"""This is the optimized capacity density of the wind plant
+ defined with the convex hull area of the turbine layout (MW/km2)"""
+ ifself.convex_hull_area!=0.0:
+ returnself.capacity/self.convex_hull_area/1E3
+ return0.0
+
+ @property
+ @none_until_optimized
+ deffull_cell_capacity_density(self):
+"""This is the optimized capacity density of the wind plant
+ defined with the full non-excluded area of the turbine layout (MW/km2)
+ """
+ ifself.full_cell_area!=0.0:
+ returnself.capacity/self.full_cell_area/1E3
+ return0.0
+
+ @property
+ @none_until_optimized
+ defaep(self):
+"""This is the annual energy production of the optimized plant (kWh)"""
+ ifself.nturbs<=0:
+ return0
+
+ self.wind_plant["wind_farm_xCoordinates"]=self.turbine_x
+ self.wind_plant["wind_farm_yCoordinates"]=self.turbine_y
+ self.wind_plant["system_capacity"]=self.capacity
+ self.wind_plant.assign_inputs()
+ self.wind_plant.execute()
+ returnself.wind_plant.annual_energy()
+
+ # pylint: disable=W0641,W0123
+ @property
+ @none_until_optimized
+ defcapital_cost(self):
+"""This is the capital cost of the optimized plant ($)"""
+ fixed_charge_rate=self.fixed_charge_rate
+ n_turbines=self.nturbs
+ system_capacity=self.capacity
+ aep=self.aep
+ avg_sl_dist_to_center_m=self.avg_sl_dist_to_center_m
+ avg_sl_dist_to_medoid_m=self.avg_sl_dist_to_medoid_m
+ nn_conn_dist_m=self.nn_conn_dist_m
+
+ mult=self.wind_plant.sam_sys_inputs.get(
+ 'capital_cost_multiplier',1)
+ returneval(self.capital_cost_function,globals(),locals())*mult
+
+ # pylint: disable=W0641,W0123
+ @property
+ @none_until_optimized
+ deffixed_operating_cost(self):
+"""This is the annual fixed operating cost of the
+ optimized plant ($/year)"""
+ fixed_charge_rate=self.fixed_charge_rate
+ n_turbines=self.nturbs
+ system_capacity=self.capacity
+ aep=self.aep
+ avg_sl_dist_to_center_m=self.avg_sl_dist_to_center_m
+ avg_sl_dist_to_medoid_m=self.avg_sl_dist_to_medoid_m
+ nn_conn_dist_m=self.nn_conn_dist_m
+
+ mult=self.wind_plant.sam_sys_inputs.get(
+ 'fixed_operating_cost_multiplier',1)
+ returneval(self.fixed_operating_cost_function,
+ globals(),locals())*mult
+
+ # pylint: disable=W0641,W0123
+ @property
+ @none_until_optimized
+ defvariable_operating_cost(self):
+"""This is the annual variable operating cost of the
+ optimized plant ($/kWh)"""
+ fixed_charge_rate=self.fixed_charge_rate
+ n_turbines=self.nturbs
+ system_capacity=self.capacity
+ aep=self.aep
+ avg_sl_dist_to_center_m=self.avg_sl_dist_to_center_m
+ avg_sl_dist_to_medoid_m=self.avg_sl_dist_to_medoid_m
+ nn_conn_dist_m=self.nn_conn_dist_m
+
+ mult=self.wind_plant.sam_sys_inputs.get(
+ 'variable_operating_cost_multiplier',1)
+ returneval(self.variable_operating_cost_function,
+ globals(),locals())*mult
+
+ @property
+ @none_until_optimized
+ defbalance_of_system_cost(self):
+"""This is the balance of system cost of the optimized plant ($)"""
+ fixed_charge_rate=self.fixed_charge_rate
+ n_turbines=self.nturbs
+ system_capacity=self.capacity
+ aep=self.aep
+ avg_sl_dist_to_center_m=self.avg_sl_dist_to_center_m
+ avg_sl_dist_to_medoid_m=self.avg_sl_dist_to_medoid_m
+ nn_conn_dist_m=self.nn_conn_dist_m
+
+ mult=self.wind_plant.sam_sys_inputs.get(
+ 'balance_of_system_cost_multiplier',1)
+ returneval(self.balance_of_system_cost_function,
+ globals(),locals())*mult
+
+ # pylint: disable=W0641,W0123
+ @property
+ @none_until_optimized
+ defobjective(self):
+"""This is the optimized objective function value"""
+ fixed_charge_rate=self.fixed_charge_rate
+ n_turbines=self.nturbs
+ system_capacity=self.capacity
+ aep=self.aep
+ capital_cost=self.capital_cost
+ fixed_operating_cost=self.fixed_operating_cost
+ variable_operating_cost=self.variable_operating_cost
+ balance_of_system_cost=self.balance_of_system_cost
+ avg_sl_dist_to_center_m=self.avg_sl_dist_to_center_m
+ avg_sl_dist_to_medoid_m=self.avg_sl_dist_to_medoid_m
+ nn_conn_dist_m=self.nn_conn_dist_m
+
+ returneval(self.objective_function,globals(),locals())
+# -*- coding: utf-8 -*-
+"""
+functions to plot turbine layouts and boundary polygons
+"""
+importnumpyasnp
+importmatplotlib.pyplotasplt
+
+
+
[docs]defget_xy(A):
+"""separate polygon exterior coordinates to x and y
+
+ Parameters
+ ----------
+ A : Polygon.exteroir.coords
+ Exterior coordinates from a shapely Polygon
+
+ Outputs
+ ----------
+ x, y : array
+ Boundary polygon x and y coordinates
+ """
+ x=np.zeros(len(A))
+ y=np.zeros(len(A))
+ fori,_inenumerate(A):
+ x[i]=A[i][0]
+ y[i]=A[i][1]
+ returnx,y
+
+
+
[docs]defplot_poly(geom,ax=None,color="black",linestyle="--",linewidth=0.5):
+"""plot the wind plant boundaries
+
+ Parameters
+ ----------
+ geom : Polygon | MultiPolygon
+ The shapely.Polygon or shapely.MultiPolygon that define the wind
+ plant boundary(ies).
+ ax : :py:class:`matplotlib.pyplot.axes`, optional
+ The figure axes on which the wind rose is plotted.
+ Defaults to :obj:`None`.
+ color : string, optional
+ The color for the wind plant boundaries
+ linestyle : string, optional
+ Style to plot the boundary lines
+ linewidth : float, optional
+ The width of the boundary lines
+ """
+ ifaxisNone:
+ _,ax=plt.subplots()
+
+ ifgeom.type=='Polygon':
+ exterior_coords=geom.exterior.coords[:]
+ x,y=get_xy(exterior_coords)
+ ax.fill(x,y,color="C0",alpha=0.25)
+ ax.plot(x,y,color=color,linestyle=linestyle,linewidth=linewidth)
+
+ forinterioringeom.interiors:
+ interior_coords=interior.coords[:]
+ x,y=get_xy(interior_coords)
+ ax.fill(x,y,color="white",alpha=1.0)
+ ax.plot(x,y,"--k",linewidth=0.5)
+
+ elifgeom.type=='MultiPolygon':
+
+ forpartingeom:
+ exterior_coords=part.exterior.coords[:]
+ x,y=get_xy(exterior_coords)
+ ax.fill(x,y,color="C0",alpha=0.25)
+ ax.plot(x,y,color=color,linestyle=linestyle,
+ linewidth=linewidth)
+
+ forinteriorinpart.interiors:
+ interior_coords=interior.coords[:]
+ x,y=get_xy(interior_coords)
+ ax.fill(x,y,color="white",alpha=1.0)
+ ax.plot(x,y,"--k",linewidth=0.5)
+ returnax
+
+
+
[docs]defplot_turbines(x,y,r,ax=None,color="C0",nums=False):
+"""plot wind turbine locations
+
+ Parameters
+ ----------
+ x, y : array
+ Wind turbine x and y locations
+ r : float
+ Wind turbine radius
+ ax :py:class:`matplotlib.pyplot.axes`, optional
+ The figure axes on which the wind rose is plotted.
+ Defaults to :obj:`None`.
+ color : string, optional
+ The color for the wind plant boundaries
+ nums : bool, optional
+ Option to show the turbine numbers next to each turbine
+ """
+ # Set up figure
+ ifaxisNone:
+ _,ax=plt.subplots()
+
+ n=len(x)
+ foriinrange(n):
+ t=plt.Circle((x[i],y[i]),r,color=color)
+ ax.add_patch(t)
+ ifnumsisTrue:
+ ax.text(x[i],y[i],"%s"%(i+1))
+
+ returnax
+
+
+
[docs]defplot_windrose(wind_directions,wind_speeds,wind_frequencies,ax=None,
+ colors=None):
+"""plot windrose
+
+ Parameters
+ ----------
+ wind_directions : 1D array
+ Wind direction samples
+ wind_speeds : 1D array
+ Wind speed samples
+ wind_frequencies : 2D array
+ Frequency of wind direction and speed samples
+ ax :py:class:`matplotlib.pyplot.axes`, optional
+ The figure axes on which the wind rose is plotted.
+ Defaults to :obj:`None`.
+ color : array, optional
+ The color for the different wind speed bins
+ """
+ ifaxisNone:
+ _,ax=plt.subplots(subplot_kw=dict(polar=True))
+
+ ndirs=len(wind_directions)
+ nspeeds=len(wind_speeds)
+
+ ifcolorsisNone:
+ colors=[]
+ foriinrange(nspeeds):
+ colors=np.append(colors,"C%s"%i)
+
+ foriinrange(ndirs):
+ wind_directions[i]=np.deg2rad(90.0-wind_directions[i])
+
+ width=0.8*2*np.pi/len(wind_directions)
+
+ foriinrange(ndirs):
+ bottom=0.0
+ forjinrange(nspeeds):
+ ifi==0:
+ ifj<nspeeds-1:
+ ax.bar(wind_directions[i],wind_frequencies[j,i],
+ bottom=bottom,width=width,edgecolor="black",
+ color=[colors[j]],
+ label="%s-%s m/s"%(int(wind_speeds[j]),
+ int(wind_speeds[j+1]))
+ )
+ else:
+ ax.bar(wind_directions[i],wind_frequencies[j,i],
+ bottom=bottom,width=width,edgecolor="black",
+ color=[colors[j]],
+ label="%s+ m/s"%int(wind_speeds[j])
+ )
+ else:
+ ax.bar(wind_directions[i],wind_frequencies[j,i],
+ bottom=bottom,width=width,edgecolor="black",
+ color=[colors[j]])
+ bottom=bottom+wind_frequencies[j,i]
+
+ ax.legend(bbox_to_anchor=(1.3,1),fontsize=10)
+ pi=np.pi
+ ax.set_xticks((0,pi/4,pi/2,3*pi/4,pi,5*pi/4,
+ 3*pi/2,7*pi/4))
+ ax.set_xticklabels(("E","NE","N","NW","W","SW","S","SE"),
+ fontsize=10)
+ plt.yticks(fontsize=10)
+
+ plt.subplots_adjust(left=0.0,right=1.0,top=0.9,bottom=0.1)
+
+ returnax
[docs]classAnalysisConfig(BaseConfig):
+"""Base analysis config (generation, lcoe, etc...)."""
+
+ NAME=None
+
+ def__init__(self,config,run_preflight=True,check_keys=True):
+"""
+ Parameters
+ ----------
+ config : str | dict
+ File path to config json (str), serialized json object (str),
+ or dictionary with pre-extracted config.
+ run_preflight : bool, optional
+ Flag to run or disable preflight checks, by default True
+ check_keys : bool, optional
+ Flag to check config keys against Class properties, by default True
+ """
+ super().__init__(config,check_keys=check_keys)
+
+ self._analysis_years=None
+ self._ec=None
+ self.dirout=self.config_dir
+ self.__config_fn=config
+
+ self._preflight()
+
+ ifrun_preflight:
+ self._analysis_config_preflight()
+
+ def_analysis_config_preflight(self):
+"""Check for required config blocks"""
+
+ if'execution_control'notinself:
+ e='reV config must have "execution_control" block!'
+ logger.error(e)
+ raiseConfigError(e)
+
+ @property
+ defanalysis_years(self):
+"""Get the analysis years.
+
+ Returns
+ -------
+ analysis_years : list
+ List of years to analyze. If this is a single year run, this return
+ value is a single entry list. If no analysis_years are specified,
+ the code will look anticipate a year in the input files.
+ """
+
+ ifself._analysis_yearsisNone:
+ self._analysis_years=self.get('analysis_years',[None])
+ ifnotisinstance(self._analysis_years,list):
+ self._analysis_years=[self._analysis_years]
+
+ ifself._analysis_years[0]isNone:
+ warn('Years may not have been specified, may default '
+ 'to available years in inputs files.',ConfigWarning)
+
+ returnself._analysis_years
+
+ @property
+ deflog_directory(self):
+"""Get the logging directory, look for key "log_directory" in the
+ config.
+ Returns
+ -------
+ log_directory : str
+ Target path for reV log files.
+ """
+ returnself.get('log_directory','./logs/')
+
+ @property
+ defexecution_control(self):
+"""Get the execution control object.
+
+ Returns
+ -------
+ _ec : BaseExecutionConfig | EagleConfig
+ reV execution config object specific to the execution_control
+ option.
+ """
+ ifself._ecisNone:
+ ec=self['execution_control']
+ # static map of avail execution options with corresponding classes
+ ec_config_types={'local':BaseExecutionConfig,
+ 'slurm':SlurmConfig,
+ 'eagle':SlurmConfig,
+ 'kestrel':SlurmConfig,
+ }
+ if'option'inec:
+ try:
+ # Try setting the attribute to the appropriate exec option
+ self._ec=ec_config_types[ec['option'].lower()](ec)
+ exceptKeyErrorasexc:
+ # Option not found
+ msg=('Execution control option not '
+ 'recognized: "{}". '
+ 'Available options are: {}.'
+ .format(ec['option'].lower(),
+ list(ec_config_types.keys())))
+ raiseConfigError(msg)fromexc
+ else:
+ # option not specified, default to a base execution (local)
+ warn('Execution control option not specified. '
+ 'Defaulting to a local run.')
+ self._ec=BaseExecutionConfig(ec)
+ returnself._ec
+
+ @property
+ defname(self):
+"""Get the job name, defaults to the output directory name.
+ Returns
+ -------
+ _name : str
+ reV job name.
+ """
+
+ ifself._nameisNone:
+
+ # name defaults to base directory name
+ self._name=os.path.basename(os.path.normpath(self.dirout))
+
+ # collect name is simple, will be added to what is being collected
+ ifself.NAME==ModuleName.COLLECT:
+ self._name=self.NAME
+
+ # Analysis job name tag (helps ensure unique job name)
+ elifself.NAMEisnotNone:
+ self._name+='_{}'.format(self.NAME)
+
+ # Throw warning if user still has 'name' key in config
+ ifself.get('name')isnotNone:
+ msg=("Specifying a job name using config key 'name' is "
+ "deprecated. Job names are now inferred from the run "
+ "directory name. To silence this warning, remove "
+ "the 'name' key from the following config file: {!r}'"
+ .format(self.__config_fn))
+ logger.warning(msg)
+ warn(reVDeprecationWarning(msg))
+
+ returnself._name
[docs]classBaseConfig(dict):
+"""Base class for configuration frameworks."""
+
+ REQUIREMENTS=()
+"""Required keys for config"""
+
+ STR_REP={'REVDIR':REVDIR,
+ 'TESTDATADIR':TESTDATADIR}
+"""Mapping of config inputs (keys) to desired replacements (values) in
+ addition to relative file paths as demarcated by ./ and ../"""
+
+ def__init__(self,config,check_keys=True,perform_str_rep=True):
+"""
+ Parameters
+ ----------
+ config : str | dict
+ File path to config json (str), serialized json object (str),
+ or dictionary with pre-extracted config.
+ check_keys : bool, optional
+ Flag to check config keys against Class properties, by default True
+ perform_str_rep : bool
+ Flag to perform string replacement for REVDIR, TESTDATADIR, and ./
+ """
+
+ # str_rep is a mapping of config strings to replace with real values
+ self._perform_str_rep=perform_str_rep
+ self._name=None
+ self._config_dir=None
+ self._log_level=None
+ self._parse_config(config)
+
+ self._preflight()
+
+ self._keys=self._get_properties()
+ ifcheck_keys:
+ self._check_keys()
+
+ @property
+ defconfig_dir(self):
+"""Get the directory that the config file is in.
+
+ Returns
+ -------
+ config_dir : str
+ Directory path that the config file is in.
+ """
+ returnself._config_dir
+
+ @property
+ defconfig_keys(self):
+"""
+ List of valid config keys
+
+ Returns
+ -------
+ list
+ """
+ returnself._keys
+
+ @property
+ deflog_level(self):
+"""Get user-specified "log_level" (DEBUG, INFO, WARNING, etc...).
+
+ Returns
+ -------
+ log_level : int
+ Python logging module level (integer format) corresponding to the
+ config-specified log level string.
+ """
+
+ ifself._log_levelisNone:
+ levels={'DEBUG':logging.DEBUG,
+ 'INFO':logging.INFO,
+ 'WARNING':logging.WARNING,
+ 'ERROR':logging.ERROR,
+ 'CRITICAL':logging.CRITICAL,
+ }
+
+ x=str(self.get('log_level','INFO'))
+ self._log_level=levels[x.upper()]
+
+ returnself._log_level
+
+ @property
+ defname(self):
+"""Get the job name, defaults to 'rev'.
+
+ Returns
+ -------
+ name : str
+ reV job name.
+ """
+ returnself._nameor'rev'
+
+ def_preflight(self):
+"""Run a preflight check on the config."""
+ if'project_control'inself:
+ msg=('config "project_control" block is no '
+ 'longer used. All project control keys should be placed at '
+ 'the top config level.')
+ logger.error(msg)
+ raiseConfigError(msg)
+
+ missing=[]
+ forreqinself.REQUIREMENTS:
+ ifreqnotinself:
+ missing.append(req)
+
+ ifany(missing):
+ e=('{} missing the following keys: {}'
+ .format(self.__class__.__name__,missing))
+ logger.error(e)
+ raiseConfigError(e)
+
+ @classmethod
+ def_get_properties(cls):
+"""
+ Get all class properties
+ Used to check against config keys
+
+ Returns
+ -------
+ properties : list
+ List of class properties, each of which should represent a valid
+ config key/entry
+ """
+ returnget_class_properties(cls)
+
+ def_check_keys(self):
+"""
+ Check on config keys to ensure they match available
+ properties
+ """
+ forkeyinself.keys():
+ ifisinstance(key,str)andkeynotinself._keys:
+ msg=('{} is not a valid config entry for {}! Must be one of:'
+ '\n{}'.format(key,self.__class__.__name__,self._keys))
+ logger.error(msg)
+ raiseConfigError(msg)
+
+
[docs]defcheck_overwrite_keys(self,primary_key,*overwrite_keys):
+"""
+ Check for overwrite keys and raise a ConfigError if present
+
+ Parameters
+ ----------
+ primary_key : str
+ Primary key that overwrites overwrite_keys, used for error message
+ overwrite_keys : str
+ Key(s) to overwrite
+ """
+ overwrite=[]
+ forkeyinoverwrite_keys:
+ ifkeyinself:
+ overwrite.append(key)
+
+ ifoverwrite:
+ msg=('A value for "{}" was provided which overwrites the '
+ ' following key: "{}", please remove them from the config'
+ .format(primary_key,', '.join(overwrite)))
+ logger.error(msg)
+ raiseConfigError(msg)
+
+ def_parse_config(self,config):
+"""Parse a config input and set appropriate instance attributes.
+
+ Parameters
+ ----------
+ config : str | dict
+ File path to config json (str), serialized json object (str),
+ or dictionary with pre-extracted config.
+ """
+
+ # str is either json file path or serialized json object
+ ifisinstance(config,str):
+ try:
+ # attempt to deserialize JSON-style string
+ config=json.loads(config)
+ exceptjson.JSONDecodeError:
+ self._config_dir=os.path.dirname(unstupify_path(config))
+ self._config_dir+='/'
+ self._config_dir=self._config_dir.replace('\\','/')
+ config=load_config(config)
+
+ # Perform string replacement, save config to self instance
+ ifself._perform_str_rep:
+ config=self.str_replace_and_resolve(config,self.STR_REP)
+
+ self.set_self_dict(config)
+
+
[docs]@staticmethod
+ defcheck_files(flist):
+"""Make sure all files in the input file list exist.
+
+ Parameters
+ ----------
+ flist : list
+ List of files (with paths) to check existance of.
+ """
+ forfinflist:
+ # ignore files that are to be specified using pipeline utils
+ if'PIPELINE'notinos.path.basename(f):
+ ifos.path.exists(f)isFalse:
+ raiseIOError('File does not exist: {}'.format(f))
+
+
[docs]defstr_replace_and_resolve(self,d,str_rep):
+"""Perform a deep string replacement and path resolve in d.
+
+ Parameters
+ ----------
+ d : dict
+ Config dictionary potentially containing strings to replace
+ and/or paths to resolve.
+ str_rep : dict
+ Replacement mapping where keys are strings to search for and
+ values are the new values.
+
+ Returns
+ -------
+ d : dict
+ Config dictionary with updated strings.
+ """
+
+ ifisinstance(d,dict):
+ # go through dict keys and values
+ forkey,valind.items():
+ d[key]=self.str_replace_and_resolve(val,str_rep)
+
+ elifisinstance(d,list):
+ # if the value is also a list, iterate through
+ fori,entryinenumerate(d):
+ d[i]=self.str_replace_and_resolve(entry,str_rep)
+
+ elifisinstance(d,str):
+ # if val is a str, check to see if str replacements apply
+ forold_str,newinstr_rep.items():
+ # old_str is in the value, replace with new value
+ d=d.replace(old_str,new)
+
+ # `resolve_path` is safe to call on any string,
+ # even if it is not a path
+ d=self.resolve_path(d)
+
+ # return updated
+ returnd
+
+
[docs]defset_self_dict(self,dictlike):
+"""Save a dict-like variable as object instance dictionary items.
+
+ Parameters
+ ----------
+ dictlike : dict
+ Python namespace object to set to this dictionary-emulating class.
+ """
+ forkey,valindictlike.items():
+ self[key]=val
+
+
[docs]defresolve_path(self,path):
+"""Resolve a file path represented by the input string.
+
+ This function resolves the input string if it resembles a path.
+ Specifically, the string will be resolved if it starts with
+ "``./``" or "``..``", or it if it contains either "``./``" or
+ "``..``" somewhere in the string body. Otherwise, the string
+ is returned unchanged, so this function *is* safe to call on any
+ string, even ones that do not resemble a path.
+
+ This method delegates the "resolving" logic to
+ :meth:`pathlib.Path.resolve`. This means the path is made
+ absolute, symlinks are resolved, and "``..``" components are
+ eliminated. If the ``path`` input starts with "``./``" or
+ "``..``", it is assumed to be w.r.t the config directory, *not*
+ the run directory.
+
+ Parameters
+ ----------
+ path : str
+ Input file path.
+
+ Returns
+ -------
+ str
+ The resolved path.
+ """
+
+ ifpath.startswith('./'):
+ path=(self.config_dir/Path(path[2:]))
+ elifpath.startswith('..'):
+ path=(self.config_dir/Path(path))
+ elif'./'inpath:# this covers both './' and '../'
+ path=Path(path)
+
+ try:
+ path=path.resolve().as_posix()
+ exceptAttributeError:# `path` is still a `str`
+ pass
+
+ returnpath
[docs]classCurtailment(BaseConfig):
+"""Config for generation curtailment."""
+
+ def__init__(self,curtailment_parameters):
+"""
+ Parameters
+ ----------
+ curtailment_parameters : str | dict
+ Configuration json file (with path) containing curtailment
+ information. Could also be a pre-extracted curtailment config
+ dictionary (the contents of the curtailment json).
+ """
+
+ ifisinstance(curtailment_parameters,str):
+ # received json, extract to dictionary
+ curtailment_parameters=load_config(curtailment_parameters)
+
+ # intialize config object with curtailment parameters
+ super().__init__(curtailment_parameters)
+
+ @property
+ defwind_speed(self):
+"""Get the wind speed threshold below which curtailment is possible.
+
+ Returns
+ -------
+ _wind_speed : float | None
+ Wind speed threshold below which curtailment is possible.
+ """
+ returnself.get('wind_speed',None)
+
+ @property
+ defdawn_dusk(self):
+"""Get the solar zenith angle that signifies dawn and dusk.
+
+ Returns
+ -------
+ _dawn_dusk : float
+ Solar zenith angle at dawn and dusk. Default is nautical, 12
+ degrees below the horizon (sza=102).
+ """
+
+ # preset commonly used dawn/dusk values in solar zenith angles.
+ presets={'nautical':102.0,
+ 'astronomical':108.0,
+ 'civil':96.0}
+
+ # set a default value
+ dd=presets['nautical']
+
+ if'dawn_dusk'inself:
+ ifisinstance(self['dawn_dusk'],str):
+ # Use a pre-set dawn/dusk
+ dd=presets[self['dawn_dusk']]
+
+ ifisinstance(self['dawn_dusk'],(int,float)):
+ # Use an explicit solar zenith angle
+ dd=float(self['dawn_dusk'])
+
+ returndd
+
+ @property
+ defmonths(self):
+"""Get the months during which curtailment is possible (inclusive).
+ This can be overridden by the date_range input.
+
+ Returns
+ -------
+ months : tuple | None
+ Tuple of month integers. These are the months during which
+ curtailment could be in effect. Default is None.
+ """
+ m=self.get('months',None)
+ ifisinstance(m,list):
+ m=tuple(m)
+ returnm
+
+ @property
+ defdate_range(self):
+"""Get the date range tuple (start, end) over which curtailment is
+ possible (inclusive, exclusive) ("MMDD", "MMDD"). This overrides the
+ months input.
+
+ Returns
+ -------
+ date_range : tuple
+ Two-entry tuple of the starting date (inclusive) and ending date
+ (exclusive) over which curtailment is possible. Input format is a
+ zero-padded string: "MMDD".
+ """
+ dr=self.get('date_range',None)
+ ifdrisnotNone:
+ msg='date_range input needs to be a tuple!'
+ assertisinstance(dr,(list,tuple)),msg
+ msg='date_range input needs to have two entries!'
+ assertlen(dr)==2,msg
+ dr=(str(int(dr[0])).zfill(4),str(int(dr[1])).zfill(4))
+
+ returndr
+
+ @property
+ deftemperature(self):
+"""Get the temperature (C) over which curtailment is possible.
+
+ Returns
+ -------
+ temperature : float | NoneType
+ Temperature over which curtailment is possible. Defaults to None.
+ """
+ returnself.get('temperature',None)
+
+ @property
+ defprecipitation(self):
+"""Get the precip rate (mm/hour) under which curtailment is possible.
+
+ Returns
+ -------
+ precipitation : float | NoneType
+ Precipitation rate under which curtailment is possible. This is
+ compared to the WTK resource dataset "precipitationrate_0m" in
+ mm/hour. Defaults to None.
+ """
+ returnself.get('precipitation',None)
+
+ @property
+ defequation(self):
+"""Get an equation-based curtailment scenario.
+
+ Returns
+ -------
+ equation : str
+ A python equation based on other curtailment variables (wind_speed,
+ temperature, precipitation_rate, solar_zenith_angle) that returns
+ a True or False output to signal curtailment.
+ """
+ eq=self.get('equation',None)
+ ifisinstance(eq,str):
+ check_eval_str(eq)
+ returneq
+
+ @property
+ defprobability(self):
+"""Get the probability that curtailment is in-effect if all other
+ screening criteria are met.
+
+ Returns
+ -------
+ probability : float
+ Fractional probability that curtailment is in-effect if all other
+ screening criteria are met. Defaults to 1 (curtailment is always
+ in effect if all other criteria are met).
+ """
+ returnfloat(self.get('probability',1.0))
+
+ @property
+ defrandom_seed(self):
+"""
+ Random seed to use for curtailment probability
+
+ Returns
+ -------
+ int
+ """
+ returnint(self.get('random_seed',0))
[docs]classPointsControl:
+"""Class to manage and split ProjectPoints."""
+
+ def__init__(self,project_points,sites_per_split=100):
+"""
+ Parameters
+ ----------
+ project_points : reV.config.ProjectPoints
+ ProjectPoints instance to be split between execution workers.
+ sites_per_split : int
+ Sites per project points split instance returned in the __next__
+ iterator function.
+ """
+
+ self._project_points=project_points
+ self._sites_per_split=sites_per_split
+ self._split_range=[]
+ self._i=0
+ self._iter_list=[]
+
+ def__iter__(self):
+"""Initialize the iterator by pre-splitting into a list attribute."""
+ last_site=0
+ ilim=len(self.project_points)
+
+ logger.debug(
+ "PointsControl iterator initializing with sites "
+ "{} through {}".format(
+ self.project_points.sites[0],self.project_points.sites[-1]
+ )
+ )
+
+ # pre-initialize all iter objects
+ whileTrue:
+ i0=last_site
+ i1=np.min([i0+self.sites_per_split,ilim])
+ ifi0==i1:
+ break
+
+ last_site=i1
+
+ new=self.split(
+ i0,
+ i1,
+ self.project_points,
+ sites_per_split=self.sites_per_split,
+ )
+ new._split_range=[i0,i1]
+ self._iter_list.append(new)
+
+ logger.debug(
+ "PointsControl stopped iteration at attempted "
+ "index of {}. Length of iterator is: {}".format(i1,len(self))
+ )
+ returnself
+
+ def__next__(self):
+"""Iterate through and return next site resource data.
+
+ Returns
+ -------
+ next_pc : config.PointsControl
+ Split instance of this class with a subset of project points based
+ on the number of sites per split.
+ """
+ ifself._i<self.N:
+ # Get next PointsControl from the iter list
+ next_pc=self._iter_list[self._i]
+ else:
+ # No more points controllers left in initialized list
+ raiseStopIteration
+
+ logger.debug(
+ "PointsControl passing site project points "
+ "with indices {} to {} on iteration #{} ".format(
+ next_pc.split_range[0],next_pc.split_range[1],self._i
+ )
+ )
+ self._i+=1
+ returnnext_pc
+
+ def__repr__(self):
+ msg="{} with {} sites from gid {} through {}".format(
+ self.__class__.__name__,
+ len(self.project_points),
+ self.sites[0],
+ self.sites[-1],
+ )
+ returnmsg
+
+ def__len__(self):
+"""Len is the number of possible iterations aka splits."""
+ returnint(np.ceil(len(self.project_points)/self.sites_per_split))
+
+ @property
+ defN(self):
+"""
+ Length of current iterator list
+
+ Returns
+ -------
+ N : int
+ Number of iterators in list
+ """
+ returnlen(self._iter_list)
+
+ @property
+ defsites_per_split(self):
+"""Get the iterator increment: number of sites per split.
+
+ Returns
+ -------
+ _sites_per_split : int
+ Sites per split iter object.
+ """
+ returnself._sites_per_split
+
+ @property
+ defproject_points(self):
+"""Get the project points property.
+
+ Returns
+ -------
+ _project_points : reV.config.project_points.ProjectPoints
+ ProjectPoints instance corresponding to this PointsControl
+ instance.
+ """
+ returnself._project_points
+
+ @property
+ defsites(self):
+"""Get the project points sites for this instance.
+
+ Returns
+ -------
+ sites : list
+ List of sites belonging to the _project_points attribute.
+ """
+ returnself._project_points.sites
+
+ @property
+ defsplit_range(self):
+"""Get the current split range property.
+
+ Returns
+ -------
+ _split_range : list
+ Two-entry list that indicates the starting and finishing
+ (inclusive, exclusive, respectively) indices of a split instance
+ of the PointsControl object. This is set in the iterator dunder
+ methods of PointsControl.
+ """
+ returnself._split_range
+
+
[docs]@classmethod
+ defsplit(cls,i0,i1,project_points,sites_per_split=100):
+"""Split this execution by splitting the project points attribute.
+
+ Parameters
+ ----------
+ i0/i1 : int
+ Beginning/end (inclusive/exclusive, respectively) index split
+ parameters for ProjectPoints.split() method.
+ project_points : reV.config.ProjectPoints
+ Project points instance that will be split.
+ sites_per_split : int
+ Sites per project points split instance returned in the __next__
+ iterator function.
+
+ Returns
+ -------
+ sub : PointsControl
+ New instance of PointsControl with a subset of the original
+ project points.
+ """
+ i0=int(i0)
+ i1=int(i1)
+ new_points=ProjectPoints.split(i0,i1,project_points)
+ sub=cls(new_points,sites_per_split=sites_per_split)
+ returnsub
+
+
+
[docs]classProjectPoints:
+"""Class to manage site and SAM input configuration requests.
+
+ Examples
+ --------
+ >>> import os
+ >>> from reV import TESTDATADIR
+ >>> from reV.config.project_points import ProjectPoints
+ >>>
+ >>> points = slice(0, 100)
+ >>> sam_file = os.path.join(TESTDATADIR, 'SAM/naris_pv_1axis_inv13.json')
+ >>> pp = ProjectPoints(points, sam_file)
+ >>>
+ >>> config_id_site0, SAM_config_dict_site0 = pp[0]
+ >>> site_list_or_slice = pp.sites
+ >>> site_list_or_slice = pp.get_sites_from_config(config_id)
+ >>> ProjectPoints_sub = pp.split(0, 10, project_points)
+ >>> h_list = pp.h
+ """
+
+ def__init__(
+ self,points,sam_configs,tech=None,res_file=None,curtailment=None
+ ):
+"""
+ Parameters
+ ----------
+ points : int | slice | list | tuple | str | pd.DataFrame | dict
+ Slice specifying project points, string pointing to a project
+ points csv, or a dataframe containing the effective csv contents.
+ Can also be a single integer site value.
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+ tech : str, optional
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed,
+ by default None
+ res_file : str | NoneType
+ Optional resource file to find maximum length of project points if
+ points slice stop is None.
+ curtailment : NoneType | dict | str | config.curtailment.Curtailment
+ Inputs for curtailment parameters. If not None, curtailment inputs
+ are expected. Can be:
+
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config json file with path (str)
+ - Instance of curtailment config object
+ (config.curtailment.Curtailment)
+
+ """
+ # set protected attributes
+ self._df=self._parse_points(points,res_file=res_file)
+ self._sam_config_obj=self._parse_sam_config(sam_configs)
+ self._check_points_config_mapping()
+ self._tech=str(tech)
+ self._h=self._d=None
+ self._curtailment=self._parse_curtailment(curtailment)
+
+ def__getitem__(self,site):
+"""Get the SAM config ID and dictionary for the requested site.
+
+ Parameters
+ ----------
+ site : int | str
+ Site number (gid) of interest (typically the resource gid).
+
+ Returns
+ -------
+ config_id : str
+ Configuration ID (variable name) specified in the sam_generation
+ config section.
+ config : dict
+ Actual SAM input values in a single level dictionary with variable
+ names (keys) and values.
+ """
+
+ site_bool=self.df[SiteDataField.GID]==site
+ try:
+ config_id=self.df.loc[site_bool,SiteDataField.CONFIG].values[0]
+ except(KeyError,IndexError)asex:
+ msg=(
+ "Site {} not found in this instance of "
+ "ProjectPoints. Available sites include: {}".format(
+ site,self.sites
+ )
+ )
+ logger.exception(msg)
+ raiseKeyError(msg)fromex
+
+ returnconfig_id,copy.deepcopy(self.sam_inputs[config_id])
+
+ def__repr__(self):
+ msg="{} with {} sites from gid {} through {}".format(
+ self.__class__.__name__,len(self),self.sites[0],self.sites[-1]
+ )
+ returnmsg
+
+ def__len__(self):
+"""Length of this object is the number of sites."""
+ returnlen(self.sites)
+
+ @property
+ defdf(self):
+"""Get the project points dataframe property.
+
+ Returns
+ -------
+ _df : pd.DataFrame
+ Table of sites and corresponding SAM configuration IDs.
+ Has columns "gid" and 'config'.
+ """
+ returnself._df
+
+ @property
+ defsam_config_ids(self):
+"""Get the SAM configs dictionary property.
+
+ Returns
+ -------
+ dict
+ Multi-level dictionary containing multiple SAM input config files.
+ The top level key is the SAM config ID, top level value is the SAM
+ config file path
+ """
+ returnsorted(self._sam_config_obj)
+
+ @property
+ defsam_config_obj(self):
+"""Get the SAM config object.
+
+ Returns
+ -------
+ _sam_config_obj : reV.config.sam_config.SAMConfig
+ SAM configuration object.
+ """
+ returnself._sam_config_obj
+
+ @property
+ defsam_inputs(self):
+"""Get the SAM configuration inputs dictionary property.
+
+ Returns
+ -------
+ dict
+ Multi-level dictionary containing multiple SAM input
+ configurations. The top level key is the SAM config ID, top level
+ value is the SAM config. Each SAM config is a dictionary with keys
+ equal to input names, values equal to the actual inputs.
+ """
+ returnself.sam_config_obj.inputs
+
+ @property
+ defall_sam_input_keys(self):
+"""Get a list of unique input keys from all SAM technology configs.
+
+ Returns
+ -------
+ all_sam_input_keys : list
+ List of unique strings where each string is a input key for the
+ SAM technology configs. For example, "gcr" or "losses" for PVWatts
+ or "wind_turbine_hub_ht" for windpower.
+ """
+ keys=[]
+ forsam_configinself.sam_inputs.values():
+ keys+=list(sam_config.keys())
+
+ keys=list(set(keys))
+
+ returnkeys
+
+ @property
+ defgids(self):
+"""Get the list of gids (resource file index values) belonging to this
+ instance of ProjectPoints. This is an alias of self.sites.
+
+ Returns
+ -------
+ gids : list
+ List of integer gids (resource file index values) belonging to this
+ instance of ProjectPoints. This is an alias of self.sites.
+ """
+ returnself.sites
+
+ @property
+ defsites(self):
+"""Get the list of sites (resource file gids) belonging to this
+ instance of ProjectPoints.
+
+ Returns
+ -------
+ sites : list
+ List of integer sites (resource file gids) belonging to this
+ instance of ProjectPoints.
+ """
+ returnself.df[SiteDataField.GID].values.tolist()
+
+ @property
+ defsites_as_slice(self):
+"""Get the sites in slice format.
+
+ Returns
+ -------
+ sites_as_slice : list | slice
+ Sites slice belonging to this instance of ProjectPoints.
+ The type is slice if possible. Will be a list only if sites are
+ non-sequential.
+ """
+ # try_slice is what the sites list would be if it is sequential
+ iflen(self.sites)>1:
+ try_step=self.sites[1]-self.sites[0]
+ else:
+ try_step=1
+ try_slice=slice(self.sites[0],self.sites[-1]+1,try_step)
+ try_list=list(range(*try_slice.indices(try_slice.stop)))
+
+ ifself.sites==try_list:
+ # try_slice is equivelant to the site list
+ sites_as_slice=try_slice
+ else:
+ # cannot be converted to a sequential slice, return list
+ sites_as_slice=self.sites
+
+ returnsites_as_slice
+
+ @property
+ deftech(self):
+"""Get the tech property from the config.
+
+ Returns
+ -------
+ _tech : str
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed.
+ """
+ return"windpower"if"wind"inself._tech.lower()elseself._tech
+
+ @property
+ defh(self):
+"""Get the hub heights corresponding to the site list.
+
+ Returns
+ -------
+ _h : list | NoneType
+ Hub heights corresponding to each site, taken from the sam config
+ for each site. This is None if the technology is not wind.
+ """
+ h_var="wind_turbine_hub_ht"
+ ifself._hisNone:
+ if"wind"inself.tech:
+ # wind technology, get a list of h values
+ self._h=[self[site][1][h_var]forsiteinself.sites]
+
+ returnself._h
+
+ @property
+ defd(self):
+"""Get the depths (m) corresponding to the site list.
+
+ Returns
+ -------
+ _d : list | NoneType
+ Resource depths (m) corresponding to each site, taken from
+ the sam config for each site. This is None if the technology
+ is not geothermal.
+ """
+ d_var="resource_depth"
+ ifself._disNone:
+ if"geothermal"inself.tech:
+ ifd_varinself.df:
+ self._d=list(self.df[d_var])
+ else:
+ self._d=[self[site][1][d_var]forsiteinself.sites]
+
+ returnself._d
+
+ @property
+ defcurtailment(self):
+"""Get the curtailment config object.
+
+ Returns
+ -------
+ _curtailment : NoneType | reV.config.curtailment.Curtailment
+ None if no curtailment, reV curtailment config object if
+ curtailment is being assessed.
+ """
+ returnself._curtailment
+
+ @staticmethod
+ def_parse_csv(fname):
+"""Import project points from .csv
+
+ Parameters
+ ----------
+ fname : str
+ Project points .csv file (with path). Must have 'gid' and
+ 'config' column names.
+
+ Returns
+ -------
+ df : pd.DataFrame
+ DataFrame mapping sites (gids) to SAM technology (config)
+ """
+ fname=fname.strip()
+ iffname.endswith(".csv"):
+ df=pd.read_csv(fname)
+ else:
+ raiseValueError(
+ "Config project points file must be "
+ ".csv, but received: {}".format(fname)
+ )
+
+ returndf
+
+ @staticmethod
+ def_parse_sites(points,res_file=None):
+"""Parse project points from list or slice
+
+ Parameters
+ ----------
+ points : int | str | pd.DataFrame | slice | list
+ Slice specifying project points, string pointing to a project
+ points csv, or a dataframe containing the effective csv contents.
+ Can also be a single integer site value.
+ res_file : str | NoneType
+ Optional resource file to find maximum length of project points if
+ points slice stop is None.
+
+ Returns
+ -------
+ df : pd.DataFrame
+ DataFrame mapping sites (gids) to SAM technology (config)
+ """
+ df=pd.DataFrame(columns=[SiteDataField.GID,SiteDataField.CONFIG])
+ ifisinstance(points,int):
+ points=[points]
+ ifisinstance(points,(list,tuple,np.ndarray)):
+ # explicit site list, set directly
+ ifany(isinstance(i,(list,tuple,np.ndarray))foriinpoints):
+ msg="Provided project points is not flat: {}!".format(points)
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ df[SiteDataField.GID]=points
+ elifisinstance(points,slice):
+ stop=points.stop
+ ifstopisNone:
+ ifres_fileisNone:
+ raiseValueError(
+ "Must supply a resource file if "
+ "points is a slice of type "
+ " slice(*, None, *)"
+ )
+
+ multi_h5_res,_=check_res_file(res_file)
+ ifmulti_h5_res:
+ stop=MultiFileResource(res_file).shape[1]
+ else:
+ stop=Resource(res_file).shape[1]
+
+ df[SiteDataField.GID]=list(range(*points.indices(stop)))
+ else:
+ raiseTypeError(
+ "Project Points sites needs to be set as a list, "
+ "tuple, or slice, but was set as: {}".format(type(points))
+ )
+
+ df[SiteDataField.CONFIG]=None
+
+ returndf
+
+ @classmethod
+ def_parse_points(cls,points,res_file=None):
+"""Generate the project points df from inputs
+
+ Parameters
+ ----------
+ points : int | str | pd.DataFrame | slice | list | dict
+ Slice specifying project points, string pointing to a project
+ points csv, or a dataframe containing the effective csv contents.
+ Can also be a single integer site value.
+ res_file : str | NoneType
+ Optional resource file to find maximum length of project points if
+ points slice stop is None.
+
+ Returns
+ -------
+ df : pd.DataFrame
+ DataFrame mapping sites (gids) to SAM technology (config)
+ """
+ ifisinstance(points,str):
+ df=cls._parse_csv(points)
+ elifisinstance(points,dict):
+ df=pd.DataFrame(points)
+ elifisinstance(points,(int,slice,list,tuple,np.ndarray)):
+ df=cls._parse_sites(points,res_file=res_file)
+ elifisinstance(points,pd.DataFrame):
+ df=points
+ else:
+ raiseValueError(
+ "Cannot parse Project points data from {}".format(type(points))
+ )
+ df=df.rename(SupplyCurveField.map_to(SiteDataField),axis=1)
+ ifSiteDataField.GIDnotindf.columns:
+ raiseKeyError(
+ "Project points data must contain "
+ f"{SiteDataField.GID} column."
+ )
+
+ # pylint: disable=no-member
+ ifSiteDataField.CONFIGnotindf.columns:
+ df[SiteDataField.CONFIG]=None
+
+ gids=df[SiteDataField.GID].values
+ ifnotnp.array_equal(np.sort(gids),gids):
+ msg=(
+ "WARNING: points are not in sequential order and will be "
+ "sorted! The original order is being preserved under "
+ 'column "points_order"'
+ )
+ logger.warning(msg)
+ warn(msg)
+ df["points_order"]=df.index.values
+ df=df.sort_values(SiteDataField.GID).reset_index(drop=True)
+
+ returndf
+
+ @staticmethod
+ def_parse_sam_config(sam_config):
+"""
+ Create SAM files dictionary.
+
+ Parameters
+ ----------
+ sam_config : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s) or SAM config
+ dict(s). Keys are the SAM config ID(s). Can also be a single
+ config file str. Can also be a pre loaded SAMConfig object.
+
+ Returns
+ -------
+ _sam_config_obj : reV.config.sam_config.SAMConfig
+ SAM configuration object.
+ """
+
+ ifisinstance(sam_config,SAMConfig):
+ returnsam_config
+
+ ifisinstance(sam_config,dict):
+ config_dict=sam_config
+ elifisinstance(sam_config,str):
+ config_dict={sam_config:sam_config}
+ else:
+ raiseValueError(
+ "Cannot parse SAM configs from {}".format(type(sam_config))
+ )
+
+ returnSAMConfig(config_dict)
+
+ @staticmethod
+ def_parse_curtailment(curtailment_input):
+"""Parse curtailment config object.
+
+ Parameters
+ ----------
+ curtailment_input : None | dict | str | config.curtailment.Curtailment
+ Inputs for curtailment parameters. If not None, curtailment inputs
+ are expected. Can be:
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config json file with path (str)
+ - Instance of curtailment config object
+ (config.curtailment.Curtailment)
+
+ Returns
+ -------
+ curtailments : NoneType | reV.config.curtailment.Curtailment
+ None if no curtailment, reV curtailment config object if
+ curtailment is being assessed.
+ """
+ ifisinstance(curtailment_input,(str,dict)):
+ # pointer to config file or explicit input namespace,
+ # instantiate curtailment config object
+ curtailment=Curtailment(curtailment_input)
+
+ elifisinstance(curtailment_input,(Curtailment,type(None))):
+ # pre-initialized curtailment object or no curtailment (None)
+ curtailment=curtailment_input
+
+ else:
+ curtailment=None
+ warn(
+ "Curtailment inputs not recognized. Received curtailment "
+ 'input of type: "{}". Expected None, dict, str, or '
+ "Curtailment object. Defaulting to no curtailment.",
+ ConfigWarning,
+ )
+
+ returncurtailment
+
+
[docs]defindex(self,gid):
+"""Get the index location (iloc not loc) for a resource gid found in
+ the project points.
+
+ Parameters
+ ----------
+ gid : int
+ Resource GID found in the project points gid column.
+
+ Returns
+ -------
+ ind : int
+ Row index of gid in the project points dataframe.
+ """
+ ifgidnotinself._df[SiteDataField.GID].values:
+ e=(
+ "Requested resource gid {} is not present in the project "
+ "points dataframe. Cannot return row index.".format(gid)
+ )
+ logger.error(e)
+ raiseConfigError(e)
+
+ ind=np.where(self._df[SiteDataField.GID]==gid)[0][0]
+
+ returnind
+
+ def_check_points_config_mapping(self):
+"""
+ Check to ensure the project points (df) and SAM configs
+ (sam_config_obj) are compatible. Update as necessary or break
+ """
+ # Extract unique config refences from project_points DataFrame
+ df_configs=self.df[SiteDataField.CONFIG].unique()
+ sam_configs=self.sam_inputs
+
+ # Checks to make sure that the same number of SAM config files
+ # as references in project_points DataFrame
+ iflen(df_configs)>len(sam_configs):
+ msg=(
+ "Points references {} configs while only "
+ "{} SAM configs were provided!".format(
+ len(df_configs),len(sam_configs)
+ )
+ )
+ logger.error(msg)
+ raiseConfigError(msg)
+
+ iflen(df_configs)==1anddf_configs[0]isNone:
+ self._df[SiteDataField.CONFIG]=list(sam_configs)[0]
+ df_configs=self.df[SiteDataField.CONFIG].unique()
+
+ # Check to see if config references in project_points DataFrame
+ # are valid file paths, if compare with SAM configs
+ # and update as needed
+ configs={}
+ forconfigindf_configs:
+ ifos.path.isfile(config):
+ configs[config]=config
+ elifconfiginsam_configs:
+ configs[config]=sam_configs[config]
+ else:
+ msg="{} does not map to a valid configuration file".format(
+ config
+ )
+ logger.error(msg)
+ raiseConfigError(msg)
+
+ # If configs has any keys that are not in sam_configs then
+ # something really weird happened so raise an error.
+ ifany(set(configs)-set(sam_configs)):
+ msg=(
+ "A wild config has appeared! Requested config keys for "
+ "ProjectPoints are {} and previous config keys are {}".format(
+ list(configs),list(sam_configs)
+ )
+ )
+ logger.error(msg)
+ raiseConfigError(msg)
+
+
[docs]defjoin_df(self,df2,key=SiteDataField.GID):
+"""Join new df2 to the _df attribute using the _df's gid as pkey.
+
+ This can be used to add site-specific data to the project_points,
+ taking advantage of the points_control iterator/split functions such
+ that only the relevant site data is passed to the analysis functions.
+
+ Parameters
+ ----------
+ df2 : pd.DataFrame
+ Dataframe to be joined to the self._df attribute (this instance
+ of project points dataframe). This likely contains
+ site-specific inputs that are to be passed to parallel workers.
+ key : str
+ Primary key of df2 to be joined to the _df attribute (this
+ instance of the project points dataframe). Primary key
+ of the self._df attribute is fixed as the gid column.
+ """
+ # ensure df2 doesnt have any duplicate columns for suffix reasons.
+ df2_cols=[cforcindf2.columnsifcnotinself._dforc==key]
+ self._df=pd.merge(
+ self._df,
+ df2[df2_cols],
+ how="left",
+ left_on=SiteDataField.GID,
+ right_on=key,
+ copy=False,
+ validate="1:1",
+ )
+
+
[docs]defget_sites_from_config(self,config):
+"""Get a site list that corresponds to a config key.
+
+ Parameters
+ ----------
+ config : str
+ SAM configuration ID associated with sites.
+
+ Returns
+ -------
+ sites : list
+ List of sites associated with the requested configuration ID. If
+ the configuration ID is not recognized, an empty list is returned.
+ """
+ sites=self.df.loc[
+ (self.df[SiteDataField.CONFIG]==config),SiteDataField.GID
+ ].values
+
+ returnlist(sites)
+
+
[docs]@classmethod
+ defsplit(cls,i0,i1,project_points):
+"""Return split instance of a ProjectPoints instance w/ site subset.
+
+ Parameters
+ ----------
+ i0 : int
+ Starting INDEX (not resource gid) (inclusive) of the site property
+ attribute to include in the split instance. This is not necessarily
+ the same as the starting site number, for instance if ProjectPoints
+ is sites 20:100, i0=0 i1=10 will result in sites 20:30.
+ i1 : int
+ Ending INDEX (not resource gid) (exclusive) of the site property
+ attribute to include in the split instance. This is not necessarily
+ the same as the final site number, for instance if ProjectPoints is
+ sites 20:100, i0=0 i1=10 will result in sites 20:30.
+ project_points: ProjectPoints
+ Instance of project points to split.
+
+ Returns
+ -------
+ sub : ProjectPoints
+ New instance of ProjectPoints with a subset of the following
+ attributes: sites, project points df, and the self dictionary data
+ struct.
+ """
+ # Extract DF subset with only index values between i0 and i1
+ n=len(project_points)
+ ifi0>nori1>n:
+ raiseValueError(
+ "{} and {} must be within the range of "
+ "project_points (0 - {})".format(i0,i1,n-1)
+ )
+
+ points_df=project_points.df.iloc[i0:i1]
+
+ # make a new instance of ProjectPoints with subset DF
+ sub=cls(
+ points_df,
+ project_points.sam_config_obj,
+ project_points.tech,
+ curtailment=project_points.curtailment,
+ )
+
+ returnsub
+
+ @staticmethod
+ def_parse_lat_lons(lat_lons):
+ msg=(
+ "Expecting a pair or multiple pairs of latitude and "
+ "longitude coordinates!"
+ )
+ ifisinstance(lat_lons,str):
+ lat_lons=parse_table(lat_lons)
+ cols=[
+ cforcinlat_lonsifc.lower().startswith(("lat","lon"))
+ ]
+ lat_lons=lat_lons[sorted(cols)].values
+ elifisinstance(lat_lons,(list,tuple)):
+ lat_lons=np.array(lat_lons)
+ elifisinstance(lat_lons,(int,float)):
+ msg+=" Recieved a single coordinate value!"
+ logger.error(msg)
+ raiseValueError(msg)
+
+ iflen(lat_lons.shape)==1:
+ lat_lons=np.expand_dims(lat_lons,axis=0)
+
+ iflat_lons.shape[1]!=2:
+ msg+=" Received {} coordinate values!".format(lat_lons.shape[1])
+ logger.error(msg)
+ raiseValueError(msg)
+
+ returnlat_lons
+
+
[docs]@classmethod
+ deflat_lon_coords(
+ cls,lat_lons,res_file,sam_configs,tech=None,curtailment=None
+ ):
+"""
+ Generate ProjectPoints for gids nearest to given latitude longitudes
+
+ Parameters
+ ----------
+ lat_lons : str | tuple | list | ndarray
+ Pair or pairs of latitude longitude coordinates
+ res_file : str
+ Resource file, needed to fine nearest neighbors
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+ tech : str, optional
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed,
+ by default None
+ curtailment : NoneType | dict | str | config.curtailment.Curtailment
+ Inputs for curtailment parameters. If not None, curtailment inputs
+ are expected. Can be:
+
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config json file with path (str)
+ - Instance of curtailment config object
+ (config.curtailment.Curtailment)
+
+
+ Returns
+ -------
+ pp : ProjectPoints
+ Initialized ProjectPoints object for points nearest to given
+ lat_lons
+ """
+ lat_lons=cls._parse_lat_lons(lat_lons)
+
+ multi_h5_res,hsds=check_res_file(res_file)
+ ifmulti_h5_res:
+ res_cls=MultiFileResourceX
+ res_kwargs={}
+ else:
+ res_cls=ResourceX
+ res_kwargs={"hsds":hsds}
+
+ logger.info(
+ "Converting latitude longitude coordinates into nearest "
+ "ProjectPoints"
+ )
+ logger.debug("- (lat, lon) pairs:\n{}".format(lat_lons))
+ withres_cls(res_file,**res_kwargs)asf:
+ gids=f.lat_lon_gid(lat_lons)# pylint: disable=no-member
+
+ ifisinstance(gids,int):
+ gids=[gids]
+ else:
+ iflen(gids)!=len(np.unique(gids)):
+ uniques,pos,counts=np.unique(
+ gids,return_counts=True,return_inverse=True
+ )
+ duplicates={}
+ foridxinnp.where(counts>1)[0]:
+ duplicate_lat_lons=lat_lons[np.where(pos==idx)[0]]
+ duplicates[uniques[idx]]=duplicate_lat_lons
+
+ msg=(
+ "reV Cannot currently handle duplicate Resource gids! "
+ "The given latitude and longitudes map to the same "
+ "gids:\n{}".format(duplicates)
+ )
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ gids=gids.tolist()
+
+ logger.debug("- Resource gids:\n{}".format(gids))
+
+ pp=cls(
+ gids,
+ sam_configs,
+ tech=tech,
+ res_file=res_file,
+ curtailment=curtailment,
+ )
+
+ if"points_order"inpp.df:
+ lat_lons=lat_lons[pp.df["points_order"].values]
+
+ pp._df["latitude"]=lat_lons[:,0]
+ pp._df["longitude"]=lat_lons[:,1]
+
+ returnpp
+
+
[docs]@classmethod
+ defregions(
+ cls,regions,res_file,sam_configs,tech=None,curtailment=None
+ ):
+"""
+ Generate ProjectPoints for gids nearest to given latitude longitudes
+
+ Parameters
+ ----------
+ regions : dict
+ Dictionary of regions to extract points for in the form:
+ {'region': 'region_column'}
+ res_file : str
+ Resource file, needed to fine nearest neighbors
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+ tech : str, optional
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed,
+ by default None
+ curtailment : NoneType | dict | str | config.curtailment.Curtailment
+ Inputs for curtailment parameters. If not None, curtailment inputs
+ are expected. Can be:
+
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config json file with path (str)
+ - Instance of curtailment config object
+ (config.curtailment.Curtailment)
+
+
+ Returns
+ -------
+ pp : ProjectPoints
+ Initialized ProjectPoints object for points nearest to given
+ lat_lons
+ """
+ multi_h5_res,hsds=check_res_file(res_file)
+ ifmulti_h5_res:
+ res_cls=MultiFileResourceX
+ else:
+ res_cls=ResourceX
+
+ logger.info("Extracting ProjectPoints for desired regions")
+ points=[]
+ withres_cls(res_file,hsds=hsds)asf:
+ meta=f.meta
+ forregion,region_colinregions.items():
+ logger.debug("- {}: {}".format(region_col,region))
+ # pylint: disable=no-member
+ gids=f.region_gids(region,region_col=region_col)
+ logger.debug("- Resource gids:\n{}".format(gids))
+ ifpoints:
+ duplicates=np.intersect1d(gids,points).tolist()
+ ifduplicates:
+ msg=(
+ "reV Cannot currently handle duplicate "
+ "Resource gids! The given regions containg the "
+ "same gids:\n{}".format(duplicates)
+ )
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ points.extend(gids.tolist())
+
+ pp=cls(
+ points,
+ sam_configs,
+ tech=tech,
+ res_file=res_file,
+ curtailment=curtailment,
+ )
+
+ meta=meta.loc[pp.sites]
+ cols=list(set(regions.values()))
+ forcincols:
+ pp._df[c]=meta[c].values
+
+ returnpp
[docs]classSAMConfig(BaseConfig):
+"""Class to handle the SAM section of config input."""
+
+ def__init__(self,SAM_configs):
+"""
+ Parameters
+ ----------
+ SAM_configs : dict
+ Keys are config ID's, values are filepaths to the SAM configs.
+ """
+ super().__init__(SAM_configs,check_keys=False)
+ self._clearsky=None
+ self._bifacial=None
+ self._icing=None
+ self._inputs=None
+ self._downscale=None
+ self._time_index_step=None
+
+ @property
+ defclearsky(self):
+"""Get a boolean for whether solar resource requires clearsky irrad.
+
+ Returns
+ -------
+ clearsky : bool
+ Flag set in the SAM config input with key "clearsky" for solar
+ analysis to process generation for clearsky irradiance.
+ Defaults to False (normal all-sky irradiance).
+ """
+ ifself._clearskyisNone:
+ self._clearsky=False
+ forvinself.inputs.values():
+ self._clearsky=any((self._clearsky,
+ bool(v.get('clearsky',False))))
+
+ ifself._clearsky:
+ logger.debug('Solar analysis being performed on clearsky '
+ 'irradiance.')
+
+ returnself._clearsky
+
+ @property
+ defbifacial(self):
+"""Get a boolean for whether bifacial solar analysis is being run.
+
+ Returns
+ -------
+ bifacial : bool
+ Flag set in the SAM config input with key "bifaciality" for solar
+ analysis to analyze bifacial PV panels. Will require albedo input.
+ Defaults to False (no bifacial panels is default).
+ """
+ ifself._bifacialisNone:
+ self._bifacial=False
+ forvinself.inputs.values():
+ bi_flags=('bifaciality','spe_is_bifacial',
+ 'cec_is_bifacial','6par_is_bifacial')
+ bi_bools=[bool(v.get(flag,0))forflaginbi_flags]
+ self._bifacial=any(bi_bools+[self._bifacial])
+
+ returnself._bifacial
+
+ @property
+ deficing(self):
+"""Get a boolean for whether wind generation is considering icing.
+
+ Returns
+ -------
+ _icing : bool
+ Flag for whether wind generation is considering icing effects.
+ Based on whether SAM input json has "en_icing_cutoff" == 1.
+ """
+ ifself._icingisNone:
+ self._icing=False
+ forvinself.inputs.values():
+ self._icing=any((self._icing,
+ bool(v.get('en_icing_cutoff',False))))
+
+ ifself._icing:
+ logger.debug('Icing analysis active for wind gen.')
+
+ returnself._icing
+
+ @property
+ deftime_index_step(self):
+"""
+ Step size for time_index for SAM profile output resolution
+
+ Returns
+ -------
+ int | None
+ Step size for time_index, used to reduce temporal resolution
+ """
+ ifself._time_index_stepisNone:
+ time_index_step=[]
+ forvinself.inputs.values():
+ time_index_step.append(v.get('time_index_step',None))
+
+ self._time_index_step=list(set(time_index_step))
+
+ iflen(self._time_index_step)>1:
+ msg=('Expecting a single unique value for "time_index_step" but '
+ 'received: {}'.format(self._time_index_step))
+ logger.error(msg)
+ raiseSAMInputError(msg)
+
+ returnself._time_index_step[0]
+
+ @property
+ defdownscale(self):
+"""
+ Resolution to downscale NSRDB resource to.
+
+ Returns
+ -------
+ dict | None
+ Option for NSRDB resource downscaling to higher temporal
+ resolution. The config expects a str entry in the Pandas
+ frequency format, e.g. '5min' or a dict of downscaling kwargs
+ such as {'frequency': '5min', 'variability_kwargs':
+ {'var_frac': 0.05, 'distribution': 'uniform'}}.
+ A str entry will be converted to a kwarg dict for the output
+ of this property e.g. '5min' -> {'frequency': '5min'}
+ """
+ ifself._downscaleisNone:
+ ds_list=[]
+ forvinself.inputs.values():
+ ds_list.append(v.get('downscale',None))
+
+ self._downscale=ds_list[0]
+ ds_list=list({str(x)forxinds_list})
+
+ iflen(ds_list)>1:
+ msg=('Expecting a single unique value for "downscale" but '
+ 'received: {}'.format(ds_list))
+ logger.error(msg)
+ raiseSAMInputError(msg)
+
+ ifisinstance(self._downscale,str):
+ self._downscale={'frequency':self._downscale}
+
+ returnself._downscale
+
+ @property
+ definputs(self):
+"""Get the SAM input file(s) (JSON/JSON5/YAML/TOML) and return
+ as a dictionary.
+
+ Parameters
+ ----------
+ _inputs : dict
+ The keys of this dictionary are the "configuration ID's".
+ The values are the imported json SAM input dictionaries.
+ """
+ ifself._inputsisNone:
+ self._inputs={}
+ forkey,configinself.items():
+ # key is ID (i.e. sam_param_0) that matches project points json
+ # fname is the actual SAM config file name (with path)
+ ifisinstance(config,str):
+ ifnotos.path.exists(config):
+ raiseIOError('SAM config file does not exist: "{}"'
+ .format(config))
+ else:
+ config=load_config(config)
+
+ ifnotisinstance(config,dict):
+ raiseRuntimeError('SAM config must be a file or a '
+ 'pre-extracted dictionary, but got: {}'
+ .format(config))
+
+ SAMInputsChecker.check(config)
+ self._inputs[key]=config
+
+ returnself._inputs
+
+
+
[docs]classSAMInputsChecker:
+"""Class to check SAM input jsons and warn against bad inputs."""
+
+ # Keys that are used to identify a technology config
+ KEYS_PV=('tilt','azimuth','module_type','array_type')
+
+ def__init__(self,config):
+"""
+ Parameters
+ ----------
+ config : dict
+ Extracted SAM technology input config in dict form.
+ """
+ ifisinstance(config,dict):
+ self._config=config
+ else:
+ raiseTypeError('Bad SAM tech config type: {}'
+ .format(type(config)))
+
+
[docs]defcheck_pv(self):
+"""Run input checks for a pv input config."""
+ ifself._config['array_type']>=2andself._config['tilt']!=0:
+ w=('SAM input for PV has array type {} (tracking) and tilt '
+ 'of {}. This is uncommon!'
+ .format(self._config['array_type'],self._config['tilt']))
+ logger.warning(w)
+ warn(w,SAMInputWarning)
+
+ def_run_checks(self):
+"""Infer config type and run applicable checks."""
+ ifall(cinself._configforcinself.KEYS_PV):
+ self.check_pv()
+
+
[docs]@classmethod
+ defcheck(cls,config):
+"""Run checks on a SAM input json config.
+
+ Parameters
+ ----------
+ config : dict
+ Extracted SAM technology input config in dict form.
+ """
+ c=cls(config)
+ c._run_checks()
[docs]classEcon(BaseGen):
+"""Econ"""
+
+ # Mapping of reV econ output strings to SAM econ modules
+ OPTIONS={
+ "lcoe_fcr":SAM_LCOE,
+ "ppa_price":SingleOwner,
+ "project_return_aftertax_npv":SingleOwner,
+ "lcoe_real":SingleOwner,
+ "lcoe_nom":SingleOwner,
+ "flip_actual_irr":SingleOwner,
+ "gross_revenue":SingleOwner,
+ "total_installed_cost":WindBos,
+ "turbine_cost":WindBos,
+ "sales_tax_cost":WindBos,
+ "bos_cost":WindBos,
+ "fixed_charge_rate":SAM_LCOE,
+ "capital_cost":SAM_LCOE,
+ "fixed_operating_cost":SAM_LCOE,
+ "variable_operating_cost":SAM_LCOE,
+ }
+"""Available ``reV`` econ `output_request` options"""
+
+ # Mapping of reV econ outputs to scale factors and units.
+ # Type is scalar or array and corresponds to the SAM single-site output
+ OUT_ATTRS=BaseGen.ECON_ATTRS
+
+ def__init__(self,project_points,sam_files,cf_file,site_data=None,
+ output_request=('lcoe_fcr',),sites_per_worker=100,
+ memory_utilization_limit=0.4,append=False):
+"""ReV econ analysis class.
+
+ ``reV`` econ analysis runs SAM econ calculations, typically to
+ compute LCOE (using :py:class:`PySAM.Lcoefcr.Lcoefcr`), though
+ :py:class:`PySAM.Singleowner.Singleowner` or
+ :py:class:`PySAM.Windbos.Windbos` calculations can also be
+ performed simply by requesting outputs from those computation
+ modules. See the keys of
+ :attr:`Econ.OPTIONS <reV.econ.econ.Econ.OPTIONS>` for all
+ available econ outputs. Econ computations rely on an input a
+ generation (i.e. capacity factor) profile. You can request
+ ``reV`` to run the analysis for one or more "sites", which
+ correspond to the meta indices in the generation data.
+
+ Parameters
+ ----------
+ project_points : int | list | tuple | str | dict | pd.DataFrame | slice
+ Input specifying which sites to process. A single integer
+ representing the GID of a site may be specified to evaluate
+ reV at a single location. A list or tuple of integers
+ (or slice) representing the GIDs of multiple sites can be
+ specified to evaluate reV at multiple specific locations.
+ A string pointing to a project points CSV file may also be
+ specified. Typically, the CSV contains the following
+ columns:
+
+ - ``gid``: Integer specifying the generation GID of each
+ site.
+ - ``config``: Key in the `sam_files` input dictionary
+ (see below) corresponding to the SAM configuration to
+ use for each particular site. This value can also be
+ ``None`` (or left out completely) if you specify only
+ a single SAM configuration file as the `sam_files`
+ input.
+ - ``capital_cost_multiplier``: This is an *optional*
+ multiplier input that, if included, will be used to
+ regionally scale the ``capital_cost`` input in the SAM
+ config. If you include this column in your CSV, you
+ *do not* need to specify ``capital_cost``, unless you
+ would like that value to vary regionally and
+ independently of the multiplier (i.e. the multiplier
+ will still be applied on top of the ``capital_cost``
+ input).
+
+ The CSV file may also contain other site-specific inputs by
+ including a column named after a config keyword (e.g. a
+ column called ``wind_turbine_rotor_diameter`` may be
+ included to specify a site-specific turbine diameter for
+ each location). Columns that do not correspond to a config
+ key may also be included, but they will be ignored. A
+ DataFrame following the same guidelines as the CSV input
+ (or a dictionary that can be used to initialize such a
+ DataFrame) may be used for this input as well.
+ sam_files : dict | str
+ A dictionary mapping SAM input configuration ID(s) to SAM
+ configuration(s). Keys are the SAM config ID(s) which
+ correspond to the ``config`` column in the project points
+ CSV. Values for each key are either a path to a
+ corresponding SAM config file or a full dictionary
+ of SAM config inputs. For example::
+
+ sam_files = {
+ "default": "/path/to/default/sam.json",
+ "onshore": "/path/to/onshore/sam_config.yaml",
+ "offshore": {
+ "sam_key_1": "sam_value_1",
+ "sam_key_2": "sam_value_2",
+ ...
+ },
+ ...
+ }
+
+ This input can also be a string pointing to a single SAM
+ config file. In this case, the ``config`` column of the
+ CSV points input should be set to ``None`` or left out
+ completely. See the documentation for the ``reV`` SAM class
+ (e.g. :class:`reV.SAM.generation.WindPower`,
+ :class:`reV.SAM.generation.PvWattsv8`,
+ :class:`reV.SAM.generation.Geothermal`, etc.) for
+ documentation on the allowed and/or required SAM config file
+ inputs.
+ cf_file : str
+ Path to reV output generation file containing a capacity
+ factor output.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ path can contain brackets ``{}`` that will be filled in
+ by the `analysis_years` input. Alternatively, this input
+ can be set to ``"PIPELINE"`` to parse the output of the
+ previous step (``reV`` generation) and use it as input to
+ this call. However, note that duplicate executions of
+ ``reV`` generation within the pipeline may invalidate this
+ parsing, meaning the `cf_file` input will have to be
+ specified manually.
+
+ site_data : str | pd.DataFrame, optional
+ Site-specific input data for SAM calculation. If this input
+ is a string, it should be a path that points to a CSV file.
+ Otherwise, this input should be a DataFrame with
+ pre-extracted site data. Rows in this table should match
+ the input sites via a ``gid`` column. The rest of the
+ columns should match configuration input keys that will take
+ site-specific values. Note that some or all site-specific
+ inputs can be specified via the `project_points` input
+ table instead. If ``None``, no site-specific data is
+ considered. By default, ``None``.
+ output_request : list | tuple, optional
+ List of output variables requested from SAM. Can be any
+ of the parameters in the "Outputs" group of the PySAM module
+ (e.g. :py:class:`PySAM.Windpower.Windpower.Outputs`,
+ :py:class:`PySAM.Pvwattsv8.Pvwattsv8.Outputs`,
+ :py:class:`PySAM.Geothermal.Geothermal.Outputs`, etc.) being
+ executed. This list can also include a select number of SAM
+ config/resource parameters to include in the output:
+ any key in any of the
+ `output attribute JSON files <https://tinyurl.com/4bmrpe3j/>`_
+ may be requested. Time-series profiles requested via this
+ input are output in UTC. By default, ``('lcoe_fcr',)``.
+ sites_per_worker : int, optional
+ Number of sites to run in series on a worker. ``None``
+ defaults to the resource file chunk size.
+ By default, ``None``.
+ memory_utilization_limit : float, optional
+ Memory utilization limit (fractional). Must be a value
+ between 0 and 1. This input sets how many site results will
+ be stored in-memory at any given time before flushing to
+ disk. By default, ``0.4``.
+ append : bool
+ Option to append econ datasets to source `cf_file`.
+ By default, ``False``.
+ """
+
+ # get a points control instance
+ pc=self.get_pc(
+ points=project_points,
+ points_range=None,
+ sam_configs=sam_files,
+ cf_file=cf_file,
+ sites_per_worker=sites_per_worker,
+ append=append,
+ )
+
+ super().__init__(
+ pc,
+ output_request,
+ site_data=site_data,
+ memory_utilization_limit=memory_utilization_limit,
+ )
+
+ self._cf_file=cf_file
+ self._append=append
+ self._run_attrs["cf_file"]=cf_file
+ self._run_attrs["sam_module"]=self._sam_module.MODULE
+
+ @property
+ defcf_file(self):
+"""Get the capacity factor output filename and path.
+
+ Returns
+ -------
+ cf_file : str
+ reV generation capacity factor output file with path.
+ """
+ returnself._cf_file
+
+ @property
+ defmeta(self):
+"""Get meta data from the source capacity factors file.
+
+ Returns
+ -------
+ _meta : pd.DataFrame
+ Meta data from capacity factor outputs file.
+ """
+ ifself._metaisNoneandself.cf_fileisnotNone:
+ withOutputs(self.cf_file)ascfh:
+ # only take meta that belongs to this project's site list
+ self._meta=cfh.meta[
+ cfh.meta[ResourceMetaField.GID].isin(
+ self.points_control.sites)]
+
+ if("offshore"inself._metaandself._meta["offshore"].sum()>1):
+ w=('Found offshore sites in econ meta data. '
+ 'This functionality has been deprecated. '
+ 'Please run the reV offshore module to '
+ 'calculate offshore wind lcoe.')
+ warn(w,OffshoreWindInputWarning)
+ logger.warning(w)
+
+ elifself._metaisNoneandself.cf_fileisNone:
+ self._meta=pd.DataFrame(
+ {ResourceMetaField.GID:self.points_control.sites})
+
+ returnself._meta
+
+ @property
+ deftime_index(self):
+"""Get the generation resource time index data."""
+ ifself._time_indexisNoneandself.cf_fileisnotNone:
+ withOutputs(self.cf_file)ascfh:
+ if"time_index"incfh.datasets:
+ self._time_index=cfh.time_index
+
+ returnself._time_index
+
+ @staticmethod
+ def_econ_append_pc(pp,cf_file,sites_per_worker=None):
+"""
+ Generate ProjectControls for econ append
+
+ Parameters
+ ----------
+ pp : reV.config.project_points.ProjectPoints
+ ProjectPoints to adjust gids for
+ cf_file : str
+ reV generation capacity factor output file with path.
+ sites_per_worker : int
+ Number of sites to run in series on a worker. None defaults to the
+ resource file chunk size.
+
+ Returns
+ -------
+ pc : reV.config.project_points.PointsControl
+ PointsControl object instance.
+ """
+ multi_h5_res,hsds=check_res_file(cf_file)
+ ifmulti_h5_res:
+ res_cls=MultiFileResource
+ res_kwargs={}
+ else:
+ res_cls=Resource
+ res_kwargs={"hsds":hsds}
+
+ withres_cls(cf_file,**res_kwargs)asf:
+ gid0=f.meta[ResourceMetaField.GID].values[0]
+ gid1=f.meta[ResourceMetaField.GID].values[-1]
+
+ i0=pp.index(gid0)
+ i1=pp.index(gid1)+1
+ pc=PointsControl.split(i0,i1,pp,sites_per_split=sites_per_worker)
+
+ returnpc
+
+
[docs]@classmethod
+ defget_pc(
+ cls,
+ points,
+ points_range,
+ sam_configs,
+ cf_file,
+ sites_per_worker=None,
+ append=False,
+ ):
+"""
+ Get a PointsControl instance.
+
+ Parameters
+ ----------
+ points : slice | list | str | reV.config.project_points.PointsControl
+ Slice specifying project points, or string pointing to a project
+ points csv, or a fully instantiated PointsControl object.
+ points_range : list | None
+ Optional two-entry list specifying the index range of the sites to
+ analyze. To be taken from the reV.config.PointsControl.split_range
+ property.
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+ cf_file : str
+ reV generation capacity factor output file with path.
+ sites_per_worker : int
+ Number of sites to run in series on a worker. None defaults to the
+ resource file chunk size.
+ append : bool
+ Flag to append econ datasets to source cf_file. This has priority
+ over the out_fpath input.
+
+ Returns
+ -------
+ pc : reV.config.project_points.PointsControl
+ PointsControl object instance.
+ """
+ pc=super().get_pc(
+ points,
+ points_range,
+ sam_configs,
+ ModuleName.ECON,
+ sites_per_worker=sites_per_worker,
+ res_file=cf_file,
+ )
+
+ ifappend:
+ pc=cls._econ_append_pc(
+ pc.project_points,cf_file,sites_per_worker=sites_per_worker
+ )
+
+ returnpc
+
+ @staticmethod
+ def_run_single_worker(pc,econ_fun,output_request,**kwargs):
+"""Run the SAM econ calculation.
+
+ Parameters
+ ----------
+ pc : reV.config.project_points.PointsControl
+ Iterable points control object from reV config module.
+ Must have project_points with df property with all relevant
+ site-specific inputs and a `SiteDataField.GID` column.
+ By passing site-specific inputs in this dataframe, which
+ was split using points_control, only the data relevant to
+ the current sites is passed.
+ econ_fun : method
+ reV_run() method from one of the econ modules (SingleOwner,
+ SAM_LCOE, WindBos).
+ output_request : str | list | tuple
+ Economic output variable(s) requested from SAM.
+ kwargs : dict
+ Additional input parameters for the SAM run module.
+
+ Returns
+ -------
+ out : dict
+ Output dictionary from the SAM reV_run function. Data is scaled
+ within this function to the datatype specified in Econ.OUT_ATTRS.
+ """
+
+ # make sure output request is a list
+ ifisinstance(output_request,str):
+ output_request=[output_request]
+
+ # Extract the site df from the project points df.
+ site_df=pc.project_points.df
+ site_df=site_df.set_index(ResourceMetaField.GID,drop=True)
+
+ # SAM execute econ analysis based on output request
+ try:
+ out=econ_fun(
+ pc,site_df,output_request=output_request,**kwargs
+ )
+ exceptExceptionase:
+ out={}
+ logger.exception("Worker failed for PC: {}".format(pc))
+ raisee
+
+ returnout
+
+ def_parse_output_request(self,req):
+"""Set the output variables requested from generation.
+
+ Parameters
+ ----------
+ req : str| list | tuple
+ Output variables requested from SAM.
+
+ Returns
+ -------
+ output_request : list
+ Output variables requested from SAM.
+ """
+
+ output_request=super()._parse_output_request(req)
+
+ forrequestinoutput_request:
+ ifrequestnotinself.OUT_ATTRS:
+ msg=(
+ 'User output request "{}" not recognized. '
+ "Will attempt to extract from PySAM.".format(request)
+ )
+ logger.debug(msg)
+
+ modules=[self.OPTIONS[request]forrequestinoutput_request
+ ifrequestinself.OPTIONS]
+
+ ifnotany(modules):
+ msg=(
+ "None of the user output requests were recognized. "
+ "Cannot run reV econ. "
+ "At least one of the following must be requested: {}".format(
+ list(self.OPTIONS.keys())
+ )
+ )
+ logger.exception(msg)
+ raiseExecutionError(msg)
+
+ b1=[m==modules[0]forminmodules]
+ b2=np.array([m==WindBosforminmodules])
+ b3=np.array([m==SingleOwnerforminmodules])
+
+ ifall(b1):
+ self._sam_module=modules[0]
+ self._fun=modules[0].reV_run
+ elifall(b2|b3):
+ self._sam_module=SingleOwner
+ self._fun=SingleOwner.reV_run
+ else:
+ msg=(
+ "Econ outputs requested from different SAM modules not "
+ "currently supported. Output request variables require "
+ "SAM methods: {}".format(modules)
+ )
+ raiseValueError(msg)
+
+ returnlist(set(output_request))
+
+ def_get_data_shape(self,dset,n_sites):
+"""Get the output array shape based on OUT_ATTRS or PySAM.Outputs.
+
+ This Econ get data shape method will also first check for the dset in
+ the site_data table. If not found in site_data, the dataset will be
+ looked for in OUT_ATTRS and PySAM.Outputs as it would for Generation.
+
+ Parameters
+ ----------
+ dset : str
+ Variable name to get shape for.
+ n_sites : int
+ Number of sites for this data shape.
+
+ Returns
+ -------
+ shape : tuple
+ 1D or 2D shape tuple for dset.
+ """
+
+ ifdsetinself.site_data:
+ data_shape=(n_sites,)
+ data=self.site_data[dset].values[0]
+
+ ifisinstance(data,(list,tuple,np.ndarray,str)):
+ msg=(
+ "Cannot pass through non-scalar site_data "
+ 'input key "{}" as an output_request!'.format(dset)
+ )
+ logger.error(msg)
+ raiseExecutionError(msg)
+
+ else:
+ data_shape=super()._get_data_shape(dset,n_sites)
+
+ returndata_shape
+
+
[docs]defrun(self,out_fpath=None,max_workers=1,timeout=1800,pool_size=None):
+"""Execute a parallel reV econ run with smart data flushing.
+
+ Parameters
+ ----------
+ out_fpath : str, optional
+ Path to output file. If this class was initialized with
+ ``append=True``, this input has no effect. If ``None``, no
+ output file will be written. If the filepath is specified
+ but the module name (econ) and/or resource data year is not
+ included, the module name and/or resource data year will get
+ added to the output file name. By default, ``None``.
+ max_workers : int, optional
+ Number of local workers to run on. By default, ``1``.
+ timeout : int, optional
+ Number of seconds to wait for parallel run iteration to
+ complete before returning zeros. By default, ``1800``
+ seconds.
+ pool_size : int, optional
+ Number of futures to submit to a single process pool for
+ parallel futures. If ``None``, the pool size is set to
+ ``os.cpu_count() * 2``. By default, ``None``.
+
+ Returns
+ -------
+ str | None
+ Path to output HDF5 file, or ``None`` if results were not
+ written to disk.
+ """
+ ifpool_sizeisNone:
+ pool_size=os.cpu_count()*2
+
+ # initialize output file or append econ data to gen file
+ ifself._append:
+ self._out_fpath=self._cf_file
+ else:
+ self._init_fpath(out_fpath,ModuleName.ECON)
+
+ self._init_h5(mode="a"ifself._appendelse"w")
+ self._init_out_arrays()
+
+ diff=list(set(self.points_control.sites)
+ -set(self.meta[ResourceMetaField.GID].values))
+ ifdiff:
+ raiseException(
+ "The following analysis sites were requested "
+ "through project points for econ but are not "
+ 'found in the CF file ("{}"): {}'.format(self.cf_file,diff)
+ )
+
+ # make a kwarg dict
+ kwargs={
+ "output_request":self.output_request,
+ "cf_file":self.cf_file,
+ "year":self.year,
+ }
+
+ logger.info(
+ "Running econ with smart data flushing ""for: {}".format(
+ self.points_control
+ )
+ )
+ logger.debug(
+ 'The following project points were specified: "{}"'.format(
+ self.project_points
+ )
+ )
+ logger.debug(
+ "The following SAM configs are available to this run:\n{}".format(
+ pprint.pformat(self.sam_configs,indent=4)
+ )
+ )
+ logger.debug(
+ "The SAM output variables have been requested:\n{}".format(
+ self.output_request
+ )
+ )
+
+ try:
+ kwargs["econ_fun"]=self._fun
+ ifmax_workers==1:
+ logger.debug(
+ "Running serial econ for: {}".format(self.points_control)
+ )
+ fori,pc_subinenumerate(self.points_control):
+ self.out=self._run_single_worker(pc_sub,**kwargs)
+ logger.info(
+ "Finished reV econ serial compute for: {} "
+ "(iteration {} out of {})".format(
+ pc_sub,i+1,len(self.points_control)
+ )
+ )
+ self.flush()
+ else:
+ logger.debug(
+ "Running parallel econ for: {}".format(self.points_control)
+ )
+ self._parallel_run(
+ max_workers=max_workers,
+ pool_size=pool_size,
+ timeout=timeout,
+ **kwargs,
+ )
+
+ exceptExceptionase:
+ logger.exception("SmartParallelJob.execute() failed for econ.")
+ raisee
+
+ returnself._out_fpath
+# -*- coding: utf-8 -*-
+"""
+reV module for calculating economies of scale where larger power plants will
+have reduced capital cost.
+"""
+
+importcopy
+importlogging
+importre
+
+# pylint: disable=unused-import
+importnumpyasnp
+importpandasaspd
+fromrex.utilities.utilitiesimportcheck_eval_str
+
+fromreV.econ.utilitiesimportlcoe_fcr
+fromreV.utilitiesimportSupplyCurveField
+
+logger=logging.getLogger(__name__)
+
+
+
[docs]classEconomiesOfScale:
+"""Class to calculate economies of scale where power plant capital cost is
+ reduced for larger power plants.
+
+ Units
+ -----
+ capacity_factor : unitless
+ capacity : kW
+ annual_energy_production : kWh
+ fixed_charge_rate : unitless
+ fixed_operating_cost : $ (per year)
+ variable_operating_cost : $/kWh
+ lcoe : $/MWh
+ """
+
+ def__init__(self,eqn,data):
+"""
+ Parameters
+ ----------
+ eqn : str
+ LCOE scaling equation to implement "economies of scale".
+ Equation must be in python string format and return a scalar
+ value to multiply the capital cost by. Independent variables in
+ the equation should match the keys in the data input arg. This
+ equation may use numpy functions with the package prefix "np".
+ data : dict | pd.DataFrame
+ Namespace of econ data to use to calculate economies of scale. Keys
+ in dict or column labels in dataframe should match the Independent
+ variables in the eqn input. Should also include variables required
+ to calculate LCOE.
+ """
+ self._eqn=eqn
+ self._data=data
+ self._preflight()
+
+ def_preflight(self):
+"""Run checks to validate EconomiesOfScale equation and input data."""
+
+ ifself._eqnisnotNone:
+ check_eval_str(str(self._eqn))
+
+ ifisinstance(self._data,pd.DataFrame):
+ self._data={
+ k:self._data[k].values.flatten()forkinself._data.columns
+ }
+
+ ifnotisinstance(self._data,dict):
+ e=(
+ "Cannot evaluate EconomiesOfScale with data input of type: "
+ "{}".format(type(self._data))
+ )
+
+ logger.error(e)
+ raiseTypeError(e)
+
+ missing=[namefornameinself.varsifnamenotinself._data]
+
+ ifany(missing):
+ e=(
+ "Cannot evaluate EconomiesOfScale, missing data for variables"
+ ": {} for equation: {}".format(missing,self._eqn)
+ )
+ logger.error(e)
+ raiseKeyError(e)
+
+
[docs]@staticmethod
+ defis_num(s):
+"""Check if a string is a number"""
+ try:
+ float(s)
+ exceptValueError:
+ returnFalse
+ else:
+ returnTrue
+
+
[docs]@staticmethod
+ defis_method(s):
+"""Check if a string is a numpy/pandas or python builtin method"""
+ returnbool(s.startswith(("np.","pd."))orsindir(__builtins__))
+
+ @property
+ defvars(self):
+"""Get a list of variable names that the EconomiesOfScale equation
+ uses as input.
+
+ Returns
+ -------
+ vars : list
+ List of strings representing variable names that were parsed from
+ the equation string. This will return an empty list if the equation
+ has no variables.
+ """
+ var_names=[]
+ ifself._eqnisnotNone:
+ delimiters=(">","<",">=","<=","==",",","*","/","+","-",
+ " ","(",")","[","]")
+ regex_pattern="|".join(map(re.escape,delimiters))
+ var_names=[]
+ forsubinre.split(regex_pattern,str(self._eqn)):
+ ifsubandnotself.is_num(sub)andnotself.is_method(sub):
+ var_names.append(sub)
+ var_names=sorted(set(var_names))
+
+ returnvar_names
+
+ def_evaluate(self):
+"""Evaluate the EconomiesOfScale equation with Independent variables
+ parsed into a kwargs dictionary input.
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Evaluated output of the EconomiesOfScale equation. Should be
+ numeric scalars to apply directly to the capital cost.
+ """
+ out=1
+ ifself._eqnisnotNone:
+ kwargs={k:self._data[k]forkinself.vars}
+ # pylint: disable=eval-used
+ out=eval(str(self._eqn),globals(),kwargs)
+
+ returnout
+
+ @staticmethod
+ def_get_prioritized_keys(input_dict,key_list):
+"""Get data from an input dictionary based on an ordered (prioritized)
+ list of retrieval keys. If no keys are found in the input_dict, an
+ error will be raised.
+
+ Parameters
+ ----------
+ input_dict : dict
+ Dictionary of data
+ key_list : list | tuple
+ Ordered (prioritized) list of retrieval keys.
+
+ Returns
+ -------
+ out : object
+ Data retrieved from input_dict using the first key in key_list
+ found in the input_dict.
+ """
+
+ out=None
+ forkeyinkey_list:
+ ifkeyininput_dict:
+ out=input_dict[key]
+ break
+
+ ifoutisNone:
+ e=(
+ "Could not find requested key list ({}) in the input "
+ "dictionary keys: {}".format(key_list,list(input_dict.keys()))
+ )
+ logger.error(e)
+ raiseKeyError(e)
+
+ returnout
+
+ @property
+ defcapital_cost_scalar(self):
+"""Evaluated output of the EconomiesOfScale equation. Should be
+ numeric scalars to apply directly to the capital cost.
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Evaluated output of the EconomiesOfScale equation. Should be
+ numeric scalars to apply directly to the capital cost.
+ """
+ returnself._evaluate()
+
+ def_cost_from_cap(self,col_name):
+"""Get full cost value from cost per mw in data.
+
+ Parameters
+ ----------
+ col_name : str
+ Name of column containing the cost per mw value.
+
+ Returns
+ -------
+ float | None
+ Cost value if it was found in data, ``None`` otherwise.
+ """
+ cap=self._data.get(SupplyCurveField.CAPACITY_AC_MW)
+ ifcapisNone:
+ returnNone
+
+ cost_per_mw=self._data.get(col_name)
+ ifcost_per_mwisNone:
+ returnNone
+
+ returncap*cost_per_mw
+
+ @property
+ defraw_capital_cost(self):
+"""Unscaled (raw) capital cost found in the data input arg.
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Unscaled (raw) capital_cost found in the data input arg.
+ """
+ raw_capital_cost_from_cap=self._cost_from_cap(
+ SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW
+ )
+ ifraw_capital_cost_from_capisnotNone:
+ returnraw_capital_cost_from_cap
+
+ key_list=["capital_cost","mean_capital_cost"]
+ returnself._get_prioritized_keys(self._data,key_list)
+
+ @property
+ defscaled_capital_cost(self):
+"""Capital cost found in the data input arg scaled by the evaluated
+ EconomiesOfScale input equation.
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Capital cost found in the data input arg scaled by the evaluated
+ EconomiesOfScale equation.
+ """
+ cc=copy.deepcopy(self.raw_capital_cost)
+ cc*=self.capital_cost_scalar
+ returncc
+
+ @property
+ deffcr(self):
+"""Fixed charge rate from input data arg
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Fixed charge rate from input data arg
+ """
+ fcr=self._data.get(SupplyCurveField.FIXED_CHARGE_RATE)
+ iffcrisnotNoneandfcr>0:
+ returnfcr
+
+ key_list=["fixed_charge_rate","mean_fixed_charge_rate","fcr",
+ "mean_fcr"]
+ returnself._get_prioritized_keys(self._data,key_list)
+
+ @property
+ deffoc(self):
+"""Fixed operating cost from input data arg
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Fixed operating cost from input data arg
+ """
+ foc_from_cap=self._cost_from_cap(
+ SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW
+ )
+ iffoc_from_capisnotNone:
+ returnfoc_from_cap
+
+ key_list=["fixed_operating_cost","mean_fixed_operating_cost",
+ "foc","mean_foc"]
+ returnself._get_prioritized_keys(self._data,key_list)
+
+ @property
+ defvoc(self):
+"""Variable operating cost from input data arg
+
+ Returns
+ -------
+ out : float | np.ndarray
+ Variable operating cost from input data arg
+ """
+ voc_from_cap=self._cost_from_cap(
+ SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW
+ )
+ ifvoc_from_capisnotNone:
+ returnvoc_from_cap
+
+ key_list=["variable_operating_cost","mean_variable_operating_cost",
+ "voc","mean_voc"]
+ returnself._get_prioritized_keys(self._data,key_list)
+
+ @property
+ defaep(self):
+"""Annual energy production back-calculated from the raw LCOE:
+
+ AEP = (fcr * raw_cap_cost + foc) / raw_lcoe
+
+ Returns
+ -------
+ out : float | np.ndarray
+ """
+
+ aep=(self.fcr*self.raw_capital_cost+self.foc)/self.raw_lcoe
+ aep*=1000# convert MWh to KWh
+ returnaep
+
+ @property
+ defraw_lcoe(self):
+"""Raw LCOE taken from the input data
+
+ Returns
+ -------
+ lcoe : float | np.ndarray
+ """
+ key_list=[SupplyCurveField.RAW_LCOE,SupplyCurveField.MEAN_LCOE]
+ returncopy.deepcopy(self._get_prioritized_keys(self._data,key_list))
+
+ @property
+ defscaled_lcoe(self):
+"""LCOE calculated with the scaled capital cost based on the
+ EconomiesOfScale input equation.
+
+ LCOE = (FCR * scaled_capital_cost + FOC) / AEP + VOC
+
+ Returns
+ -------
+ lcoe : float | np.ndarray
+ LCOE calculated with the scaled capital cost based on the
+ EconomiesOfScale input equation.
+ """
+ returnlcoe_fcr(
+ self.fcr,self.scaled_capital_cost,self.foc,self.aep,self.voc
+ )
[docs]classBaseGen(ABC):
+"""Base class for reV gen and econ classes to run SAM simulations."""
+
+ # Mapping of reV requests to SAM objects that should be used for simulation
+ OPTIONS={}
+
+ # Mapping of reV generation / econ outputs to scale factors and units.
+ OUT_ATTRS=copy.deepcopy(OTHER_ATTRS)
+
+ # Mapping of reV econ outputs to scale factors and units.
+ # Type is scalar or array and corresponds to the SAM single-site output
+ # This is the OUT_ATTRS class attr for Econ but should also be accessible
+ # to rev generation
+ ECON_ATTRS=copy.deepcopy(OTHER_ATTRS)
+ ECON_ATTRS.update(LCOE_ATTRS)
+ ECON_ATTRS.update(SO_ATTRS)
+ ECON_ATTRS.update(BOS_ATTRS)
+ ECON_ATTRS.update(LCOE_IN_ATTRS)
+
+ # SAM argument names used to calculate LCOE
+ # Note that system_capacity is not included here because it is never used
+ # downstream and could be confused with the supply_curve point capacity
+ LCOE_ARGS=('fixed_charge_rate','capital_cost',
+ 'fixed_operating_cost',
+ 'variable_operating_cost')
+
+ def__init__(
+ self,
+ points_control,
+ output_request,
+ site_data=None,
+ drop_leap=False,
+ memory_utilization_limit=0.4,
+ scale_outputs=True,
+ ):
+"""
+ Parameters
+ ----------
+ points_control : reV.config.project_points.PointsControl
+ Project points control instance for site and SAM config spec.
+ output_request : list | tuple
+ Output variables requested from SAM.
+ site_data : str | pd.DataFrame | None
+ Site-specific input data for SAM calculation. String should be a
+ filepath that points to a csv, DataFrame is pre-extracted data.
+ Rows match sites, columns are input keys. Need a "gid" column.
+ Input as None if no site-specific data.
+ drop_leap : bool
+ Drop leap day instead of final day of year during leap years.
+ memory_utilization_limit : float
+ Memory utilization limit (fractional). This sets how many site
+ results will be stored in-memory at any given time before flushing
+ to disk.
+ scale_outputs : bool
+ Flag to scale outputs in-place immediately upon Gen returning data.
+ """
+ log_versions(logger)
+ self._points_control=points_control
+ self._year=None
+ self._site_limit=None
+ self._site_mem=None
+ self._out_fpath=None
+ self._meta=None
+ self._time_index=None
+ self._sam_module=None
+ self._sam_obj_default=None
+ self._drop_leap=drop_leap
+ self.mem_util_lim=memory_utilization_limit
+ self.scale_outputs=scale_outputs
+
+ self._run_attrs={
+ "points_control":str(points_control),
+ "output_request":output_request,
+ "site_data":str(site_data),
+ "drop_leap":str(drop_leap),
+ "memory_utilization_limit":self.mem_util_lim,
+ }
+
+ self._site_data=self._parse_site_data(site_data)
+ self.add_site_data_to_pp(self._site_data)
+ output_request=SAMOutputRequest(output_request)
+ self._output_request=self._parse_output_request(output_request)
+
+ # pre-initialize output arrays to store results when available.
+ self._out={}
+ self._finished_sites=[]
+ self._out_n_sites=0
+ self._out_chunk=()
+ self._check_sam_version_inputs()
+
+ @property
+ defoutput_request(self):
+"""Get the output variables requested from the user.
+
+ Returns
+ -------
+ output_request : list
+ Output variables requested from SAM.
+ """
+ returnself._output_request
+
+ @property
+ defout_chunk(self):
+"""Get the current output chunk index range (INCLUSIVE).
+
+ Returns
+ -------
+ _out_chunk : tuple
+ Two entry tuple (start, end) indicies (inclusive) for where the
+ current data in-memory belongs in the final output.
+ """
+ returnself._out_chunk
+
+ @property
+ defsite_data(self):
+"""Get the site-specific inputs in dataframe format.
+
+ Returns
+ -------
+ _site_data : pd.DataFrame
+ Site-specific input data for gen or econ calculation. Rows match
+ sites, columns are variables.
+ """
+ returnself._site_data
+
+ @property
+ defsite_limit(self):
+"""Get the number of sites results that can be stored in memory at once
+
+ Returns
+ -------
+ _site_limit : int
+ Number of site result sets that can be stored in memory at once
+ without violating memory limits.
+ """
+
+ ifself._site_limitisNone:
+ tot_mem=psutil.virtual_memory().total/1e6
+ avail_mem=self.mem_util_lim*tot_mem
+ self._site_limit=int(np.floor(avail_mem/self.site_mem))
+ logger.info(
+ "Limited to storing {0} sites in memory "
+ "({1:.1f} GB total hardware, {2:.1f} GB available "
+ "with {3:.1f}% utilization).".format(
+ self._site_limit,
+ tot_mem/1e3,
+ avail_mem/1e3,
+ self.mem_util_lim*100,
+ )
+ )
+
+ returnself._site_limit
+
+ @property
+ defsite_mem(self):
+"""Get the memory (MB) required to store all results for a single site.
+
+ Returns
+ -------
+ _site_mem : float
+ Memory (MB) required to store all results in requested in
+ output_request for a single site.
+ """
+
+ ifself._site_memisNone:
+ # average the memory usage over n sites
+ # (for better understanding of array overhead)
+ n=100
+ self._site_mem=0
+ forrequestinself.output_request:
+ dtype="float32"
+ ifrequestinself.OUT_ATTRS:
+ dtype=self.OUT_ATTRS[request].get("dtype","float32")
+
+ shape=self._get_data_shape(request,n)
+ self._site_mem+=sys.getsizeof(np.ones(shape,dtype=dtype))
+
+ self._site_mem=self._site_mem/1e6/n
+ logger.info(
+ "Output results from a single site are calculated to "
+ "use {0:.1f} KB of memory.".format(self._site_mem/1000)
+ )
+
+ returnself._site_mem
+
+ @property
+ defpoints_control(self):
+"""Get project points controller.
+
+ Returns
+ -------
+ points_control : reV.config.project_points.PointsControl
+ Project points control instance for site and SAM config spec.
+ """
+ returnself._points_control
+
+ @property
+ defproject_points(self):
+"""Get project points
+
+ Returns
+ -------
+ project_points : reV.config.project_points.ProjectPoints
+ Project points from the points control instance.
+ """
+ returnself._points_control.project_points
+
+ @property
+ defsam_configs(self):
+"""Get the sam config dictionary.
+
+ Returns
+ -------
+ sam_configs : dict
+ SAM config from the project points instance.
+ """
+ returnself.project_points.sam_inputs
+
+ @property
+ defsam_metas(self):
+"""
+ SAM configurations including runtime module
+
+ Returns
+ -------
+ sam_metas : dict
+ Nested dictionary of SAM configuration files with module used
+ at runtime
+ """
+ sam_metas=self.sam_configs.copy()
+ forvinsam_metas.values():
+ v.update({"module":self._sam_module.MODULE})
+
+ returnsam_metas
+
+ @property
+ defsam_module(self):
+"""Get the SAM module class to be used for SAM simulations.
+
+ Returns
+ -------
+ sam_module : object
+ SAM object like PySAM.Pvwattsv7 or PySAM.Lcoefcr
+ """
+ returnself._sam_module
+
+ @property
+ defmeta(self):
+"""Get resource meta for all sites in project points.
+
+ Returns
+ -------
+ meta : pd.DataFrame
+ Meta data df for sites in project points. Column names are meta
+ data variables, rows are different sites. The row index
+ does not indicate the site number if the project points are
+ non-sequential or do not start from 0, so a `SiteDataField.GID`
+ column is added.
+ """
+ returnself._meta
+
+ @property
+ deftime_index(self):
+"""Get the resource time index data.
+
+ Returns
+ -------
+ _time_index : pandas.DatetimeIndex
+ Time-series datetime index
+ """
+ returnself._time_index
+
+ @property
+ defrun_attrs(self):
+"""
+ Run time attributes (__init__ args and kwargs)
+
+ Returns
+ -------
+ run_attrs : dict
+ Dictionary of runtime args and kwargs
+ """
+ returnself._run_attrs
+
+ @property
+ defyear(self):
+"""Get the resource year.
+
+ Returns
+ -------
+ _year : int
+ Year of the time-series datetime index.
+ """
+
+ ifself._yearisNoneandself.time_indexisnotNone:
+ self._year=int(self.time_index.year[0])
+
+ returnself._year
+
+ @property
+ deftech(self):
+"""Get the reV technology string.
+
+ Returns
+ -------
+ tech : str
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam, econ)
+ The string should be lower-cased with spaces and _ removed.
+ """
+ returnself.project_points.tech
+
+ @property
+ defout(self):
+"""Get the reV gen or econ output results.
+
+ Returns
+ -------
+ out : dict
+ Dictionary of gen or econ results from SAM.
+ """
+ out={}
+ fork,vinself._out.items():
+ ifkinself.OUT_ATTRS:
+ scale_factor=self.OUT_ATTRS[k].get("scale_factor",1)
+ else:
+ scale_factor=1
+
+ ifscale_factor!=1andself.scale_outputs:
+ v=v.astype("float32")
+ v/=scale_factor
+
+ out[k]=v
+
+ returnout
+
+ @out.setter
+ defout(self,result):
+"""Set the output attribute, unpack futures, clear output from mem.
+
+ Parameters
+ ----------
+ result : list | dict | None
+ Gen or Econ results to set to output dictionary. Use cases:
+ - List input is interpreted as a futures list, which is unpacked
+ before setting to the output dict.
+ - Dictionary input is interpreted as an already unpacked result.
+ - None is interpreted as a signal to clear the output dictionary.
+ """
+ ifisinstance(result,list):
+ # unpack futures list to dictionary first
+ result=self.unpack_futures(result)
+
+ ifisinstance(result,dict):
+ # iterate through dict where sites are keys and values are
+ # corresponding results
+ forsite_gid,site_outputinresult.items():
+ # check that the sites are stored sequentially then add to
+ # the finished site list
+ ifself._finished_sites:
+ ifint(site_gid)<np.max(self._finished_sites):
+ raiseException("Site results are non sequential!")
+
+ # unpack site output object
+ self.unpack_output(site_gid,site_output)
+
+ # add site gid to the finished list after outputs are unpacked
+ self._finished_sites.append(site_gid)
+
+ elifisinstance(result,type(None)):
+ self._out.clear()
+ self._finished_sites.clear()
+ else:
+ raiseTypeError(
+ "Did not recognize the type of output. "
+ 'Tried to set output type "{}", but requires '
+ "list, dict or None.".format(type(result))
+ )
+
+ @staticmethod
+ def_output_request_type_check(req):
+"""Output request type check and ensure list for manipulation.
+
+ Parameters
+ ----------
+ req : list | tuple | str
+ Output request of variable type.
+
+ Returns
+ -------
+ output_request : list
+ Output request.
+ """
+
+ ifisinstance(req,list):
+ output_request=req
+ elifisinstance(req,tuple):
+ output_request=list(req)
+ elifisinstance(req,str):
+ output_request=[req]
+ else:
+ raiseTypeError(
+ "Output request must be str, list, or tuple but "
+ "received: {}".format(type(req))
+ )
+
+ returnoutput_request
+
+
[docs]@staticmethod
+ defhandle_leap_ti(ti,drop_leap=False):
+"""Handle a time index for a leap year by dropping a day.
+
+ Parameters
+ ----------
+ ti : pandas.DatetimeIndex
+ Time-series datetime index with or without leap days.
+ drop_leap : bool
+ Option to drop leap days (if True) or drop the last day of each
+ leap year (if False).
+
+ Returns
+ -------
+ ti : pandas.DatetimeIndex
+ Time-series datetime index with length a multiple of 365.
+ """
+
+ # Drop leap day or last day
+ leap_day=(ti.month==2)&(ti.day==29)
+ leap_year=ti.year%4==0
+ last_day=((ti.month==12)&(ti.day==31))*leap_year
+ ifdrop_leap:
+ # Preference is to drop leap day if exists
+ ti=ti.drop(ti[leap_day])
+ elifany(leap_day):
+ # Leap day exists but preference is to drop last day of year
+ ti=ti.drop(ti[last_day])
+
+ iflen(ti)%365!=0:
+ raiseValueError(
+ "Bad time index with length not a multiple of "
+ "365: {}".format(ti)
+ )
+
+ returnti
+
+ @staticmethod
+ def_pp_to_pc(
+ points,
+ points_range,
+ sam_configs,
+ tech,
+ sites_per_worker=None,
+ res_file=None,
+ curtailment=None,
+ ):
+"""
+ Create ProjectControl from ProjectPoints
+
+ Parameters
+ ----------
+ points : int | slice | list | str | pandas.DataFrame
+ | reV.config.project_points.PointsControl
+ Single site integer,
+ or slice or list specifying project points,
+ or string pointing to a project points csv,
+ or a pre-loaded project points DataFrame,
+ or a fully instantiated PointsControl object.
+ points_range : list | None
+ Optional two-entry list specifying the index range of the sites to
+ analyze. To be taken from the reV.config.PointsControl.split_range
+ property.
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+ tech : str
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed.
+ sites_per_worker : int
+ Number of sites to run in series on a worker. None defaults to the
+ resource file chunk size.
+ res_file : str
+ Filepath to single resource file, multi-h5 directory,
+ or /h5_dir/prefix*suffix
+ curtailment : NoneType | dict | str | config.curtailment.Curtailment
+ Inputs for curtailment parameters. If not None, curtailment inputs
+ are expected. Can be:
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config json file with path (str)
+ - Instance of curtailment config object
+ (config.curtailment.Curtailment)
+
+ Returns
+ -------
+ pc : reV.config.project_points.PointsControl
+ PointsControl object instance.
+ """
+ ifhasattr(points,"df"):
+ points=points.df
+
+ pp=ProjectPoints(
+ points,
+ sam_configs,
+ tech=tech,
+ res_file=res_file,
+ curtailment=curtailment,
+ )
+
+ # make Points Control instance
+ ifpoints_rangeisnotNone:
+ # PointsControl is for just a subset of the project points...
+ # this is the case if generation is being initialized on one
+ # of many HPC nodes in a large project
+ pc=PointsControl.split(
+ points_range[0],
+ points_range[1],
+ pp,
+ sites_per_split=sites_per_worker,
+ )
+ else:
+ # PointsControl is for all of the project points
+ pc=PointsControl(pp,sites_per_split=sites_per_worker)
+
+ returnpc
+
+
[docs]@classmethod
+ defget_pc(
+ cls,
+ points,
+ points_range,
+ sam_configs,
+ tech,
+ sites_per_worker=None,
+ res_file=None,
+ curtailment=None,
+ ):
+"""Get a PointsControl instance.
+
+ Parameters
+ ----------
+ points : int | slice | list | str | pandas.DataFrame | PointsControl
+ Single site integer,
+ or slice or list specifying project points,
+ or string pointing to a project points csv,
+ or a pre-loaded project points DataFrame,
+ or a fully instantiated PointsControl object.
+ points_range : list | None
+ Optional two-entry list specifying the index range of the sites to
+ analyze. To be taken from the reV.config.PointsControl.split_range
+ property.
+ sam_configs : dict | str | SAMConfig
+ SAM input configuration ID(s) and file path(s). Keys are the SAM
+ config ID(s) which map to the config column in the project points
+ CSV. Values are either a JSON SAM config file or dictionary of SAM
+ config inputs. Can also be a single config file path or a
+ pre loaded SAMConfig object.
+ tech : str
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed.
+ sites_per_worker : int
+ Number of sites to run in series on a worker. None defaults to the
+ resource file chunk size.
+ res_file : str
+ Filepath to single resource file, multi-h5 directory,
+ or /h5_dir/prefix*suffix
+ curtailment : NoneType | dict | str | config.curtailment.Curtailment
+ Inputs for curtailment parameters. If not None, curtailment inputs
+ are expected. Can be:
+
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config json file with path (str)
+ - Instance of curtailment config object
+ (config.curtailment.Curtailment)
+
+
+ Returns
+ -------
+ pc : reV.config.project_points.PointsControl
+ PointsControl object instance.
+ """
+
+ iftechnotincls.OPTIONSandtech.lower()!=ModuleName.ECON:
+ msg=(
+ 'Did not recognize reV-SAM technology string "{}". '
+ "Technology string options are: {}".format(
+ tech,list(cls.OPTIONS.keys())
+ )
+ )
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ ifsites_per_workerisNone:
+ # get the optimal sites per split based on res file chunk size
+ sites_per_worker=cls.get_sites_per_worker(res_file)
+
+ logger.debug(
+ "Sites per worker being set to {} for ""PointsControl.".format(
+ sites_per_worker
+ )
+ )
+
+ ifisinstance(points,PointsControl):
+ # received a pre-intialized instance of pointscontrol
+ pc=points
+ else:
+ pc=cls._pp_to_pc(
+ points,
+ points_range,
+ sam_configs,
+ tech,
+ sites_per_worker=sites_per_worker,
+ res_file=res_file,
+ curtailment=curtailment,
+ )
+
+ returnpc
+
+
[docs]@staticmethod
+ defget_sites_per_worker(res_file,default=100):
+"""Get the nominal sites per worker (x-chunk size) for a given file.
+
+ This is based on the concept that it is most efficient for one core to
+ perform one read on one chunk of resource data, such that chunks will
+ not have to be read into memory twice and no sites will be read
+ redundantly.
+
+ Parameters
+ ----------
+ res_file : str
+ Filepath to single resource file, multi-h5 directory,
+ or /h5_dir/prefix*suffix
+ default : int
+ Sites to be analyzed on a single core if the chunk size cannot be
+ determined from res_file.
+
+ Returns
+ -------
+ sites_per_worker : int
+ Nominal sites to be analyzed per worker. This is set to the x-axis
+ chunk size for windspeed and dni datasets for the WTK and NSRDB
+ data, respectively.
+ """
+ ifnotres_fileornotos.path.isfile(res_file):
+ returndefault
+
+ withResource(res_file)asres:
+ if"wtk"inres_file.lower():
+ fordsetinres.datasets:
+ if"speed"indset:
+ # take nominal WTK chunks from windspeed
+ _,_,chunks=res.get_dset_properties(dset)
+ break
+ elif"nsrdb"inres_file.lower():
+ # take nominal NSRDB chunks from dni
+ _,_,chunks=res.get_dset_properties("dni")
+ else:
+ warn(
+ "Could not infer dataset chunk size as the resource type "
+ "could not be determined from the filename: {}".format(
+ res_file
+ )
+ )
+ chunks=None
+
+ ifchunksisNone:
+ # if chunks not set, go to default
+ sites_per_worker=default
+ logger.debug(
+ "Sites per worker being set to {} (default) based on "
+ "no set chunk size in {}.".format(sites_per_worker,res_file)
+ )
+ else:
+ sites_per_worker=chunks[1]
+ logger.debug(
+ "Sites per worker being set to {} based on chunk "
+ "size of {}.".format(sites_per_worker,res_file)
+ )
+
+ returnsites_per_worker
+
+
[docs]@staticmethod
+ defunpack_futures(futures):
+"""Combine list of futures results into their native dict format/type.
+
+ Parameters
+ ----------
+ futures : list
+ List of dictionary futures results.
+
+ Returns
+ -------
+ out : dict
+ Compiled results of the native future results type (dict).
+ """
+
+ out={}
+ forxinfutures:
+ out.update(x)
+
+ returnout
+
+ @staticmethod
+ @abstractmethod
+ def_run_single_worker(
+ points_control,
+ tech=None,
+ res_file=None,
+ output_request=None,
+ scale_outputs=True,
+ ):
+"""Run a reV-SAM analysis based on the points_control iterator.
+
+ Parameters
+ ----------
+ points_control : reV.config.PointsControl
+ A PointsControl instance dictating what sites and configs are run.
+ tech : str
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed.
+ res_file : str
+ Filepath to single resource file, multi-h5 directory,
+ or /h5_dir/prefix*suffix
+ output_request : list | tuple
+ Output variables requested from SAM.
+ scale_outputs : bool
+ Flag to scale outputs in-place immediately upon returning data.
+
+ Returns
+ -------
+ out : dict
+ Output dictionary from the SAM reV_run function. Data is scaled
+ within this function to the datatype specified in cls.OUT_ATTRS.
+ """
+
+ def_parse_site_data(self,inp):
+"""Parse site-specific data from input arg
+
+ Parameters
+ ----------
+ inp : str | pd.DataFrame | None
+ Site data in .csv or pre-extracted dataframe format. None signifies
+ that there is no extra site-specific data and that everything is
+ fully defined in the input h5 and SAM json configs.
+
+ Returns
+ -------
+ site_data : pd.DataFrame
+ Site-specific data for econ calculation. Rows correspond to sites,
+ columns are variables.
+ """
+
+ ifinpisNoneorinpisFalse:
+ # no input, just initialize dataframe with site gids as index
+ site_data=pd.DataFrame(index=self.project_points.sites)
+ site_data.index.name=ResourceMetaField.GID
+ else:
+ # explicit input, initialize df
+ ifisinstance(inp,str):
+ ifinp.endswith(".csv"):
+ site_data=pd.read_csv(inp)
+ elifisinstance(inp,pd.DataFrame):
+ site_data=inp
+ else:
+ # site data was not able to be set. Raise error.
+ raiseException(
+ "Site data input must be .csv or "
+ "dataframe, but received: {}".format(inp)
+ )
+
+ gid_not_in_site_data=ResourceMetaField.GIDnotinsite_data
+ index_name_not_gid=site_data.index.name!=ResourceMetaField.GID
+ ifgid_not_in_site_dataandindex_name_not_gid:
+ # require gid as column label or index
+ raiseKeyError('Site data input must have '
+ f'{ResourceMetaField.GID} column to match '
+ 'reV site gid.')
+
+ # pylint: disable=no-member
+ ifsite_data.index.name!=ResourceMetaField.GID:
+ # make gid the dataframe index if not already
+ site_data=site_data.set_index(ResourceMetaField.GID,
+ drop=True)
+
+ if"offshore"insite_data:
+ ifsite_data["offshore"].sum()>1:
+ w=('Found offshore sites in econ site data input. '
+ 'This functionality has been deprecated. '
+ 'Please run the reV offshore module to '
+ 'calculate offshore wind lcoe.')
+ warn(w,OffshoreWindInputWarning)
+ logger.warning(w)
+
+ returnsite_data
+
+
[docs]defadd_site_data_to_pp(self,site_data):
+"""Add the site df (site-specific inputs) to project points dataframe.
+
+ This ensures that only the relevant site's data will be passed through
+ to parallel workers when points_control is iterated and split.
+
+ Parameters
+ ----------
+ site_data : pd.DataFrame
+ Site-specific data for econ calculation. Rows correspond to sites,
+ columns are variables.
+ """
+ self.project_points.join_df(site_data,key=self.site_data.index.name)
+
+ def_parse_output_request(self,req):
+"""Set the output variables requested from the user.
+
+ Parameters
+ ----------
+ req : list | tuple
+ Output variables requested from SAM.
+
+ Returns
+ -------
+ output_request : list
+ Output variables requested from SAM.
+ """
+ output_request=self._output_request_type_check(req)
+
+ if"lcoe_fcr"inoutput_request:
+ output_request=_add_lcoe_outputs(output_request)
+
+ returnoutput_request
+
+ def_get_data_shape(self,dset,n_sites):
+"""Get the output array shape based on OUT_ATTRS or PySAM.Outputs.
+
+ Parameters
+ ----------
+ dset : str
+ Variable name to get shape for.
+ n_sites : int
+ Number of sites for this data shape.
+
+ Returns
+ -------
+ shape : tuple
+ 1D or 2D shape tuple for dset.
+ """
+
+ ifdsetinself.OUT_ATTRS:
+ returnself._get_data_shape_from_out_attrs(dset,n_sites)
+
+ ifdsetinself.project_points.all_sam_input_keys:
+ returnself._get_data_shape_from_sam_config(dset,n_sites)
+
+ returnself._get_data_shape_from_pysam(dset,n_sites)
+
+ def_get_data_shape_from_out_attrs(self,dset,n_sites):
+"""Get data shape from ``OUT_ATTRS`` variable"""
+ ifself.OUT_ATTRS[dset]["type"]=="array":
+ return(len(self.time_index),n_sites)
+ return(n_sites,)
+
+ def_get_data_shape_from_sam_config(self,dset,n_sites):
+"""Get data shape from SAM input config"""
+ data=list(self.project_points.sam_inputs.values())[0][dset]
+ ifisinstance(data,(list,tuple,np.ndarray)):
+ return(*np.array(data).shape,n_sites)
+
+ ifisinstance(data,str):
+ msg=(
+ 'Cannot pass through non-scalar SAM input key "{}" '
+ "as an output_request!".format(dset)
+ )
+ logger.error(msg)
+ raiseExecutionError(msg)
+
+ return(n_sites,)
+
+ def_get_data_shape_from_pysam(self,dset,n_sites):
+"""Get data shape from PySAM output object"""
+ ifself._sam_obj_defaultisNone:
+ self._sam_obj_default=self.sam_module.default()
+
+ try:
+ out_data=getattr(self._sam_obj_default.Outputs,dset)
+ exceptAttributeErrorase:
+ msg=(
+ 'Could not get data shape for dset "{}" '
+ 'from object "{}". '
+ 'Received the following error: "{}"'.format(
+ dset,self._sam_obj_default,e
+ )
+ )
+ logger.error(msg)
+ raiseExecutionError(msg)frome
+
+ ifisinstance(out_data,(int,float,str)):
+ return(n_sites,)
+
+ iflen(out_data)%len(self.time_index)==0:
+ return(len(self.time_index),n_sites)
+
+ return(len(out_data),n_sites)
+
+ def_init_fpath(self,out_fpath,module):
+"""Combine directory and filename, ensure .h5 ext., make out dirs."""
+ ifout_fpathisNone:
+ return
+
+ project_dir,out_fn=os.path.split(out_fpath)
+
+ # ensure output file is an h5
+ ifnotout_fn.endswith(".h5"):
+ out_fn+=".h5"
+
+ ifmodulenotinout_fn:
+ extension_with_module="_{}.h5".format(module)
+ out_fn=out_fn.replace(".h5",extension_with_module)
+
+ # ensure year is in out_fpath
+ ifself.yearisnotNone:
+ extension_with_year="_{}.h5".format(self.year)
+ ifextension_with_yearnotinout_fn:
+ out_fn=out_fn.replace(".h5",extension_with_year)
+
+ # create and use optional output dir
+ ifproject_dirandnotos.path.exists(project_dir):
+ os.makedirs(project_dir,exist_ok=True)
+
+ self._out_fpath=os.path.join(project_dir,out_fn)
+ self._run_attrs["out_fpath"]=out_fpath
+
+ def_init_h5(self,mode="w"):
+"""Initialize the single h5 output file with all output requests.
+
+ Parameters
+ ----------
+ mode : str
+ Mode to instantiate h5py.File instance
+ """
+
+ ifself._out_fpathisNone:
+ return
+
+ if"w"inmode:
+ logger.info(
+ 'Initializing full output file: "{}" with mode: {}'.format(
+ self._out_fpath,mode
+ )
+ )
+ elif"a"inmode:
+ logger.info(
+ 'Appending data to output file: "{}" with mode: {}'.format(
+ self._out_fpath,mode
+ )
+ )
+
+ attrs={d:{}fordinself.output_request}
+ chunks={}
+ dtypes={}
+ shapes={}
+
+ # flag to write time index if profiles are being output
+ write_ti=False
+
+ fordsetinself.output_request:
+ tmp="other"
+ ifdsetinself.OUT_ATTRS:
+ tmp=dset
+
+ attrs[dset]["units"]=self.OUT_ATTRS[tmp].get("units","unknown")
+ attrs[dset]["scale_factor"]=self.OUT_ATTRS[tmp].get(
+ "scale_factor",1
+ )
+ chunks[dset]=self.OUT_ATTRS[tmp].get("chunks",None)
+ dtypes[dset]=self.OUT_ATTRS[tmp].get("dtype","float32")
+ shapes[dset]=self._get_data_shape(dset,len(self.meta))
+ iflen(shapes[dset])>1:
+ write_ti=True
+
+ # only write time index if profiles were found in output request
+ ifwrite_ti:
+ ti=self.time_index
+ else:
+ ti=None
+
+ Outputs.init_h5(
+ self._out_fpath,
+ self.output_request,
+ shapes,
+ attrs,
+ chunks,
+ dtypes,
+ self.meta,
+ time_index=ti,
+ configs=self.sam_metas,
+ run_attrs=self.run_attrs,
+ mode=mode,
+ )
+
+ def_init_out_arrays(self,index_0=0):
+"""Initialize output arrays based on the number of sites that can be
+ stored in memory safely.
+
+ Parameters
+ ----------
+ index_0 : int
+ This is the site list index (not gid) for the first site in the
+ output data. If a node cannot process all sites in-memory at once,
+ this is used to segment the sites in the current output chunk.
+ """
+
+ self._out={}
+ self._finished_sites=[]
+
+ # Output chunk is the index range (inclusive) of this set of site outs
+ self._out_chunk=(
+ index_0,
+ np.min((index_0+self.site_limit,len(self.project_points)-1)),
+ )
+ self._out_n_sites=int(self.out_chunk[1]-self.out_chunk[0])+1
+
+ logger.info(
+ "Initializing in-memory outputs for {} sites with gids "
+ "{} through {} inclusive (site list index {} through {})".format(
+ self._out_n_sites,
+ self.project_points.sites[self.out_chunk[0]],
+ self.project_points.sites[self.out_chunk[1]],
+ self.out_chunk[0],
+ self.out_chunk[1],
+ )
+ )
+
+ forrequestinself.output_request:
+ dtype="float32"
+ ifrequestinself.OUT_ATTRSandself.scale_outputs:
+ dtype=self.OUT_ATTRS[request].get("dtype","float32")
+
+ shape=self._get_data_shape(request,self._out_n_sites)
+
+ # initialize the output request as an array of zeros
+ self._out[request]=np.zeros(shape,dtype=dtype)
+
+ def_check_sam_version_inputs(self):
+"""Check the PySAM version and input keys. Fix where necessary."""
+ forkey,parametersinself.project_points.sam_inputs.items():
+ updated=PySamVersionChecker.run(self.tech,parameters)
+ sam_obj=self._points_control._project_points._sam_config_obj
+ sam_obj._inputs[key]=updated
+
+
[docs]defunpack_output(self,site_gid,site_output):
+"""Unpack a SAM SiteOutput object to the output attribute.
+
+ Parameters
+ ----------
+ site_gid : int
+ Resource-native site gid (index).
+ site_output : dict
+ SAM site output object.
+ """
+
+ # iterate through the site results
+ forvar,valueinsite_output.items():
+ ifvarnotinself._out:
+ raiseKeyError(
+ 'Tried to collect output variable "{}", but it '
+ "was not yet initialized in the output "
+ "dictionary."
+ )
+
+ # get the index in the output array for the current site
+ i=self.site_index(site_gid,out_index=True)
+
+ # check to see if we have exceeded the current output chunk.
+ # If so, flush data to disk and reset the output initialization
+ ifi+1>self._out_n_sites:
+ self.flush()
+ global_site_index=self.site_index(site_gid)
+ self._init_out_arrays(index_0=global_site_index)
+ i=self.site_index(site_gid,out_index=True)
+
+ ifisinstance(value,(list,tuple,np.ndarray)):
+ ifnotisinstance(value,np.ndarray):
+ value=np.array(value)
+
+ self._out[var][:,i]=value.T
+
+ elifvalue!=0:
+ self._out[var][i]=value
+
+
[docs]defsite_index(self,site_gid,out_index=False):
+"""Get the index corresponding to the site gid.
+
+ Parameters
+ ----------
+ site_gid : int
+ Resource-native site index (gid).
+ out_index : bool
+ Option to get output index (if true) which is the column index in
+ the current in-memory output array, or (if false) the global site
+ index from the project points site list.
+
+ Returns
+ -------
+ index : int
+ Global site index if out_index=False, otherwise column index in
+ the current in-memory output array.
+ """
+
+ # get the index for site_gid in the (global) project points site list.
+ global_site_index=self.project_points.sites.index(site_gid)
+
+ ifnotout_index:
+ output_index=global_site_index
+ else:
+ output_index=global_site_index-self.out_chunk[0]
+ ifoutput_index<0:
+ raiseValueError(
+ "Attempting to set output data for site with "
+ "gid {} to global site index {}, which was "
+ "already set based on the current output "
+ "index chunk of {}".format(
+ site_gid,global_site_index,self.out_chunk
+ )
+ )
+
+ returnoutput_index
+
+
[docs]defflush(self):
+"""Flush the output data in self.out attribute to disk in .h5 format.
+
+ The data to be flushed is accessed from the instance attribute
+ "self.out". The disk target is based on the instance attributes
+ "self._out_fpath". Data is not flushed if _fpath is None or if .out is
+ empty.
+ """
+
+ # handle output file request if file is specified and .out is not empty
+ ifisinstance(self._out_fpath,str)andself._out:
+ logger.info(
+ 'Flushing outputs to disk, target file: "{}"'.format(
+ self._out_fpath
+ )
+ )
+
+ # get the slice of indices to write outputs to
+ islice=slice(self.out_chunk[0],self.out_chunk[1]+1)
+
+ # open output file in append mode to add output results to
+ withOutputs(self._out_fpath,mode="a")asf:
+ # iterate through all output requests writing each as a dataset
+ fordset,arrinself._out.items():
+ iflen(arr.shape)==1:
+ # write array of scalars
+ f[dset,islice]=arr
+ else:
+ # write 2D array of profiles
+ f[dset,:,islice]=arr
+
+ logger.debug("Flushed output successfully to disk.")
+
+ def_pre_split_pc(self,pool_size=None):
+"""Pre-split project control iterator into sub chunks to further
+ split the parallelization.
+
+ Parameters
+ ----------
+ pool_size : int
+ Number of futures to submit to a single process pool for
+ parallel futures. If ``None``, the pool size is set to
+ ``os.cpu_count() * 2``. By default, ``None``.
+
+ Returns
+ -------
+ N : int
+ Total number of points control split instances.
+ pc_chunks : list
+ List of lists of points control split instances.
+ """
+ N=0
+ pc_chunks=[]
+ i_chunk=[]
+ ifpool_sizeisNone:
+ pool_size=os.cpu_count()*2
+
+ fori,splitinenumerate(self.points_control):
+ N+=1
+ i_chunk.append(split)
+ if(i+1)%pool_size==0:
+ pc_chunks.append(i_chunk)
+ i_chunk=[]
+
+ ifi_chunk:
+ pc_chunks.append(i_chunk)
+
+ logger.debug(
+ "Pre-splitting points control into {} chunks with the "
+ "following chunk sizes: {}".format(
+ len(pc_chunks),[len(x)forxinpc_chunks]
+ )
+ )
+ returnN,pc_chunks
+
+ # pylint: disable=unused-argument
+ def_reduce_kwargs(self,pc,**kwargs):
+"""Placeholder for functions that need to reduce the global kwargs that
+ they send to workers to reduce memory footprint
+
+ Parameters
+ ----------
+ pc : PointsControl
+ PointsControl object for a single worker chunk
+ kwargs : dict
+ Kwargs for all gids that needs to be reduced before being sent to
+ ``_run_single_worker()``
+
+ Returns
+ -------
+ kwargs : dict
+ Same as input but reduced just for the gids in pc
+ """
+ returnkwargs
+
+ def_parallel_run(
+ self,max_workers=None,pool_size=None,timeout=1800,**kwargs
+ ):
+"""Execute parallel compute.
+
+ Parameters
+ ----------
+ max_workers : None | int
+ Number of workers. None will default to cpu count.
+ pool_size : int
+ Number of futures to submit to a single process pool for
+ parallel futures. If ``None``, the pool size is set to
+ ``os.cpu_count() * 2``. By default, ``None``.
+ timeout : int | float
+ Number of seconds to wait for parallel run iteration to complete
+ before returning zeros.
+ kwargs : dict
+ Keyword arguments to self._run_single_worker().
+ """
+
+ ifpool_sizeisNone:
+ pool_size=os.cpu_count()*2
+ ifmax_workersisNone:
+ max_workers=os.cpu_count()
+ logger.info(
+ "Running parallel execution with max_workers={}".format(
+ max_workers
+ )
+ )
+ i=0
+ N,pc_chunks=self._pre_split_pc(pool_size=pool_size)
+ forj,pc_chunkinenumerate(pc_chunks):
+ logger.debug(
+ "Starting process pool for points control "
+ "iteration {} out of {}".format(j+1,len(pc_chunks))
+ )
+
+ failed_futures=False
+ chunks={}
+ futures=[]
+ loggers=[__name__,"reV.gen","reV.econ","reV"]
+ withSpawnProcessPool(
+ max_workers=max_workers,loggers=loggers
+ )asexe:
+ forpcinpc_chunk:
+ pc_kwargs=self._reduce_kwargs(pc,**kwargs)
+ future=exe.submit(
+ self._run_single_worker,pc,**pc_kwargs
+ )
+ futures.append(future)
+ chunks[future]=pc
+
+ forfutureinfutures:
+ i+=1
+ try:
+ result=future.result(timeout=timeout)
+ exceptTimeoutError:
+ failed_futures=True
+ sites=chunks[future].project_points.sites
+ result=self._handle_failed_future(
+ future,i,sites,timeout
+ )
+
+ self.out=result
+
+ mem=psutil.virtual_memory()
+ m=(
+ "Parallel run at iteration {0} out of {1}. "
+ "Memory utilization is {2:.3f} GB out of {3:.3f} GB "
+ "total ({4:.1f}% used, intended limit of {5:.1f}%)"
+ .format(
+ i,
+ N,
+ mem.used/1e9,
+ mem.total/1e9,
+ 100*mem.used/mem.total,
+ 100*self.mem_util_lim,
+ )
+ )
+ logger.info(m)
+
+ iffailed_futures:
+ logger.info("Forcing pool shutdown after failed futures.")
+ exe.shutdown(wait=False)
+ logger.info("Forced pool shutdown complete.")
+
+ self.flush()
+
+ def_handle_failed_future(self,future,i,sites,timeout):
+"""Handle a failed future and return zeros.
+
+ Parameters
+ ----------
+ future : concurrent.futures.Future
+ Failed future to cancel.
+ i : int
+ Iteration number for logging
+ sites : list
+ List of site gids belonging to this failed future.
+ timeout : int
+ Number of seconds to wait for parallel run iteration to complete
+ before returning zeros.
+ """
+
+ w=("Iteration {} hit the timeout limit of {} seconds! "
+ "Passing zeros.".format(i,timeout))
+ logger.warning(w)
+ warn(w,OutputWarning)
+
+ site_out=dict.fromkeys(self.output_request,0)
+ result=dict.fromkeys(sites,site_out)
+
+ try:
+ cancelled=future.cancel()
+ exceptExceptionase:
+ w="Could not cancel future! Received exception: {}".format(e)
+ logger.warning(w)
+ warn(w,ParallelExecutionWarning)
+
+ ifnotcancelled:
+ w="Could not cancel future!"
+ logger.warning(w)
+ warn(w,ParallelExecutionWarning)
+
+ returnresult
[docs]classGen(BaseGen):
+"""Gen"""
+
+ # Mapping of reV technology strings to SAM generation objects
+ OPTIONS={
+ "geothermal":Geothermal,
+ "lineardirectsteam":LinearDirectSteam,
+ "mhkwave":MhkWave,
+ "pvsamv1":PvSamv1,
+ "pvwattsv5":PvWattsv5,
+ "pvwattsv7":PvWattsv7,
+ "pvwattsv8":PvWattsv8,
+ "solarwaterheat":SolarWaterHeat,
+ "tcsmoltensalt":TcsMoltenSalt,
+ "troughphysicalheat":TroughPhysicalHeat,
+ "windpower":WindPower,
+ }
+
+"""reV technology options."""
+
+ # Mapping of reV generation outputs to scale factors and units.
+ # Type is scalar or array and corresponds to the SAM single-site output
+ OUT_ATTRS=copy.deepcopy(OTHER_ATTRS)
+ OUT_ATTRS.update(GEN_ATTRS)
+ OUT_ATTRS.update(LIN_ATTRS)
+ OUT_ATTRS.update(SWH_ATTRS)
+ OUT_ATTRS.update(TPPH_ATTRS)
+ OUT_ATTRS.update(BaseGen.ECON_ATTRS)
+
+ def__init__(
+ self,
+ technology,
+ project_points,
+ sam_files,
+ resource_file,
+ low_res_resource_file=None,
+ output_request=("cf_mean",),
+ site_data=None,
+ curtailment=None,
+ gid_map=None,
+ drop_leap=False,
+ sites_per_worker=None,
+ memory_utilization_limit=0.4,
+ scale_outputs=True,
+ write_mapped_gids=False,
+ bias_correct=None,
+ ):
+"""ReV generation analysis class.
+
+ ``reV`` generation analysis runs SAM simulations by piping in
+ renewable energy resource data (usually from the NSRDB or WTK),
+ loading the SAM config, and then executing the PySAM compute
+ module for a given technology. See the documentation for the
+ ``reV`` SAM class (e.g. :class:`reV.SAM.generation.WindPower`,
+ :class:`reV.SAM.generation.PvWattsv8`,
+ :class:`reV.SAM.generation.Geothermal`, etc.) for info on the
+ allowed and/or required SAM config file inputs. If economic
+ parameters are supplied in the SAM config, then you can bundle a
+ "follow-on" econ calculation by just adding the desired econ
+ output keys to the `output_request`. You can request ``reV`` to
+ run the analysis for one or more "sites", which correspond to
+ the meta indices in the resource data (also commonly called the
+ ``gid's``).
+
+ Examples
+ --------
+ The following is an example of the most simple way to run reV
+ generation. Note that the ``TESTDATADIR`` refers to the local cloned
+ repository and will need to be replaced with a valid path if you
+ installed ``reV`` via a simple pip install.
+
+ >>> import os
+ >>> from reV import Gen, TESTDATADIR
+ >>>
+ >>> sam_tech = 'pvwattsv8'
+ >>> sites = 0
+ >>> fp_sam = os.path.join(TESTDATADIR, 'SAM/naris_pv_1axis_inv13.json')
+ >>> fp_res = os.path.join(TESTDATADIR, 'nsrdb/ri_100_nsrdb_2013.h5')
+ >>>
+ >>> gen = Gen(sam_tech, sites, fp_sam, fp_res)
+ >>> gen.run()
+ >>>
+ >>> gen.out
+ {'cf_mean': array([0.16966143], dtype=float32)}
+ >>>
+ >>> sites = [3, 4, 7, 9]
+ >>> req = ('cf_mean', 'lcoe_fcr')
+ >>> gen = Gen(sam_tech, sites, fp_sam, fp_res, output_request=req)
+ >>> gen.run()
+ >>>
+ >>> gen.out
+ {'fixed_charge_rate': array([0.096, 0.096, 0.096, 0.096],
+ 'base_capital_cost': array([39767200, 39767200, 39767200, 39767200],
+ 'base_variable_operating_cost': array([0, 0, 0, 0],
+ 'base_fixed_operating_cost': array([260000, 260000, 260000, 260000],
+ 'capital_cost': array([39767200, 39767200, 39767200, 39767200],
+ 'fixed_operating_cost': array([260000, 260000, 260000, 260000],
+ 'variable_operating_cost': array([0, 0, 0, 0],
+ 'capital_cost_multiplier': array([1, 1, 1, 1],
+ 'cf_mean': array([0.17859147, 0.17869979, 0.1834818 , 0.18646291],
+ 'lcoe_fcr': array([130.32126, 130.24226, 126.84782, 124.81981]}
+
+ Parameters
+ ----------
+ technology : str
+ String indicating which SAM technology to analyze. Must be
+ one of the keys of
+ :attr:`~reV.generation.generation.Gen.OPTIONS`. The string
+ should be lower-cased with spaces and underscores removed.
+ project_points : int | list | tuple | str | dict | pd.DataFrame | slice
+ Input specifying which sites to process. A single integer
+ representing the generation GID of a site may be specified
+ to evaluate reV at a single location. A list or tuple of
+ integers (or slice) representing the generation GIDs of
+ multiple sites can be specified to evaluate reV at multiple
+ specific locations. A string pointing to a project points
+ CSV file may also be specified. Typically, the CSV contains
+ the following columns:
+
+ - ``gid``: Integer specifying the generation GID of each
+ site.
+ - ``config``: Key in the `sam_files` input dictionary
+ (see below) corresponding to the SAM configuration to
+ use for each particular site. This value can also be
+ ``None`` (or left out completely) if you specify only
+ a single SAM configuration file as the `sam_files`
+ input.
+ - ``capital_cost_multiplier``: This is an *optional*
+ multiplier input that, if included, will be used to
+ regionally scale the ``capital_cost`` input in the SAM
+ config. If you include this column in your CSV, you
+ *do not* need to specify ``capital_cost``, unless you
+ would like that value to vary regionally and
+ independently of the multiplier (i.e. the multiplier
+ will still be applied on top of the ``capital_cost``
+ input).
+
+ The CSV file may also contain other site-specific inputs by
+ including a column named after a config keyword (e.g. a
+ column called ``wind_turbine_rotor_diameter`` may be
+ included to specify a site-specific turbine diameter for
+ each location). Columns that do not correspond to a config
+ key may also be included, but they will be ignored. A
+ DataFrame following the same guidelines as the CSV input
+ (or a dictionary that can be used to initialize such a
+ DataFrame) may be used for this input as well.
+
+ .. Note:: By default, the generation GID of each site is
+ assumed to match the resource GID to be evaluated for that
+ site. However, unique generation GID's can be mapped to
+ non-unique resource GID's via the `gid_map` input (see the
+ documentation for `gid_map` for more details).
+
+ sam_files : dict | str
+ A dictionary mapping SAM input configuration ID(s) to SAM
+ configuration(s). Keys are the SAM config ID(s) which
+ correspond to the ``config`` column in the project points
+ CSV. Values for each key are either a path to a
+ corresponding SAM config file or a full dictionary
+ of SAM config inputs. For example::
+
+ sam_files = {
+ "default": "/path/to/default/sam.json",
+ "onshore": "/path/to/onshore/sam_config.yaml",
+ "offshore": {
+ "sam_key_1": "sam_value_1",
+ "sam_key_2": "sam_value_2",
+ ...
+ },
+ ...
+ }
+
+ This input can also be a string pointing to a single SAM
+ config file. In this case, the ``config`` column of the
+ CSV points input should be set to ``None`` or left out
+ completely. See the documentation for the ``reV`` SAM class
+ (e.g. :class:`reV.SAM.generation.WindPower`,
+ :class:`reV.SAM.generation.PvWattsv8`,
+ :class:`reV.SAM.generation.Geothermal`, etc.) for
+ info on the allowed and/or required SAM config file inputs.
+ resource_file : str
+ Filepath to resource data. This input can be path to a
+ single resource HDF5 file or a path including a wildcard
+ input like ``/h5_dir/prefix*suffix`` (i.e. if your datasets
+ for a single year are spread out over multiple files). In
+ all cases, the resource data must be readable by
+ :py:class:`rex.resource.Resource`
+ or :py:class:`rex.multi_file_resource.MultiFileResource`.
+ (i.e. the resource data conform to the
+ `rex data format <https://tinyurl.com/3fy7v5kx>`_). This
+ means the data file(s) must contain a 1D ``time_index``
+ dataset indicating the UTC time of observation, a 1D
+ ``meta`` dataset represented by a DataFrame with
+ site-specific columns, and 2D resource datasets that match
+ the dimensions of (``time_index``, ``meta``). The time index
+ must start at 00:00 of January 1st of the year under
+ consideration, and its shape must be a multiple of 8760.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ input string can contain brackets ``{}`` that will be
+ filled in by the `analysis_years` input. Alternatively,
+ this input can be a list of explicit files to process. In
+ this case, the length of the list must match the length of
+ the `analysis_years` input exactly, and the path are
+ assumed to align with the `analysis_years` (i.e. the first
+ path corresponds to the first analysis year, the second
+ path corresponds to the second analysis year, and so on).
+
+ .. Important:: If you are using custom resource data (i.e.
+ not NSRDB/WTK/Sup3rCC, etc.), ensure the following:
+
+ - The data conforms to the
+ `rex data format <https://tinyurl.com/3fy7v5kx>`_.
+ - The ``meta`` DataFrame is organized such that every
+ row is a pixel and at least the columns
+ ``latitude``, ``longitude``, ``timezone``, and
+ ``elevation`` are given for each location.
+ - The time index and associated temporal data is in
+ UTC.
+ - The latitude is between -90 and 90 and longitude is
+ between -180 and 180.
+ - For solar data, ensure the DNI/DHI are not zero. You
+ can calculate one of these these inputs from the
+ other using the relationship
+
+ .. math:: GHI = DNI * cos(SZA) + DHI
+
+ low_res_resource_file : str, optional
+ Optional low resolution resource file that will be
+ dynamically mapped+interpolated to the nominal-resolution
+ `resource_file`. This needs to be of the same format as
+ `resource_file` - both files need to be handled by the
+ same ``rex Resource`` handler (e.g. ``WindResource``). All
+ of the requirements from the `resource_file` apply to this
+ input as well. If ``None``, no dynamic mapping to higher
+ resolutions is performed. By default, ``None``.
+ output_request : list | tuple, optional
+ List of output variables requested from SAM. Can be any
+ of the parameters in the "Outputs" group of the PySAM module
+ (e.g. :py:class:`PySAM.Windpower.Windpower.Outputs`,
+ :py:class:`PySAM.Pvwattsv8.Pvwattsv8.Outputs`,
+ :py:class:`PySAM.Geothermal.Geothermal.Outputs`, etc.) being
+ executed. This list can also include a select number of SAM
+ config/resource parameters to include in the output:
+ any key in any of the
+ `output attribute JSON files <https://tinyurl.com/4bmrpe3j/>`_
+ may be requested. If ``cf_mean`` is not included in this
+ list, it will automatically be added. Time-series profiles
+ requested via this input are output in UTC.
+
+ .. Note:: If you are performing ``reV`` solar runs using
+ ``PVWatts`` and would like ``reV`` to include AC capacity
+ values in your aggregation/supply curves, then you must
+ include the ``"dc_ac_ratio"`` time series as an output in
+ `output_request` when running ``reV`` generation. The AC
+ capacity outputs will automatically be added during the
+ aggregation/supply curve step if the ``"dc_ac_ratio"``
+ dataset is detected in the generation file.
+
+ By default, ``('cf_mean',)``.
+ site_data : str | pd.DataFrame, optional
+ Site-specific input data for SAM calculation. If this input
+ is a string, it should be a path that points to a CSV file.
+ Otherwise, this input should be a DataFrame with
+ pre-extracted site data. Rows in this table should match
+ the input sites via a ``gid`` column. The rest of the
+ columns should match configuration input keys that will take
+ site-specific values. Note that some or all site-specific
+ inputs can be specified via the `project_points` input
+ table instead. If ``None``, no site-specific data is
+ considered.
+
+ .. Note:: This input is often used to provide site-based
+ regional capital cost multipliers. ``reV`` does not
+ ingest multipliers directly; instead, this file is
+ expected to have a ``capital_cost`` column that gives the
+ multiplier-adjusted capital cost value for each location.
+ Therefore, you *must* re-create this input file every
+ time you change your base capital cost assumption.
+
+ By default, ``None``.
+ curtailment : dict | str, optional
+ Inputs for curtailment parameters, which can be:
+
+ - Explicit namespace of curtailment variables (dict)
+ - Pointer to curtailment config file with path (str)
+
+ The allowed key-value input pairs in the curtailment
+ configuration are documented as properties of the
+ :class:`reV.config.curtailment.Curtailment` class. If
+ ``None``, no curtailment is modeled. By default, ``None``.
+ gid_map : dict | str, optional
+ Mapping of unique integer generation gids (keys) to single
+ integer resource gids (values). This enables unique
+ generation gids in the project points to map to non-unique
+ resource gids, which can be useful when evaluating multiple
+ resource datasets in ``reV`` (e.g., forecasted ECMWF
+ resource data to complement historical WTK meteorology).
+ This input can be a pre-extracted dictionary or a path to a
+ JSON or CSV file. If this input points to a CSV file, the
+ file must have the columns ``gid`` (which matches the
+ project points) and ``gid_map`` (gids to extract from the
+ resource input). If ``None``, the GID values in the project
+ points are assumed to match the resource GID values.
+ By default, ``None``.
+ drop_leap : bool, optional
+ Drop leap day instead of final day of year when handling
+ leap years. By default, ``False``.
+ sites_per_worker : int, optional
+ Number of sites to run in series on a worker. ``None``
+ defaults to the resource file chunk size.
+ By default, ``None``.
+ memory_utilization_limit : float, optional
+ Memory utilization limit (fractional). Must be a value
+ between 0 and 1. This input sets how many site results will
+ be stored in-memory at any given time before flushing to
+ disk. By default, ``0.4``.
+ scale_outputs : bool, optional
+ Flag to scale outputs in-place immediately upon ``Gen``
+ returning data. By default, ``True``.
+ write_mapped_gids : bool, optional
+ Option to write mapped gids to output meta instead of
+ resource gids. By default, ``False``.
+ bias_correct : str | pd.DataFrame, optional
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+ """
+ pc=self.get_pc(
+ points=project_points,
+ points_range=None,
+ sam_configs=sam_files,
+ tech=technology,
+ sites_per_worker=sites_per_worker,
+ res_file=resource_file,
+ curtailment=curtailment,
+ )
+
+ super().__init__(
+ pc,
+ output_request,
+ site_data=site_data,
+ drop_leap=drop_leap,
+ memory_utilization_limit=memory_utilization_limit,
+ scale_outputs=scale_outputs,
+ )
+
+ ifself.technotinself.OPTIONS:
+ msg=(
+ 'Requested technology "{}" is not available. '
+ "reV generation can analyze the following "
+ "SAM technologies: {}".format(
+ self.tech,list(self.OPTIONS.keys())
+ )
+ )
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ self.write_mapped_gids=write_mapped_gids
+ self._res_file=resource_file
+ self._lr_res_file=low_res_resource_file
+ self._sam_module=self.OPTIONS[self.tech]
+ self._run_attrs["sam_module"]=self._sam_module.MODULE
+ self._run_attrs["res_file"]=resource_file
+
+ self._multi_h5_res,self._hsds=check_res_file(resource_file)
+ self._gid_map=self._parse_gid_map(gid_map)
+ self._nn_map=self._parse_nn_map()
+ self._bc=self._parse_bc(bias_correct)
+
+ @property
+ defres_file(self):
+"""Get the resource filename and path.
+
+ Returns
+ -------
+ res_file : str
+ Filepath to single resource file, multi-h5 directory,
+ or /h5_dir/prefix*suffix
+ """
+ returnself._res_file
+
+ @property
+ deflr_res_file(self):
+"""Get the (optional) low-resolution resource filename and path.
+
+ Returns
+ -------
+ str | None
+ """
+ returnself._lr_res_file
+
+ @property
+ defmeta(self):
+"""Get resource meta for all sites in project points.
+
+ Returns
+ -------
+ meta : pd.DataFrame
+ Meta data df for sites in project points. Column names are meta
+ data variables, rows are different sites. The row index
+ does not indicate the site number if the project points are
+ non-sequential or do not start from 0, so a `SiteDataField.GID`
+ column is added.
+ """
+ ifself._metaisNone:
+ res_cls=Resource
+ kwargs={"hsds":self._hsds}
+ ifself._multi_h5_res:
+ res_cls=MultiFileResource
+ kwargs={}
+
+ res_gids=self.project_points.sites
+ ifself._gid_mapisnotNone:
+ res_gids=[self._gid_map[i]foriinres_gids]
+
+ withres_cls(self.res_file,**kwargs)asres:
+ meta_len=res.shapes["meta"][0]
+
+ ifnp.max(res_gids)>meta_len:
+ msg=(
+ "ProjectPoints has a max site gid of {} which is "
+ "out of bounds for the meta data of len {} from "
+ "resource file: {}".format(
+ np.max(res_gids),meta_len,self.res_file
+ )
+ )
+ logger.error(msg)
+ raiseProjectPointsValueError(msg)
+
+ self._meta=res["meta",res_gids]
+
+ self._meta.loc[:,ResourceMetaField.GID]=res_gids
+ ifself.write_mapped_gids:
+ sites=self.project_points.sites
+ self._meta.loc[:,ResourceMetaField.GID]=sites
+ self._meta.index=self.project_points.sites
+ self._meta.index.name=ResourceMetaField.GID
+ self._meta.loc[:,"reV_tech"]=self.project_points.tech
+
+ returnself._meta
+
+ @property
+ deftime_index(self):
+"""Get the generation resource time index data.
+
+ Returns
+ -------
+ _time_index : pandas.DatetimeIndex
+ Time-series datetime index
+ """
+ ifself._time_indexisNone:
+ ifnotself._multi_h5_res:
+ res_cls=Resource
+ kwargs={"hsds":self._hsds}
+ else:
+ res_cls=MultiFileResource
+ kwargs={}
+
+ withres_cls(self.res_file,**kwargs)asres:
+ time_index=res.time_index
+
+ downscale=self.project_points.sam_config_obj.downscale
+ step=self.project_points.sam_config_obj.time_index_step
+ ifdownscaleisnotNone:
+ fromrex.utilities.downscaleimportmake_time_index
+
+ year=time_index.year[0]
+ ds_freq=downscale["frequency"]
+ time_index=make_time_index(year,ds_freq)
+ logger.info(
+ "reV solar generation running with temporal "
+ 'downscaling frequency "{}" with final '
+ "time_index length {}".format(ds_freq,len(time_index))
+ )
+ elifstepisnotNone:
+ time_index=time_index[::step]
+
+ time_index=self.handle_lifetime_index(time_index)
+ time_index=self.handle_leap_ti(
+ time_index,drop_leap=self._drop_leap
+ )
+
+ self._time_index=time_index
+
+ returnself._time_index
+
+
[docs]defhandle_lifetime_index(self,ti):
+"""Adjust the time index if modeling full system lifetime.
+
+ Parameters
+ ----------
+ ti : pandas.DatetimeIndex
+ Time-series datetime index with leap days.
+
+ Returns
+ -------
+ ti : pandas.DatetimeIndex
+ Time-series datetime index.
+ """
+ life_var="system_use_lifetime_output"
+ lifetime_periods=[]
+ forsam_metainself.sam_metas.values():
+ iflife_varinsam_metaandsam_meta[life_var]==1:
+ lifetime_period=sam_meta["analysis_period"]
+ lifetime_periods.append(lifetime_period)
+ else:
+ lifetime_periods.append(1)
+
+ ifnotany(ltp>1forltpinlifetime_periods):
+ returnti
+
+ # Only one time index may be passed, check that lifetime periods match
+ n_unique_periods=len(np.unique(lifetime_periods))
+ ifn_unique_periods!=1:
+ msg=(
+ "reV cannot handle multiple analysis_periods when "
+ "modeling with `system_use_lifetime_output` set "
+ "to 1. Found {} different analysis_periods in the SAM "
+ "configs".format(n_unique_periods)
+ )
+ logger.error(msg)
+ raiseConfigError(msg)
+
+ # Collect requested variables to check for lifetime compatibility
+ array_vars=[
+ varforvar,attrsinGEN_ATTRS.items()ifattrs["type"]=="array"
+ ]
+ valid_vars=["gen_profile","cf_profile","cf_profile_ac"]
+ invalid_vars=set(array_vars)-set(valid_vars)
+ invalid_requests=[
+ varforvarinself.output_requestifvarininvalid_vars
+ ]
+
+ ifinvalid_requests:
+ # SAM does not output full lifetime for all array variables
+ msg=(
+ "reV can only handle the following output arrays "
+ "when modeling with `system_use_lifetime_output` set "
+ "to 1: {}. Try running without {}.".format(
+ ", ".join(valid_vars),", ".join(invalid_requests)
+ )
+ )
+ logger.error(msg)
+ raiseConfigError(msg)
+
+ sam_meta=self.sam_metas[next(iter(self.sam_metas))]
+ analysis_period=sam_meta["analysis_period"]
+ logger.info(
+ "reV generation running with a full system "
+ "life of {} years.".format(analysis_period)
+ )
+
+ old_end=ti[-1]
+ new_end=old_end+pd.DateOffset(years=analysis_period-1)
+ step=old_end-ti[-2]
+ time_extension=pd.date_range(old_end,new_end,freq=step)
+ ti=time_extension.union(ti)
+
+ returnti
+
+ @classmethod
+ def_run_single_worker(
+ cls,
+ points_control,
+ tech=None,
+ res_file=None,
+ lr_res_file=None,
+ output_request=None,
+ scale_outputs=True,
+ gid_map=None,
+ nn_map=None,
+ bias_correct=None,
+ ):
+"""Run a SAM generation analysis based on the points_control iterator.
+
+ Parameters
+ ----------
+ points_control : reV.config.PointsControl
+ A PointsControl instance dictating what sites and configs are run.
+ tech : str
+ SAM technology to analyze (pvwattsv7, windpower, tcsmoltensalt,
+ solarwaterheat, troughphysicalheat, lineardirectsteam)
+ The string should be lower-cased with spaces and _ removed.
+ res_file : str
+ Filepath to single resource file, multi-h5 directory,
+ or /h5_dir/prefix*suffix
+ lr_res_file : str | None
+ Optional low resolution resource file that will be dynamically
+ mapped+interpolated to the nominal-resolution res_file. This
+ needs to be of the same format as resource_file, e.g. they both
+ need to be handled by the same rex Resource handler such as
+ WindResource
+ output_request : list | tuple
+ Output variables requested from SAM.
+ scale_outputs : bool
+ Flag to scale outputs in-place immediately upon Gen returning data.
+ gid_map : None | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This enables the user to input unique
+ generation gids in the project points that map to non-unique
+ resource gids. This can be None or a pre-extracted dict.
+ nn_map : np.ndarray
+ Optional 1D array of nearest neighbor mappings associated with the
+ res_file to lr_res_file spatial mapping. For details on this
+ argument, see the rex.MultiResolutionResource docstring.
+ bias_correct : None | pd.DataFrame
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+
+ Returns
+ -------
+ out : dict
+ Output dictionary from the SAM reV_run function. Data is scaled
+ within this function to the datatype specified in Gen.OUT_ATTRS.
+ """
+
+ # Extract the site df from the project points df.
+ site_df=points_control.project_points.df
+ site_df=site_df.set_index(ResourceMetaField.GID,drop=True)
+
+ # run generation method for specified technology
+ try:
+ out=cls.OPTIONS[tech].reV_run(
+ points_control,
+ res_file,
+ site_df,
+ lr_res_file=lr_res_file,
+ output_request=output_request,
+ gid_map=gid_map,
+ nn_map=nn_map,
+ bias_correct=bias_correct,
+ )
+
+ exceptExceptionase:
+ out={}
+ logger.exception("Worker failed for PC: {}".format(points_control))
+ raisee
+
+ ifscale_outputs:
+ # dtype convert in-place so no float data is stored unnecessarily
+ forsite,site_outputinout.items():
+ forkinsite_output.keys():
+ # iterate through variable names in each site's output dict
+ ifkincls.OUT_ATTRS:
+ ifout[site][k]isNone:
+ continue
+ # get dtype and scale for output variable name
+ dtype=cls.OUT_ATTRS[k].get("dtype","float32")
+ scale_factor=cls.OUT_ATTRS[k].get("scale_factor",1)
+
+ # apply scale factor and dtype
+ out[site][k]*=scale_factor
+
+ ifnp.issubdtype(dtype,np.integer):
+ # round after scaling if integer dtype
+ out[site][k]=np.round(out[site][k])
+
+ ifisinstance(out[site][k],np.ndarray):
+ # simple astype for arrays
+ out[site][k]=out[site][k].astype(dtype)
+ else:
+ # use numpy array conversion for scalar values
+ out[site][k]=np.array(
+ [out[site][k]],dtype=dtype
+ )[0]
+
+ returnout
+
+ def_parse_gid_map(self,gid_map):
+"""
+ Parameters
+ ----------
+ gid_map : None | dict | str
+ This can be None, a pre-extracted dict, or a filepath to json or
+ csv. If this is a csv, it must have the columns "gid" (which
+ matches the project points) and "gid_map" (gids to extract from the
+ resource input)
+
+ Returns
+ -------
+ gid_map : None | dict
+ Mapping of unique integer generation gids (keys) to single integer
+ resource gids (values). This enables the user to input unique
+ generation gids in the project points that map to non-unique
+ resource gids.
+ """
+
+ ifisinstance(gid_map,str):
+ ifgid_map.endswith(".csv"):
+ gid_map=pd.read_csv(gid_map).to_dict()
+ msg=f"Need {ResourceMetaField.GID} in gid_map column"
+ assertResourceMetaField.GIDingid_map,msg
+ assert"gid_map"ingid_map,'Need "gid_map" in gid_map column'
+ gid_map={
+ gid_map[ResourceMetaField.GID][i]:gid_map["gid_map"][i]
+ foriingid_map[ResourceMetaField.GID].keys()
+ }
+
+ elifgid_map.endswith(".json"):
+ withopen(gid_map)asf:
+ gid_map=json.load(f)
+
+ ifisinstance(gid_map,dict):
+ ifnotself._multi_h5_res:
+ res_cls=Resource
+ kwargs={"hsds":self._hsds}
+ else:
+ res_cls=MultiFileResource
+ kwargs={}
+
+ withres_cls(self.res_file,**kwargs)asres:
+ forgen_gid,res_gidingid_map.items():
+ msg1=(
+ "gid_map values must all be int but received "
+ "{}: {}".format(gen_gid,res_gid)
+ )
+ msg2=(
+ "Could not find the gen_gid to res_gid mapping "
+ "{}: {} in the resource meta data.".format(
+ gen_gid,res_gid
+ )
+ )
+ assertisinstance(gen_gid,int),msg1
+ assertisinstance(res_gid,int),msg1
+ assertres_gidinres.meta.index.values,msg2
+
+ forgen_gidinself.project_points.sites:
+ msg3=(
+ "Could not find the project points gid {} in the "
+ "gen_gid input of the gid_map.".format(gen_gid)
+ )
+ assertgen_gidingid_map,msg3
+
+ elifgid_mapisnotNone:
+ msg=(
+ "Could not parse gid_map, must be None, dict, or path to "
+ "csv or json, but received: {}".format(gid_map)
+ )
+ logger.error(msg)
+ raiseInputError(msg)
+
+ returngid_map
+
+ def_parse_nn_map(self):
+"""Parse a nearest-neighbor spatial mapping array if lr_res_file is
+ provided (resource data is at two resolutions and the low-resolution
+ data must be mapped to the nominal-resolution data)
+
+ Returns
+ -------
+ nn_map : np.ndarray
+ Optional 1D array of nearest neighbor mappings associated with the
+ res_file to lr_res_file spatial mapping. For details on this
+ argument, see the rex.MultiResolutionResource docstring.
+ """
+ nn_map=None
+ ifself.lr_res_fileisnotNone:
+ handler_class=Resource
+ if"*"inself.res_fileor"*"inself.lr_res_file:
+ handler_class=MultiFileResource
+
+ withhandler_class(self.res_file)ashr_res,handler_class(
+ self.lr_res_file
+ )aslr_res:
+ logger.info(
+ "Making nearest neighbor map for multi "
+ "resolution resource data..."
+ )
+ nn_d,nn_map=MultiResolutionResource.make_nn_map(
+ hr_res,lr_res
+ )
+ logger.info(
+ "Done making nearest neighbor map for multi "
+ "resolution resource data!"
+ )
+
+ logger.info(
+ "Made nearest neighbor mapping between nominal-"
+ "resolution and low-resolution resource files. "
+ "Min / mean / max dist: {:.3f} / {:.3f} / {:.3f}".format(
+ nn_d.min(),nn_d.mean(),nn_d.max()
+ )
+ )
+
+ returnnn_map
+
+ @staticmethod
+ def_parse_bc(bias_correct):
+"""Parse the bias correction data.
+
+ Parameters
+ ----------
+ bias_correct : str | pd.DataFrame, optional
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+
+ Returns
+ -------
+ bias_correct : None | pd.DataFrame
+ Optional DataFrame or CSV filepath to a wind or solar
+ resource bias correction table. This has columns:
+
+ - ``gid``: GID of site (can be index name of dataframe)
+ - ``method``: function name from ``rex.bias_correction`` module
+
+ The ``gid`` field should match the true resource ``gid`` regardless
+ of the optional ``gid_map`` input. Only ``windspeed`` **or**
+ ``GHI`` + ``DNI`` + ``DHI`` are corrected, depending on the
+ technology (wind for the former, PV or CSP for the latter). See the
+ functions in the ``rex.bias_correction`` module for available
+ inputs for ``method``. Any additional kwargs required for the
+ requested ``method`` can be input as additional columns in the
+ ``bias_correct`` table e.g., for linear bias correction functions
+ you can include ``scalar`` and ``adder`` inputs as columns in the
+ ``bias_correct`` table on a site-by-site basis. If ``None``, no
+ corrections are applied. By default, ``None``.
+ """
+
+ ifisinstance(bias_correct,type(None)):
+ returnbias_correct
+
+ ifisinstance(bias_correct,str):
+ bias_correct=pd.read_csv(bias_correct).rename(
+ SupplyCurveField.map_to(ResourceMetaField),axis=1
+ )
+
+ msg=(
+ "Bias correction data must be a filepath to csv or a dataframe "
+ "but received: {}".format(type(bias_correct))
+ )
+ assertisinstance(bias_correct,pd.DataFrame),msg
+
+ msg=(
+ "Bias correction table must have {!r} column but only found: "
+ "{}".format(ResourceMetaField.GID,list(bias_correct.columns))
+ )
+ assert(
+ ResourceMetaField.GIDinbias_correct
+ orbias_correct.index.name==ResourceMetaField.GID
+ ),msg
+
+ ifbias_correct.index.name!=ResourceMetaField.GID:
+ bias_correct=bias_correct.set_index(ResourceMetaField.GID)
+
+ msg=(
+ 'Bias correction table must have "method" column but only '
+ "found: {}".format(list(bias_correct.columns))
+ )
+ assert"method"inbias_correct,msg
+
+ returnbias_correct
+
+ def_parse_output_request(self,req):
+"""Set the output variables requested from generation.
+
+ Parameters
+ ----------
+ req : list | tuple
+ Output variables requested from SAM.
+
+ Returns
+ -------
+ output_request : list
+ Output variables requested from SAM.
+ """
+
+ output_request=super()._parse_output_request(req)
+
+ # ensure that cf_mean is requested from output
+ if"cf_mean"notinoutput_request:
+ output_request.append("cf_mean")
+
+ if_is_solar_run_with_ac_outputs(self.tech):
+ if"dc_ac_ratio"notinoutput_request:
+ output_request.append("dc_ac_ratio")
+ fordsetin["cf_mean","cf_profile"]:
+ ac_dset=f"{dset}_ac"
+ ifdsetinoutput_requestandac_dsetnotinoutput_request:
+ output_request.append(ac_dset)
+
+ forrequestinoutput_request:
+ ifrequestnotinself.OUT_ATTRS:
+ msg=(
+ 'User output request "{}" not recognized. '
+ "Will attempt to extract from PySAM.".format(request)
+ )
+ logger.debug(msg)
+
+ returnlist(set(output_request))
+
+ def_reduce_kwargs(self,pc,**kwargs):
+"""Reduce the global kwargs on a per-worker basis to reduce memory
+ footprint
+
+ Parameters
+ ----------
+ pc : PointsControl
+ PointsControl object for a single worker chunk
+ kwargs : dict
+ reV generation kwargs for all gids that needs to be reduced before
+ being sent to ``_run_single_worker()``
+
+ Returns
+ -------
+ kwargs : dict
+ Same as input but reduced just for the gids in pc
+ """
+
+ gids=pc.project_points.gids
+ gid_map=kwargs.get("gid_map",None)
+ bias_correct=kwargs.get("bias_correct",None)
+
+ ifbias_correctisnotNone:
+ ifgid_mapisnotNone:
+ gids=[gid_map[gid]forgidingids]
+
+ mask=bias_correct.index.isin(gids)
+ kwargs["bias_correct"]=bias_correct[mask]
+
+ returnkwargs
+
+
[docs]defrun(self,out_fpath=None,max_workers=1,timeout=1800,pool_size=None):
+"""Execute a parallel reV generation run with smart data flushing.
+
+ Parameters
+ ----------
+ out_fpath : str, optional
+ Path to output file. If ``None``, no output file will
+ be written. If the filepath is specified but the module name
+ (generation) and/or resource data year is not included, the
+ module name and/or resource data year will get added to the
+ output file name. By default, ``None``.
+ max_workers : int, optional
+ Number of local workers to run on. If ``None``, or if
+ running from the command line and omitting this argument
+ from your config file completely, this input is set to
+ ``os.cpu_count()``. Otherwise, the default is ``1``.
+ timeout : int, optional
+ Number of seconds to wait for parallel run iteration to
+ complete before returning zeros. By default, ``1800``
+ seconds.
+ pool_size : int, optional
+ Number of futures to submit to a single process pool for
+ parallel futures. If ``None``, the pool size is set to
+ ``os.cpu_count() * 2``. By default, ``None``.
+
+ Returns
+ -------
+ str | None
+ Path to output HDF5 file, or ``None`` if results were not
+ written to disk.
+ """
+ # initialize output file
+ self._init_fpath(out_fpath,module=ModuleName.GENERATION)
+ self._init_h5()
+ self._init_out_arrays()
+ ifpool_sizeisNone:
+ pool_size=os.cpu_count()*2
+
+ kwargs={
+ "tech":self.tech,
+ "res_file":self.res_file,
+ "lr_res_file":self.lr_res_file,
+ "output_request":self.output_request,
+ "scale_outputs":self.scale_outputs,
+ "gid_map":self._gid_map,
+ "nn_map":self._nn_map,
+ "bias_correct":self._bc,
+ }
+
+ logger.info(
+ "Running reV generation for: {}".format(self.points_control)
+ )
+ logger.debug(
+ 'The following project points were specified: "{}"'.format(
+ self.project_points
+ )
+ )
+ logger.debug(
+ "The following SAM configs are available to this run:\n{}".format(
+ pprint.pformat(self.sam_configs,indent=4)
+ )
+ )
+ logger.debug(
+ "The SAM output variables have been requested:\n{}".format(
+ self.output_request
+ )
+ )
+
+ # use serial or parallel execution control based on max_workers
+ try:
+ ifmax_workers==1:
+ logger.debug(
+ "Running serial generation for: {}".format(
+ self.points_control
+ )
+ )
+ fori,pc_subinenumerate(self.points_control):
+ self.out=self._run_single_worker(pc_sub,**kwargs)
+ logger.info(
+ "Finished reV gen serial compute for: {} "
+ "(iteration {} out of {})".format(
+ pc_sub,i+1,len(self.points_control)
+ )
+ )
+ self.flush()
+ else:
+ logger.debug(
+ "Running parallel generation for: {}".format(
+ self.points_control
+ )
+ )
+ self._parallel_run(
+ max_workers=max_workers,
+ pool_size=pool_size,
+ timeout=timeout,
+ **kwargs,
+ )
+
+ exceptExceptionase:
+ logger.exception("reV generation failed!")
+ raisee
+
+ returnself._out_fpath
+
+
+def_is_solar_run_with_ac_outputs(tech):
+"""True if tech is pvwattsv8+"""
+ if"pvwatts"notintech.casefold():
+ returnFalse
+ returntech.casefold()notin{f"pvwattsv{i}"foriinrange(8)}
+
[docs]classExclusionLayers:
+"""
+ Handler of .h5 file and techmap for Exclusion Layers
+ """
+
+ def__init__(self,h5_file,hsds=False):
+"""
+ Parameters
+ ----------
+ h5_file : str | list | tuple
+ .h5 file containing exclusion layers and techmap,
+ or a list of h5 files
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+ """
+
+ self.h5_file=h5_file
+
+ ifisinstance(h5_file,str):
+ self._h5=Resource(h5_file,hsds=hsds)
+ elifisinstance(h5_file,(list,tuple)):
+ self._h5=MultiFileResource(h5_file,check_files=False)
+ self._preflight_multi_file()
+ else:
+ msg=('Expected str, list, or tuple for h5_file input but '
+ 'received {}'.format(type(h5_file)))
+ logger.error(msg)
+ raiseTypeError(msg)
+
+ self._iarr=None
+
+ def__repr__(self):
+ msg="{} for {}".format(self.__class__.__name__,self.h5_file)
+
+ returnmsg
+
+ def__enter__(self):
+ returnself
+
+ def__exit__(self,type,value,traceback):
+ self.close()
+
+ iftypeisnotNone:
+ raise
+
+ def__len__(self):
+ returnlen(self.layers)
+
+ def__getitem__(self,keys):
+ ds,ds_slice=parse_keys(keys)
+
+ ifds.lower().startswith('lat'):
+ out=self._get_latitude(*ds_slice)
+ elifds.lower().startswith('lon'):
+ out=self._get_longitude(*ds_slice)
+ else:
+ out=self._get_layer(ds,*ds_slice)
+
+ returnout
+
+ def__contains__(self,layer):
+ returnlayerinself.layers
+
+ def_preflight_multi_file(self):
+"""Run simple multi-file exclusion checks."""
+ lat_shape=self.h5.shapes[LATITUDE]
+ lon_shape=self.h5.shapes[LONGITUDE]
+ forlayerinself.layers:
+ lshape=self.h5.shapes[layer]
+ lshape=lshape[1:]iflen(lshape)>2elselshape
+ iflshape!=lon_shapeorlshape!=lat_shape:
+ msg=('Shape of layer "{}" is {} which does not match '
+ 'latitude and longitude shapes of {} and {}. '
+ 'Check your exclusion file inputs: {}'
+ .format(layer,self.h5.shapes[layer],
+ lat_shape,lon_shape,self.h5._h5_files))
+ logger.error(msg)
+ raiseMultiFileExclusionError(msg)
+
+ check_attrs=('height','width','crs','transform')
+ base_profile={}
+ forfpinself.h5_file:
+ withExclusionLayers(fp)asf:
+ ifnotbase_profile:
+ base_profile=f.profile
+ else:
+ forattrincheck_attrs:
+ ifattrnotinbase_profileorattrnotinf.profile:
+ msg=('Multi-file exclusion inputs from {} '
+ 'dont have profiles with height, width, '
+ 'crs, and transform: {} and {}'
+ .format(self.h5_file,base_profile,
+ f.profile))
+ logger.error(msg)
+ raiseMultiFileExclusionError(msg)
+
+ base_attr=base_profile[attr]
+ file_attr=f.profile[attr]
+ attrs_are_str=(isinstance(base_attr,str)
+ andisinstance(file_attr,str))
+ ifattr=='crs'andattrs_are_str:
+ attrs_match=(set(base_attr.split(' '))
+ ==set(file_attr.split(' ')))
+ else:
+ attrs_match=base_profile[attr]==f.profile[attr]
+
+ ifnotattrs_match:
+ msg=('Multi-file exclusion inputs from {} '
+ 'dont have matching "{}": {} and {}'
+ .format(self.h5_file,attr,
+ base_profile[attr],
+ f.profile[attr]))
+ logger.error(msg)
+ raiseMultiFileExclusionError(msg)
+
+
[docs]defclose(self):
+"""
+ Close h5 instance
+ """
+ self._h5.close()
+
+ @property
+ defh5(self):
+"""
+ Open h5py File instance.
+
+ Returns
+ -------
+ h5 : rex.MultiFileResource | rex.Resource
+ """
+ returnself._h5
+
+ @property
+ defiarr(self):
+"""Get an array of 1D index values for the flattened h5 excl extent.
+
+ Returns
+ -------
+ iarr : np.ndarray
+ Uint array with same shape as exclusion extent, representing the 1D
+ index values if the geotiff extent was flattened
+ (with default flatten order 'C')
+ """
+ ifself._iarrisNone:
+ N=self.shape[0]*self.shape[1]
+ self._iarr=np.arange(N,dtype=np.uint32)
+ self._iarr=self._iarr.reshape(self.shape)
+
+ returnself._iarr
+
+ @property
+ defprofile(self):
+"""
+ GeoTiff profile for exclusions
+
+ Returns
+ -------
+ profile : dict
+ """
+ returnjson.loads(self.h5.global_attrs['profile'])
+
+ @property
+ defcrs(self):
+"""
+ GeoTiff projection crs
+
+ Returns
+ -------
+ str
+ """
+ returnself.profile['crs']
+
+ @property
+ defpixel_area(self):
+"""Get pixel area in km2 from the transform profile of the excl file.
+
+ Returns
+ -------
+ area : float
+ Exclusion pixel area in km2. Will return None if the
+ appropriate transform attribute is not found.
+ """
+
+ area=None
+ if'transform'inself.profile:
+ transform=self.profile['transform']
+ area=np.abs(transform[0]*transform[4])
+ area/=1000**2
+
+ returnarea
+
+ @property
+ deflayers(self):
+"""
+ Available exclusions layers
+
+ Returns
+ -------
+ layers : list
+ """
+ layers=self.h5.datasets
+
+ returnlayers
+
+ @property
+ defshape(self):
+"""
+ Exclusion shape (latitude, longitude)
+
+ Returns
+ -------
+ shape : tuple
+ """
+ shape=self.h5.attrs.get('shape',None)
+ ifshapeisNone:
+ shape=self.h5.shapes[LATITUDE]
+
+ returntuple(shape)
+
+ @property
+ defchunks(self):
+"""
+ Exclusion layers chunks default chunk size
+
+ Returns
+ -------
+ chunks : tuple | None
+ Chunk size of exclusion layers
+ """
+ chunks=self.h5.attrs.get('chunks',None)
+ ifchunksisNone:
+ chunks=self.h5.chunks[LATITUDE]
+
+ returnchunks
+
+ @property
+ deflatitude(self):
+"""
+ Latitude coordinates array
+
+ Returns
+ -------
+ ndarray
+ """
+ returnself[LATITUDE]
+
+ @property
+ deflongitude(self):
+"""
+ Longitude coordinates array
+
+ Returns
+ -------
+ ndarray
+ """
+ returnself[LONGITUDE]
+
+
[docs]defget_layer_profile(self,layer):
+"""
+ Get profile for a specific exclusion layer
+
+ Parameters
+ ----------
+ layer : str
+ Layer to get profile for
+
+ Returns
+ -------
+ profile : dict | None
+ GeoTiff profile for single exclusion layer
+ """
+ profile=self.h5.get_attrs(dset=layer).get('profile',None)
+ ifprofileisnotNone:
+ profile=json.loads(profile)
+
+ returnprofile
+
+
[docs]defget_layer_crs(self,layer):
+"""
+ Get crs for a specific exclusion layer
+
+ Parameters
+ ----------
+ layer : str
+ Layer to get profile for
+
+ Returns
+ -------
+ crs : str | None
+ GeoTiff projection crs
+ """
+ profile=self.get_layer_profile(layer)
+ ifprofileisnotNone:
+ crs=profile['crs']
+ else:
+ crs=None
+
+ returncrs
+
+
[docs]defget_layer_values(self,layer):
+"""
+ Get values for given layer in Geotiff format (bands, y, x)
+
+ Parameters
+ ----------
+ layer : str
+ Layer to get values for
+
+ Returns
+ -------
+ values : ndarray
+ GeoTiff values for single exclusion layer
+ """
+ values=self.h5[layer]
+
+ returnvalues
+
+
[docs]defget_layer_description(self,layer):
+"""
+ Get description for given layer
+
+ Parameters
+ ----------
+ layer : str
+ Layer to get description for
+
+ Returns
+ -------
+ description : str
+ Description of layer
+ """
+ description=self.h5.get_attrs(dset=layer).get('description',None)
+
+ returndescription
+
+
[docs]defget_nodata_value(self,layer):
+"""
+ Get the nodata value for a given layer
+
+ Parameters
+ ----------
+ layer : str
+ Layer to get nodata value for
+
+ Returns
+ -------
+ nodata : int | float | None
+ nodata value for layer or None if not found
+ """
+ profile=self.get_layer_profile(layer)
+ nodata=profile.get('nodata',None)
+
+ returnnodata
+
+ def_get_latitude(self,*ds_slice):
+"""
+ Extract latitude coordinates
+
+ Parameters
+ ----------
+ ds_slice : tuple of int | list | slice
+ Pandas slicing describing which sites and columns to extract
+
+ Returns
+ -------
+ lat : ndarray
+ Latitude coordinates
+ """
+ ifLATITUDEnotinself.h5:
+ msg=('"latitude" is missing from {}'
+ .format(self.h5_file))
+ logger.error(msg)
+ raiseHandlerKeyError(msg)
+
+ ds_slice=(LATITUDE,)+ds_slice
+
+ lat=self.h5[ds_slice]
+
+ returnlat
+
+ def_get_longitude(self,*ds_slice):
+"""
+ Extract longitude coordinates
+
+ Parameters
+ ----------
+ ds_slice : tuple of int | list | slice
+ Pandas slicing describing which sites and columns to extract
+
+ Returns
+ -------
+ lon : ndarray
+ Longitude coordinates
+ """
+ ifLONGITUDEnotinself.h5:
+ msg=('"longitude" is missing from {}'
+ .format(self.h5_file))
+ logger.error(msg)
+ raiseHandlerKeyError(msg)
+
+ ds_slice=(LONGITUDE,)+ds_slice
+
+ lon=self.h5[ds_slice]
+
+ returnlon
+
+ def_get_layer(self,layer_name,*ds_slice):
+"""
+ Extract data from given dataset
+
+ Parameters
+ ----------
+ layer_name : str
+ Exclusion layer to extract
+ ds_slice : tuple of int | list | slice
+ tuple describing slice of layer array to extract
+
+ Returns
+ -------
+ layer_data : ndarray
+ Array of exclusion data
+ """
+ iflayer_namenotinself.layers:
+ msg=('{} not in available layers: {}'
+ .format(layer_name,self.layers))
+ logger.error(msg)
+ raiseHandlerKeyError(msg)
+
+ shape=self.h5.get_dset_properties(layer_name)[0]
+ iflen(shape)==3:
+ ds_slice=(layer_name,0)+ds_slice
+ else:
+ ds_slice=(layer_name,)+ds_slice
+
+ layer_data=self.h5[ds_slice]
+
+ returnlayer_data
[docs]classMultiYearGroup:
+"""
+ Handle group parameters
+ """
+
+ def__init__(self,name,out_dir,source_files=None,
+ source_dir=None,source_prefix=None,
+ source_pattern=None,
+ dsets=('cf_mean',),pass_through_dsets=None):
+"""
+ Parameters
+ ----------
+ name : str
+ Group name. Can be ``"none"`` for no collection groups.
+ out_dir : str
+ Output directory - used for Pipeline handling.
+ source_files : str | list, optional
+ Explicit list of source files. Use either this input *OR*
+ `source_dir` + `source_prefix`. If this input is
+ ``"PIPELINE"``, the `source_files` input is determined from
+ the status file of the previous pipeline step.
+ If ``None``, use `source_dir` and `source_prefix`.
+ By default, ``None``.
+ source_dir : str, optional
+ Directory to extract source files from (must be paired with
+ `source_prefix`). By default, ``None``.
+ source_prefix : str, optional
+ File prefix to search for in source directory (must be
+ paired with `source_dir`). By default, ``None``.
+ source_pattern : str, optional
+ Optional unix-style ``/filepath/pattern*.h5`` to specify the
+ source files. This takes priority over `source_dir` and
+ `source_prefix` but is not used if `source_files` are
+ specified explicitly. By default, ``None``.
+ dsets : str | list | tuple, optional
+ List of datasets to collect. This can be set to
+ ``"PIPELINE"`` if running from the command line as part of a
+ reV pipeline. In this case, all the datasets from the
+ previous pipeline step will be collected.
+ By default, ``('cf_mean',)``.
+ pass_through_dsets : list | tuple, optional
+ Optional list of datasets that are identical in the
+ multi-year files (e.g. input datasets that don't vary from
+ year to year) that should be copied to the output multi-year
+ file once without a year suffix or means/stdev calculation.
+ By default, ``None``.
+ """
+ self._name=name
+ self._dirout=out_dir
+ self._source_files=source_files
+ self._source_dir=source_dir
+ self._source_prefix=source_prefix
+ self._source_pattern=source_pattern
+ self._pass_through_dsets=None
+ self._dsets=None
+
+ self._parse_pass_through_dsets(dsets,pass_through_dsetsor[])
+ self._parse_dsets(dsets)
+
+ def_parse_pass_through_dsets(self,dsets,pass_through_dsets):
+"""Parse a multi-year pass-through dataset collection request.
+
+ Parameters
+ ----------
+ dsets : str | list
+ One or more datasets to collect, or "PIPELINE"
+ pass_through_dsets : list
+ List of pass through datasets.
+ """
+ ifisinstance(dsets,str)anddsets=='PIPELINE':
+ files=parse_previous_status(self._dirout,ModuleName.MULTI_YEAR)
+ withResource(files[0])asres:
+ dsets=res.datasets
+
+ if"lcoe_fcr"indsets:
+ fordsetinLCOE_REQUIRED_OUTPUTS:
+ ifdsetnotinpass_through_dsetsanddsetindsets:
+ pass_through_dsets.append(dset)
+
+ if"dc_ac_ratio"indsets:
+ if"dc_ac_ratio"notinpass_through_dsets:
+ pass_through_dsets.append("dc_ac_ratio")
+
+ self._pass_through_dsets=SAMOutputRequest(pass_through_dsets)
+
+ def_parse_dsets(self,dsets):
+"""Parse a multi-year dataset collection request. Can handle PIPELINE
+ argument which will find all datasets from one of the files being
+ collected ignoring meta, time index, and pass_through_dsets
+
+ Parameters
+ ----------
+ dsets : str | list
+ One or more datasets to collect, or "PIPELINE"
+ """
+ ifisinstance(dsets,str)anddsets=='PIPELINE':
+ files=parse_previous_status(self._dirout,ModuleName.MULTI_YEAR)
+ withResource(files[0])asres:
+ dsets=[dfordinres
+ ifnotd.startswith('time_index')
+ andd!='meta'
+ anddnotinself.pass_through_dsets]
+
+ self._dsets=SAMOutputRequest(dsets)
+
+ @property
+ defname(self):
+"""
+ Returns
+ -------
+ name : str
+ Group name
+ """
+ name=self._nameifself._name.lower()!="none"elseNone
+ returnname
+
+ @property
+ defsource_files(self):
+"""
+ Returns
+ -------
+ source_files : list
+ list of source files to collect from
+ """
+ ifself._source_filesisnotNone:
+ ifisinstance(self._source_files,(list,tuple)):
+ source_files=self._source_files
+ elifself._source_files=="PIPELINE":
+ source_files=parse_previous_status(self._dirout,
+ ModuleName.MULTI_YEAR)
+ else:
+ e="source_files must be a list, tuple, or 'PIPELINE'"
+ logger.error(e)
+ raiseConfigError(e)
+
+ elifself._source_pattern:
+ source_files=glob.glob(self._source_pattern)
+ ifnotall(fp.endswith('.h5')forfpinsource_files):
+ msg=('Source pattern resulted in non-h5 files that cannot '
+ 'be collected: {}, pattern: {}'
+ .format(source_files,self._source_pattern))
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ elifself._source_dirandself._source_prefix:
+ source_files=[]
+ forfileinos.listdir(self._source_dir):
+ if(file.startswith(self._source_prefix)
+ andfile.endswith('.h5')and'_node'notinfile):
+ source_files.append(os.path.join(self._source_dir,
+ file))
+ else:
+ e=("source_files or both source_dir and "
+ "source_prefix must be provided")
+ logger.error(e)
+ raiseConfigError(e)
+
+ ifnotany(source_files):
+ e=('Could not find any source files for '
+ 'multi-year collection group: "{}" in "{}"'
+ .format(self.name,self._source_dir))
+ logger.error(e)
+ raiseFileNotFoundError(e)
+
+ returnsource_files
+
+ @property
+ defdsets(self):
+"""
+ Returns
+ -------
+ _dsets :list | tuple
+ Datasets to collect
+ """
+ returnself._dsets
+
+ @property
+ defpass_through_dsets(self):
+"""Optional list of datasets that are identical in the multi-year
+ files (e.g. input datasets that don't vary from year to year) that
+ should be copied to the output multi-year file once without a
+ year suffix or means/stdev calculation
+
+ Returns
+ -------
+ list | tuple | None
+ """
+ returnself._pass_through_dsets
+
+ def_dict_rep(self):
+"""Get a dictionary representation of this multi year collection group
+
+ Returns
+ -------
+ dict
+ """
+ props=get_class_properties(self.__class__)
+ out={k:getattr(self,k)forkinprops}
+ out['group']=self.name
+ returnout
+
+ @classmethod
+ def_factory(cls,out_dir,groups_dict):
+"""
+ Generate dictionary of MultiYearGroup objects for all groups in groups
+
+ Parameters
+ ----------
+ out_dir : str
+ Output directory, used for Pipeline handling
+ groups_dict : dict
+ Dictionary of group parameters, parsed from multi-year config file
+
+ Returns
+ -------
+ groups : dict
+ Dictionary of MultiYearGroup objects for each group in groups
+ """
+ groups={}
+ forname,kwargsingroups_dict.items():
+ groups[name]=cls(name,out_dir,**kwargs)
+
+ returngroups
+
+
+
[docs]classMultiYear(Outputs):
+"""
+ Class to handle multiple years of data and:
+ - collect datasets from multiple years
+ - compute multi-year means
+ - compute multi-year standard deviations
+ - compute multi-year coefficient of variations
+
+ """
+
+ def__init__(self,h5_file,group=None,unscale=True,mode='r',
+ str_decode=True):
+"""
+ Parameters
+ ----------
+ h5_file : str
+ Path to .h5 resource file
+ group : str
+ Group to collect datasets into
+ unscale : bool
+ Boolean flag to automatically unscale variables on extraction
+ mode : str
+ Mode to instantiate h5py.File instance
+ str_decode : bool
+ Boolean flag to decode the bytestring meta data into normal
+ strings. Setting this to False will speed up the meta data read.
+ """
+ log_versions(logger)
+ super().__init__(h5_file,group=group,unscale=unscale,mode=mode,
+ str_decode=str_decode)
+
+ @staticmethod
+ def_create_dset_name(source_h5,dset):
+"""
+ Create output dataset name by parsing year from source_h5 and
+ appending to source dataset name.
+
+ Parameters
+ ----------
+ source_h5 : str
+ Path to source .h5 file to copy data from
+ dset : str
+ Dataset to copy
+
+ Returns
+ -------
+ dset_out : str
+ Ouput dataset name
+ """
+ f_name=os.path.basename(source_h5)
+ year=parse_year(f_name)
+ dset_out="{}-{}".format(dset,year)
+ returndset_out
+
+ def_copy_time_index(self,source_h5):
+"""
+ Copy time_index from source_h5 to time_index-{year} in multiyear .h5
+
+ Parameters
+ ----------
+ source_h5 : str
+ Path to source .h5 file to copy data from
+ """
+ dset_out=self._create_dset_name(source_h5,'time_index')
+ ifdset_outnotinself.datasets:
+ logger.debug("- Collecting time_index from {}"
+ .format(os.path.basename(source_h5)))
+ withOutputs(source_h5,mode='r')asf_in:
+ time_index=f_in.h5['time_index'][...]
+
+ self._create_dset(dset_out,time_index.shape,time_index.dtype,
+ data=time_index)
+
+ def_copy_dset(self,source_h5,dset,meta=None,pass_through=False):
+"""
+ Copy dset_in from source_h5 to multiyear .h5
+
+ Parameters
+ ----------
+ source_h5 : str
+ Path to source .h5 file to copy data from
+ dset : str
+ Dataset to copy
+ meta : pandas.DataFrame
+ If provided confirm that source meta matches given meta
+ pass_through : bool
+ Flag to just pass through dataset without name modifications
+ (no differences between years, no means or stdevs)
+ """
+ ifpass_through:
+ dset_out=dset
+ else:
+ dset_out=self._create_dset_name(source_h5,dset)
+
+ ifdset_outnotinself.datasets:
+ logger.debug("- Collecting {} from {}"
+ .format(dset,os.path.basename(source_h5)))
+ withOutputs(source_h5,unscale=False,mode='r')asf_in:
+ ifmetaisnotNone:
+ cols=get_lat_lon_cols(meta)
+ source_meta=f_in.meta
+
+ iflen(meta)!=len(source_meta):
+ msg=('Meta data has different lengths between '
+ 'collection files! Found {} and {}'
+ .format(len(meta),len(source_meta)))
+ logger.error(msg)
+ raiseHandlerRuntimeError(msg)
+
+ ifnotnp.allclose(meta[cols],source_meta[cols]):
+ msg=('Coordinates do not match between '
+ 'collection files!')
+ logger.warning(msg)
+ warn(msg)
+
+ _,ds_dtype,ds_chunks=f_in.get_dset_properties(dset)
+ ds_attrs=f_in.get_attrs(dset=dset)
+ ds_data=f_in[dset]
+
+ self._create_dset(dset_out,ds_data.shape,ds_dtype,
+ chunks=ds_chunks,attrs=ds_attrs,data=ds_data)
+
+
[docs]@staticmethod
+ defparse_source_files_pattern(source_files):
+"""Parse a source_files pattern that can be either an explicit list of
+ source files or a unix-style /filepath/pattern*.h5 and either way
+ return a list of explicit filepaths.
+
+ Parameters
+ ----------
+ source_files : list | str
+ List of .h5 files to collect datasets from. This can also be a
+ unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+ however all resulting files must be .h5 otherwise an exception will
+ be raised. NOTE: .h5 file names must indicate the year the data
+ pertains to
+
+ Returns
+ -------
+ source_files : list
+ List of .h5 filepaths.
+ """
+
+ ifisinstance(source_files,str)and'*'insource_files:
+ source_files=glob.glob(source_files)
+ elifisinstance(source_files,str):
+ source_files=[source_files]
+ elifnotisinstance(source_files,(list,tuple)):
+ msg=('Cannot recognize source_files type: {}{}'
+ .format(source_files,type(source_files)))
+ logger.error(msg)
+ raiseTypeError(msg)
+
+ ifnotall(fp.endswith('.h5')forfpinsource_files):
+ msg=('Non-h5 files cannot be collected: {}'.format(source_files))
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ returnsource_files
+
+
[docs]defcollect(self,source_files,dset,profiles=False,pass_through=False):
+"""
+ Collect dataset dset from given list of h5 files
+
+ Parameters
+ ----------
+ source_files : list | str
+ List of .h5 files to collect datasets from. This can also be a
+ unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+ however all resulting files must be .h5 otherwise an exception will
+ be raised. NOTE: .h5 file names must indicate the year the data
+ pertains to
+ dset : str
+ Dataset to collect
+ profiles : bool
+ Boolean flag to indicate if profiles are being collected
+ If True also collect time_index
+ pass_through : bool
+ Flag to just pass through dataset without name modifications
+ (no differences between years, no means or stdevs)
+ """
+ source_files=self.parse_source_files_pattern(source_files)
+ withOutputs(source_files[0],mode='r')asf_in:
+ meta=f_in.h5['meta'][...]
+
+ if'meta'notinself.datasets:
+ logger.debug("Copying meta")
+ self._create_dset('meta',meta.shape,meta.dtype,
+ data=meta)
+
+ meta=pd.DataFrame(meta)
+ foryear_h5insource_files:
+ ifprofiles:
+ self._copy_time_index(year_h5)
+
+ self._copy_dset(year_h5,dset,meta=meta,
+ pass_through=pass_through)
+
+ def_get_source_dsets(self,dset_out):
+"""
+ Extract all available annual datasets associated with dset
+
+ Parameters
+ ----------
+ dset_out : str
+ Output dataset to find source datasets for
+
+ Returns
+ -------
+ source_dsets : list
+ List of annual datasets
+ """
+ dset=os.path.basename(dset_out).split("-")[0]
+ logger.debug('-- source_dset root = {}'.format(dset))
+ my_dset=["{}-{}".format(dset,val)forvalin['means','stdev']]
+ source_dsets=[dsfordsinself.datasetsifdsetinds
+ anddsnotinmy_dset]
+ ifdset_outinsource_dsets:
+ source_dsets.remove(dset_out)
+
+ returnsource_dsets
+
+ def_update_dset(self,dset_out,dset_data):
+"""
+ Update dataset, create if needed
+
+ Parameters
+ ----------
+ dset_out : str
+ Dataset name
+ dset_data : ndarray
+ Dataset data to write to disc
+ """
+ ifdset_outinself.datasets:
+ logger.debug("- Updating {}".format(dset_out))
+ self[dset_out]=dset_data
+ else:
+ logger.debug("- Creating {}".format(dset_out))
+ source_dset=self._get_source_dsets(dset_out)[0]
+ _,ds_dtype,ds_chunks=self.get_dset_properties(source_dset)
+ ds_attrs=self.get_attrs(dset=source_dset)
+ self._add_dset(dset_out,dset_data,ds_dtype,
+ chunks=ds_chunks,attrs=ds_attrs)
+
+ def_compute_means(self,dset_out):
+"""
+ Compute multi-year means for given dataset
+
+ Parameters
+ ----------
+ dset_out : str
+ Multi-year means dataset name
+
+ Returns
+ -------
+ my_means : ndarray
+ Array of multi-year means
+ """
+ source_dsets=self._get_source_dsets(dset_out)
+ logger.debug('\t- Computing {} from {}'.format(dset_out,source_dsets))
+
+ my_means=np.zeros(len(self),dtype='float32')
+ fordsinsource_dsets:
+ ifself.h5[ds].shape==my_means.shape:
+ my_means+=self[ds]
+ else:
+ raiseHandlerRuntimeError("{} shape {} should be {}"
+ .format(ds,self.h5[ds].shape,
+ my_means.shape))
+ my_means/=len(source_dsets)
+ self._update_dset(dset_out,my_means)
+
+ returnmy_means
+
+
[docs]defmeans(self,dset):
+"""
+ Extract or compute multi-year means for given source dset
+
+ Parameters
+ ----------
+ dset : str
+ Dataset of interest
+
+ Returns
+ -------
+ my_means : ndarray
+ Array of multi-year means for dataset of interest
+ """
+ my_dset="{}-means".format(dset)
+ ifmy_dsetinself.datasets:
+ my_means=self[my_dset]
+ else:
+ my_means=self._compute_means(my_dset)
+
+ returnmy_means
+
+ def_compute_stdev(self,dset_out,means=None):
+"""
+ Compute multi-year standard deviation for given dataset
+
+ Parameters
+ ----------
+ dset_out : str
+ Multi-year stdev dataset name
+ means : ndarray
+ Array of pre-computed means
+
+ Returns
+ -------
+ my_stdev : ndarray
+ Array of multi-year standard deviations
+ """
+ ifmeansisNone:
+ means=self._compute_means("{}-means".format(dset_out))
+
+ source_dsets=self._get_source_dsets(dset_out)
+
+ my_stdev=np.zeros(means.shape,dtype='float32')
+ fordsinsource_dsets:
+ ifself.h5[ds].shape==my_stdev.shape:
+ my_stdev+=(self[ds]-means)**2
+ else:
+ raiseHandlerRuntimeError("{} shape {} should be {}"
+ .format(ds,self.h5[ds].shape,
+ my_stdev.shape))
+
+ my_stdev=np.sqrt(my_stdev/len(source_dsets))
+ self._update_dset(dset_out,my_stdev)
+
+ returnmy_stdev
+
+
[docs]defstdev(self,dset):
+"""
+ Extract or compute multi-year standard deviation for given source dset
+
+ Parameters
+ ----------
+ dset : str
+ Dataset of interest
+
+ Returns
+ -------
+ my_stdev : ndarray
+ Array of multi-year standard deviation for dataset of interest
+ """
+ my_dset="{}-stdev".format(dset)
+ ifmy_dsetinself.datasets:
+ my_stdev=self[my_dset]
+ else:
+ my_means=self.means(dset)
+ my_stdev=self._compute_stdev(my_dset,means=my_means)
+
+ returnmy_stdev
+
+
[docs]defCV(self,dset):
+"""
+ Extract or compute multi-year coefficient of variation for given
+ source dset
+
+ Parameters
+ ----------
+ dset : str
+ Dataset of interest
+
+ Returns
+ -------
+ my_cv : ndarray
+ Array of multi-year coefficient of variation for
+ dataset of interest
+ """
+ my_cv=self.stdev(dset)/self.means(dset)
+ returnmy_cv
+
+
[docs]@classmethod
+ defis_profile(cls,source_files,dset):
+"""
+ Check dataset in source files to see if it is a profile.
+
+ Parameters
+ ----------
+ source_files : list | str
+ List of .h5 files to collect datasets from. This can also be a
+ unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+ however all resulting files must be .h5 otherwise an exception will
+ be raised. NOTE: .h5 file names must indicate the year the data
+ pertains to
+ dset : str
+ Dataset to collect
+
+ Returns
+ -------
+ is_profile : bool
+ True if profile, False if not.
+ """
+ source_files=cls.parse_source_files_pattern(source_files)
+ withOutputs(source_files[0])asf:
+ ifdsetnotinf.datasets:
+ raiseKeyError('Dataset "{}" not found in source file: "{}"'
+ .format(dset,source_files[0]))
+
+ shape,_,_=f.get_dset_properties(dset)
+
+ returnlen(shape)==2
+
+
[docs]@classmethod
+ defpass_through(cls,my_file,source_files,dset,group=None):
+"""
+ Pass through a dataset that is identical in all source files to a
+ dataset of the same name in the output multi-year file.
+
+ Parameters
+ ----------
+ my_file : str
+ Path to multi-year .h5 file
+ source_files : list | str
+ List of .h5 files to collect datasets from. This can also be a
+ unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+ however all resulting files must be .h5 otherwise an exception will
+ be raised. NOTE: .h5 file names must indicate the year the data
+ pertains to
+ dset : str
+ Dataset to pass through (will also be the name of the output
+ dataset in my_file)
+ group : str
+ Group to collect datasets into
+ """
+ source_files=cls.parse_source_files_pattern(source_files)
+ logger.info('Passing through {} into {}.'
+ .format(dset,my_file))
+ withcls(my_file,mode='a',group=group)asmy:
+ my.collect(source_files,dset,pass_through=True)
+
+
[docs]@classmethod
+ defcollect_means(cls,my_file,source_files,dset,group=None):
+"""
+ Collect and compute multi-year means for given dataset
+
+ Parameters
+ ----------
+ my_file : str
+ Path to multi-year .h5 file
+ source_files : list | str
+ List of .h5 files to collect datasets from. This can also be a
+ unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+ however all resulting files must be .h5 otherwise an exception will
+ be raised. NOTE: .h5 file names must indicate the year the data
+ pertains to
+ dset : str
+ Dataset to collect
+ group : str
+ Group to collect datasets into
+ """
+ logger.info('Collecting {} into {} '
+ 'and computing multi-year means and standard deviations.'
+ .format(dset,my_file))
+ source_files=cls.parse_source_files_pattern(source_files)
+ withcls(my_file,mode='a',group=group)asmy:
+ my.collect(source_files,dset)
+ means=my._compute_means("{}-means".format(dset))
+ my._compute_stdev("{}-stdev".format(dset),means=means)
+
+
[docs]@classmethod
+ defcollect_profiles(cls,my_file,source_files,dset,group=None):
+"""
+ Collect multi-year profiles associated with given dataset
+
+ Parameters
+ ----------
+ my_file : str
+ Path to multi-year .h5 file
+ source_files : list | str
+ List of .h5 files to collect datasets from. This can also be a
+ unix-style /filepath/pattern*.h5 to find .h5 files to collect,
+ however all resulting files must be .h5 otherwise an exception will
+ be raised. NOTE: .h5 file names must indicate the year the data
+ pertains to
+ dset : str
+ Profiles dataset to collect
+ group : str
+ Group to collect datasets into
+ """
+ logger.info('Collecting {} into {}'.format(dset,my_file))
+ source_files=cls.parse_source_files_pattern(source_files)
+ withcls(my_file,mode='a',group=group)asmy:
+ my.collect(source_files,dset,profiles=True)
+
+
+
[docs]defmy_collect_groups(out_fpath,groups,clobber=True):
+"""Collect all groups into a single multi-year HDF5 file.
+
+ ``reV`` multi-year combines ``reV`` generation data from multiple
+ years (typically stored in separate files) into a single multi-year
+ file. Each dataset in the multi-year file is labeled with the
+ corresponding years, and multi-year averages of the yearly datasets
+ are also computed.
+
+ Parameters
+ ----------
+ out_fpath : str
+ Path to multi-year HDF5 file to use for multi-year
+ collection.
+ groups : dict
+ Dictionary of collection groups and their parameters. This
+ should be a dictionary mapping group names (keys) to a set
+ of key word arguments (values) that can be used to initialize
+ :class:`~reV.handlers.multi_year.MultiYearGroup` (excluding the
+ required ``name`` and ``out_dir`` inputs, which are populated
+ automatically). For example::
+
+ groups = {
+ "none": {
+ "dsets": [
+ "cf_profile",
+ "cf_mean",
+ "ghi_mean",
+ "lcoe_fcr",
+ ],
+ "source_dir": "./",
+ "source_prefix": "",
+ "pass_through_dsets": [
+ "capital_cost",
+ "fixed_operating_cost",
+ "system_capacity",
+ "fixed_charge_rate",
+ "variable_operating_cost",
+ ]
+ },
+ "solar_group": {
+ "source_files": "PIPELINE",
+ "dsets": [
+ "cf_profile_ac",
+ "cf_mean_ac",
+ "ac",
+ "dc",
+ "clipped_power"
+ ],
+ "pass_through_dsets": [
+ "system_capacity_ac",
+ "dc_ac_ratio"
+ ]
+ },
+ ...
+ }
+
+ The group names will be used as the HDF5 file group name under
+ which the collected data will be stored. You can have exactly
+ one group with the name ``"none"`` for a "no group" collection
+ (this is typically what you want and all you need to specify).
+ clobber : bool, optional
+ Flag to purge the multi-year output file prior to running the
+ multi-year collection step if the file already exists on disk.
+ This ensures the data is always freshly collected from the
+ single-year files. If ``False``, then datasets in the existing
+ file will **not** be overwritten with (potentially new/updated)
+ data from the single-year files. By default, ``True``.
+ """
+ ifnotout_fpath.endswith(".h5"):
+ out_fpath='{}.h5'.format(out_fpath)
+
+ ifclobberandos.path.exists(out_fpath):
+ msg=('Found existing multi-year file: "{}". Removing...'
+ .format(str(out_fpath)))
+ logger.warning(msg)
+ warn(msg)
+ os.remove(out_fpath)
+
+ out_dir=os.path.dirname(out_fpath)
+ groups=MultiYearGroup._factory(out_dir,groups)
+ group_params={name:group._dict_rep()
+ forname,groupingroups.items()}
+
+ logger.info('Multi-year collection is being run with output path: {}'
+ .format(out_fpath))
+ ts=time.time()
+ forgroup_name,groupingroup_params.items():
+ logger.info('- Collecting datasets "{}" from "{}" into "{}/"'
+ .format(group['dsets'],group['source_files'],
+ group_name))
+ t0=time.time()
+ fordsetingroup['dsets']:
+ ifMultiYear.is_profile(group['source_files'],dset):
+ MultiYear.collect_profiles(out_fpath,group['source_files'],
+ dset,group=group['group'])
+ else:
+ MultiYear.collect_means(out_fpath,group['source_files'],
+ dset,group=group['group'])
+
+ pass_through_dsets=group.get('pass_through_dsets')or[]
+ fordsetinpass_through_dsets:
+ MultiYear.pass_through(out_fpath,group['source_files'],
+ dset,group=group['group'])
+
+ runtime=(time.time()-t0)/60
+ logger.info('- {} collection completed in: {:.2f} min.'
+ .format(group_name,runtime))
+
+ runtime=(time.time()-ts)/60
+ logger.info('Multi-year collection completed in : {:.2f} min.'
+ .format(runtime))
+
+ returnout_fpath
[docs]classOutputs(rexOutputs):
+"""
+ Base class to handle reV output data in .h5 format
+
+ Examples
+ --------
+ The reV Outputs handler can be used to initialize h5 files in the standard
+ reV/rex resource data format.
+
+ >>> from reV import Outputs
+ >>> import pandas as pd
+ >>> import numpy as np
+ >>>
+ >>> meta = pd.DataFrame({SupplyCurveField.LATITUDE: np.ones(100),
+ >>> SupplyCurveField.LONGITUDE: np.ones(100)})
+ >>>
+ >>> time_index = pd.date_range('20210101', '20220101', freq='1h',
+ >>> closed='right')
+ >>>
+ >>> with Outputs('test.h5', 'w') as f:
+ >>> f.meta = meta
+ >>> f.time_index = time_index
+
+ You can also use the Outputs handler to read output h5 files from disk.
+ The Outputs handler will automatically parse the meta data and time index
+ into the expected pandas objects (DataFrame and DatetimeIndex,
+ respectively).
+
+ >>> with Outputs('test.h5') as f:
+ >>> print(f.meta.head())
+ >>>
+ latitude longitude
+ gid
+ 0 1.0 1.0
+ 1 1.0 1.0
+ 2 1.0 1.0
+ 3 1.0 1.0
+ 4 1.0 1.0
+
+ >>> with Outputs('test.h5') as f:
+ >>> print(f.time_index)
+ DatetimeIndex(['2021-01-01 01:00:00+00:00', '2021-01-01 02:00:00+00:00',
+ '2021-01-01 03:00:00+00:00', '2021-01-01 04:00:00+00:00',
+ '2021-01-01 05:00:00+00:00', '2021-01-01 06:00:00+00:00',
+ '2021-01-01 07:00:00+00:00', '2021-01-01 08:00:00+00:00',
+ '2021-01-01 09:00:00+00:00', '2021-01-01 10:00:00+00:00',
+ ...
+ '2021-12-31 15:00:00+00:00', '2021-12-31 16:00:00+00:00',
+ '2021-12-31 17:00:00+00:00', '2021-12-31 18:00:00+00:00',
+ '2021-12-31 19:00:00+00:00', '2021-12-31 20:00:00+00:00',
+ '2021-12-31 21:00:00+00:00', '2021-12-31 22:00:00+00:00',
+ '2021-12-31 23:00:00+00:00', '2022-01-01 00:00:00+00:00'],
+ dtype='datetime64[ns, UTC]', length=8760, freq=None)
+
+ There are a few ways to use the Outputs handler to write data to a file.
+ Here is one example using the pre-initialized file we created earlier.
+ Note that the Outputs handler will automatically scale float data using
+ the "scale_factor" attribute. The Outputs handler will unscale the data
+ while being read unless the unscale kwarg is explicityly set to False.
+ This behavior is intended to reduce disk storage requirements for big
+ data and can be disabled by setting dtype=np.float32 or dtype=np.float64
+ when writing data.
+
+ >>> Outputs.add_dataset(h5_file='test.h5', dset_name='dset1',
+ >>> dset_data=np.ones((8760, 100)) * 42.42,
+ >>> attrs={'scale_factor': 100}, dtype=np.int32)
+
+
+ >>> with Outputs('test.h5') as f:
+ >>> print(f['dset1'])
+ >>> print(f['dset1'].dtype)
+ [[42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ ...
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]]
+ float32
+
+ >>> with Outputs('test.h5', unscale=False) as f:
+ >>> print(f['dset1'])
+ >>> print(f['dset1'].dtype)
+ [[4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]
+ ...
+ [4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]]
+ int32
+
+ Note that the reV Outputs handler is specifically designed to read and
+ write spatiotemporal data. It is therefore important to intialize the meta
+ data and time index objects even if your data is only spatial or only
+ temporal. Furthermore, the Outputs handler will always assume that 1D
+ datasets represent scalar data (non-timeseries) that corresponds to the
+ meta data shape, and that 2D datasets represent spatiotemporal data whose
+ shape corresponds to (len(time_index), len(meta)). You can see these
+ constraints here:
+
+ >>> Outputs.add_dataset(h5_file='test.h5', dset_name='bad_shape',
+ dset_data=np.ones((1, 100)) * 42.42,
+ attrs={'scale_factor': 100}, dtype=np.int32)
+ HandlerValueError: 2D data with shape (1, 100) is not of the proper
+ spatiotemporal shape: (8760, 100)
+
+ >>> Outputs.add_dataset(h5_file='test.h5', dset_name='bad_shape',
+ dset_data=np.ones((8760,)) * 42.42,
+ attrs={'scale_factor': 100}, dtype=np.int32)
+ HandlerValueError: 1D data with shape (8760,) is not of the proper
+ spatial shape: (100,)
+ """
+
+ @property
+ deffull_version_record(self):
+"""Get record of versions for dependencies
+
+ Returns
+ -------
+ dict
+ Dictionary of package versions for dependencies
+ """
+ rev_versions={'reV':__version__,
+ 'rex':rex.__version__,
+ 'pysam':PySAM.__version__,
+ 'python':sys.version,
+ 'nrwal':NRWAL.__version__,
+ }
+ versions=super().full_version_record
+ versions.update(rev_versions)
+ returnversions
+
+
[docs]defset_version_attr(self):
+"""Set the version attribute to the h5 file."""
+ self.h5.attrs['version']=__version__
+ self.h5.attrs['full_version_record']=json.dumps(
+ self.full_version_record)
+ self.h5.attrs['package']='reV'
[docs]classTransmissionFeatures:
+"""
+ Class to handle Supply Curve Transmission features
+ """
+ def__init__(self,trans_table,line_tie_in_cost=14000,line_cost=2279,
+ station_tie_in_cost=0,center_tie_in_cost=0,
+ sink_tie_in_cost=1e9,avail_cap_frac=1,
+ line_limited=False):
+"""
+ Parameters
+ ----------
+ trans_table : str | pandas.DataFrame
+ Path to .csv or config file or DataFrame containing supply curve
+ transmission mapping
+ line_tie_in_cost : float, optional
+ Cost of connecting to a transmission line in $/MW,
+ by default 14000
+ line_cost : float, optional
+ Cost of building transmission line during connection in $/MW-km,
+ by default 2279
+ station_tie_in_cost : float, optional
+ Cost of connecting to a substation in $/MW,
+ by default 0
+ center_tie_in_cost : float, optional
+ Cost of connecting to a load center in $/MW,
+ by default 0
+ sink_tie_in_cost : float, optional
+ Cost of connecting to a synthetic load center (infinite sink)
+ in $/MW, by default 1e9
+ avail_cap_frac : float, optional
+ Fraction of capacity that is available for connection, by default 1
+ line_limited : bool, optional
+ Substation connection is limited by maximum capacity of the
+ attached lines, legacy method, by default False
+ """
+
+ logger.debug('Trans table input: {}'.format(trans_table))
+ logger.debug('Line tie in cost: {} $/MW'.format(line_tie_in_cost))
+ logger.debug('Line cost: {} $/MW-km'.format(line_cost))
+ logger.debug('Station tie in cost: {} $/MW'
+ .format(station_tie_in_cost))
+ logger.debug('Center tie in cost: {} $/MW'.format(center_tie_in_cost))
+ logger.debug('Synthetic load center tie in cost: {} $/MW'
+ .format(sink_tie_in_cost))
+ logger.debug('Available capacity fraction: {}'
+ .format(avail_cap_frac))
+ logger.debug('Line limited substation connections: {}'
+ .format(line_limited))
+
+ self._line_tie_in_cost=line_tie_in_cost
+ self._line_cost=line_cost
+ self._station_tie_in_cost=station_tie_in_cost
+ self._center_tie_in_cost=center_tie_in_cost
+ self._sink_tie_in_cost=sink_tie_in_cost
+ self._avail_cap_frac=avail_cap_frac
+
+ self._features=self._get_features(trans_table)
+
+ self._feature_gid_list=list(self._features.keys())
+ self._available_mask=np.ones(
+ (int(1+max(list(self._features.keys()))),),dtype=bool)
+
+ self._line_limited=line_limited
+
+ def__repr__(self):
+ msg="{} with {} features".format(self.__class__.__name__,len(self))
+ returnmsg
+
+ def__len__(self):
+ returnlen(self._features)
+
+ def__getitem__(self,gid):
+ ifgidnotinself._features:
+ msg="Invalid feature gid {}".format(gid)
+ logger.error(msg)
+ raiseHandlerKeyError(msg)
+
+ returnself._features[gid]
+
+ @staticmethod
+ def_parse_dictionary(features):
+"""
+ Parse features dict object or config file
+
+ Parameters
+ ----------
+ features : dict | str
+ Dictionary of transmission features or path to config containing
+ dictionary of transmission features
+
+ Returns
+ -------
+ features : dict
+ Nested dictionary of features (lines, substations, loadcenters)
+ lines : {capacity}
+ substations : {lines}
+ loadcenters : {capacity}
+ """
+ ifisinstance(features,str):
+ ifos.path.isfile(features):
+ features=load_config(features)
+ else:
+ features=json.loads(features)
+
+ elifnotisinstance(features,dict):
+ msg=("Transmission features must be a config file, object, "
+ "or a dictionary")
+ logger.error(msg)
+ raiseValueError(msg)
+
+ returnfeatures
+
+ @staticmethod
+ def_parse_table(trans_table):
+"""
+ Extract features and their capacity from supply curve transmission
+ mapping table
+
+ Parameters
+ ----------
+ trans_table : str | pandas.DataFrame
+ Path to .csv or .json containing supply curve transmission mapping
+
+ Returns
+ -------
+ trans_table : pandas.DataFrame
+ DataFrame of transmission features
+ """
+ try:
+ trans_table=parse_table(trans_table)
+ exceptValueErrorasex:
+ logger.error(ex)
+ raise
+
+ trans_table= \
+ trans_table.rename(
+ columns={'trans_line_gid':SupplyCurveField.TRANS_GID,
+ 'trans_gids':'trans_line_gids'})
+
+ contains_dist_in_miles="dist_mi"intrans_table
+ missing_km_dist=SupplyCurveField.DIST_SPUR_KMnotintrans_table
+ ifcontains_dist_in_milesandmissing_km_dist:
+ trans_table=trans_table.rename(
+ columns={"dist_mi":SupplyCurveField.DIST_SPUR_KM}
+ )
+ trans_table[SupplyCurveField.DIST_SPUR_KM]*=1.60934
+
+ returntrans_table
+
+ def_features_from_table(self,trans_table):
+"""
+ Extract features and their capacity from supply curve transmission
+ mapping table
+
+ Parameters
+ ----------
+ trans_table : pandas.DataFrame
+ DataFrame of transmission features
+
+ Returns
+ -------
+ features : dict
+ Nested dictionary of features (lines, substations, loadcenters)
+ lines : {capacity}
+ substations : {lines}
+ loadcenters : {capacity}
+ """
+
+ features={}
+
+ cap_frac=self._avail_cap_frac
+ trans_features=trans_table.groupby(SupplyCurveField.TRANS_GID)
+ trans_features=trans_features.first()
+
+ forgid,featureintrans_features.iterrows():
+ name=feature[SupplyCurveField.TRANS_TYPE].lower()
+ feature_dict={'type':name}
+
+ ifname=='transline':
+ feature_dict[SupplyCurveField.TRANS_CAPACITY]=(
+ feature['ac_cap']*cap_frac
+ )
+
+ elifname=='substation':
+ feature_dict['lines']=json.loads(feature['trans_line_gids'])
+
+ elifname=='loadcen':
+ feature_dict[SupplyCurveField.TRANS_CAPACITY]=(
+ feature['ac_cap']*cap_frac
+ )
+
+ elifname=='pcaloadcen':
+ feature_dict[SupplyCurveField.TRANS_CAPACITY]=None
+
+ else:
+ msg=('Cannot not recognize feature type "{}" '
+ 'for trans gid {}!'.format(name,gid))
+ logger.error(msg)
+ raiseHandlerKeyError(msg)
+
+ features[gid]=feature_dict
+
+ returnfeatures
+
+ def_get_features(self,trans_table):
+"""
+ Create transmission features dictionary either from supply curve
+ transmission mapping or from pre-created dictionary
+
+ Parameters
+ ----------
+ trans_table : str
+ Path to .csv or .json containing supply curve transmission mapping
+
+ Returns
+ -------
+ features : dict
+ Nested dictionary of features (lines, substations, loadcenters)
+ lines : {capacity}
+ substations : {lines}
+ loadcenters : {capacity}
+ """
+
+ trans_table=self._parse_table(trans_table)
+ features=self._features_from_table(trans_table)
+
+ returnfeatures
+
+
[docs]defcheck_feature_dependencies(self):
+"""Check features for dependencies that are missing and raise error."""
+ missing={}
+ forgid,feature_dictinself._features.items():
+ forline_gidinfeature_dict.get('lines',[]):
+ ifline_gidnotinself._features:
+ ifgidnotinmissing:
+ missing[gid]=[]
+ missing[gid].append(line_gid)
+
+ ifany(missing):
+ emsg=('Transmission feature table has {} parent features that '
+ 'depend on missing lines. Missing dependencies: {}'
+ .format(len(missing),missing))
+ logger.error(emsg)
+ raiseRuntimeError(emsg)
+
+ @staticmethod
+ def_calc_cost(distance,line_cost=2279,tie_in_cost=0,
+ transmission_multiplier=1):
+"""
+ Compute transmission cost in $/MW
+
+ Parameters
+ ----------
+ distance : float
+ Distance to feature in kms
+ line_tie_in_cost : float, optional
+ Cost of connecting to a transmission line in $/MW,
+ by default 14000
+ tie_in_cost : float, optional
+ Cost to tie in line to feature in $/MW, by default 0
+ tranmission_multiplier : float, optional
+ Multiplier for region specific line cost increases, by default 1
+
+ Returns
+ -------
+ cost : float
+ Cost of transmission in $/MW
+ """
+ cost=(distance*line_cost*transmission_multiplier+tie_in_cost)
+
+ returncost
+
+ def_substation_capacity(self,gid,line_gids):
+"""
+ Get capacity of a substation from its tranmission lines
+
+ Parameters
+ ----------
+ gid : int
+ Substation gid
+ line_gids : list
+ List of transmission line gids connected to the substation
+
+ Returns
+ -------
+ avail_cap : float
+ Substation available capacity
+ """
+ try:
+ line_caps=[self[l_gid][SupplyCurveField.TRANS_CAPACITY]
+ forl_gidinline_gids]
+ exceptHandlerKeyErrorase:
+ msg=('Could not find capacities for substation gid {} and '
+ 'connected lines: {}'.format(gid,line_gids))
+ logger.error(msg)
+ raiseHandlerKeyError(msg)frome
+
+ avail_cap=sum(line_caps)/2
+
+ ifself._line_limited:
+ max_cap=max(line_caps)/2
+ ifmax_cap<avail_cap:
+ avail_cap=max_cap
+
+ returnavail_cap
+
+
[docs]defavailable_capacity(self,gid):
+"""
+ Get available capacity for given line
+
+ Parameters
+ ----------
+ gid : int
+ Unique id of feature of interest
+
+ Returns
+ -------
+ avail_cap : float
+ Available capacity = capacity * available fraction
+ default = 100%
+ """
+
+ feature=self[gid]
+
+ ifSupplyCurveField.TRANS_CAPACITYinfeature:
+ avail_cap=feature[SupplyCurveField.TRANS_CAPACITY]
+
+ elif'lines'infeature:
+ avail_cap=self._substation_capacity(gid,feature['lines'])
+
+ else:
+ msg=('Could not parse available capacity from feature: {}'
+ .format(feature))
+ logger.error(msg)
+ raiseHandlerRuntimeError(msg)
+
+ returnavail_cap
+
+ def_update_availability(self,gid):
+"""
+ Check features available capacity, if its 0 update _available_mask
+
+ Parameters
+ ----------
+ gid : list
+ Feature gid to check
+ """
+ avail_cap=self.available_capacity(gid)
+ ifavail_cap==0:
+ self._available_mask[gid]=False
+
+
[docs]defcheck_availability(self,gid):
+"""
+ Check availablity of feature with given gid
+
+ Parameters
+ ----------
+ gid : int
+ Feature gid to check
+
+ Returns
+ -------
+ bool
+ Whether the gid is available or not
+ """
+ returnself._available_mask[gid]
+
+ def_connect(self,gid,capacity):
+"""
+ Connect to a standalone transmission feature (not a substation)
+ and decrement the feature's available capacity.
+ Raise exception if not able to connect.
+
+ Parameters
+ ----------
+ gid : int
+ Feature gid to connect to
+ capacity : float
+ Capacity needed in MW
+ """
+ avail_cap=self[gid][SupplyCurveField.TRANS_CAPACITY]
+
+ ifavail_cap<capacity:
+ msg=("Cannot connect to {}: "
+ "needed capacity({} MW) > "
+ "available capacity({} MW)"
+ .format(gid,capacity,avail_cap))
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ self[gid][SupplyCurveField.TRANS_CAPACITY]-=capacity
+
+ def_fill_lines(self,line_gids,line_caps,capacity):
+"""
+ Fill any lines that cannot handle equal portion of capacity and
+ remove from lines to be filled and capacity needed
+
+ Parameters
+ ----------
+ line_gids : ndarray
+ Vector of transmission line gids connected to the substation
+ line_caps : ndarray
+ Vector of available capacity of the transmission lines
+ capacity : float
+ Capacity needed in MW
+
+ Returns
+ ----------
+ line_gids : ndarray
+ Transmission lines with available capacity
+ line_caps : ndarray
+ Capacity of lines with available capacity
+ capacity : float
+ Updated capacity needed to be applied to substation in MW
+ """
+ apply_cap=capacity/len(line_gids)
+ mask=line_caps<apply_cap
+ forposinnp.where(line_caps<apply_cap)[0]:
+ gid=line_gids[pos]
+ apply_cap=line_caps[pos]
+ self._connect(gid,apply_cap)
+ capacity-=apply_cap
+
+ returnline_gids[~mask],line_caps[~mask],capacity
+
+ def_spread_substation_load(self,line_gids,line_caps,capacity):
+"""
+ Spread needed capacity over all lines connected to substation
+
+ Parameters
+ ----------
+ line_gids : ndarray
+ Vector of transmission line gids connected to the substation
+ line_caps : ndarray
+ Vector of available capacity of the transmission lines
+ capacity : float
+ Capacity needed to be applied to substation in MW
+ """
+ whileTrue:
+ lines,line_caps,capacity=self._fill_lines(line_gids,line_caps,
+ capacity)
+ iflen(lines)<len(line_gids):
+ line_gids=lines
+ else:
+ break
+
+ apply_cap=capacity/len(lines)
+ forgidinlines:
+ self._connect(gid,apply_cap)
+
+ def_connect_to_substation(self,line_gids,capacity):
+"""
+ Connect to substation and update internal dictionary accordingly
+
+ Parameters
+ ----------
+ line_gids : list
+ List of transmission line gids connected to the substation
+ capacity : float
+ Capacity needed in MW
+ line_lmited : bool
+ Substation connection is limited by maximum capacity of the
+ attached lines
+ """
+ line_caps=np.array([self[gid][SupplyCurveField.TRANS_CAPACITY]
+ forgidinline_gids])
+ ifself._line_limited:
+ gid=line_gids[np.argmax(line_caps)]
+ self._connect(gid,capacity)
+ else:
+ non_zero=np.nonzero(line_caps)[0]
+ line_gids=np.array([line_gids[i]foriinnon_zero])
+ line_caps=line_caps[non_zero]
+ self._spread_substation_load(line_gids,line_caps,capacity)
+
+
[docs]defconnect(self,gid,capacity,apply=True):
+"""
+ Check if you can connect to given feature
+ If apply, update internal dictionary accordingly
+
+ Parameters
+ ----------
+ gid : int
+ Unique id of feature of intereset
+ capacity : float
+ Capacity needed in MW
+ apply : bool
+ Apply capacity to feature with given gid and update
+ internal dictionary
+
+ Returns
+ -------
+ connected : bool
+ Flag as to whether connection is possible or not
+ """
+ ifself.check_availability(gid):
+ avail_cap=self.available_capacity(gid)
+ ifavail_capisnotNoneandcapacity>avail_cap:
+ connected=False
+ else:
+ connected=True
+ ifapply:
+ feature_type=self[gid]['type']
+ iffeature_type=='transline':
+ self._connect(gid,capacity)
+ eliffeature_type=='substation':
+ lines=self[gid]['lines']
+ self._connect_to_substation(lines,capacity)
+ eliffeature_type=='loadcen':
+ self._connect(gid,capacity)
+
+ self._update_availability(gid)
+ else:
+ connected=False
+
+ returnconnected
+
+
[docs]defcost(self,gid,distance,transmission_multiplier=1,
+ capacity=None):
+"""
+ Compute levelized cost of transmission (LCOT) for connecting to give
+ feature
+
+ Parameters
+ ----------
+ gid : int
+ Feature gid to connect to
+ distance : float
+ Distance to feature in kms
+ line_multiplier : float
+ Multiplier for region specific line cost increases
+ capacity : float
+ Capacity needed in MW, if None DO NOT check if connection is
+ possible
+
+ Returns
+ -------
+ cost : float
+ Cost of transmission in $/MW, if None indicates connection is
+ NOT possible
+ """
+ feature_type=self[gid]['type']
+ line_cost=self._line_cost
+ iffeature_type=='transline':
+ tie_in_cost=self._line_tie_in_cost
+ eliffeature_type=='substation':
+ tie_in_cost=self._station_tie_in_cost
+ eliffeature_type=='loadcen':
+ tie_in_cost=self._center_tie_in_cost
+ eliffeature_type=='pcaloadcen':
+ tie_in_cost=self._sink_tie_in_cost
+ else:
+ tie_in_cost=0
+ msg=("Do not recognize feature type {}, tie_in_cost set to 0"
+ .format(feature_type))
+ logger.warning(msg)
+ warn(msg,HandlerWarning)
+
+ cost=self._calc_cost(distance,line_cost=line_cost,
+ tie_in_cost=tie_in_cost,
+ transmission_multiplier=transmission_multiplier)
+ ifcapacityisnotNone:
+ ifnotself.connect(gid,capacity,apply=False):
+ cost=None
+
+ returncost
+
+
[docs]@classmethod
+ deffeature_capacity(cls,trans_table,avail_cap_frac=1):
+"""
+ Compute available capacity for all features
+
+ Parameters
+ ----------
+ trans_table : str | pandas.DataFrame
+ Path to .csv or .json containing supply curve transmission mapping
+ avail_cap_frac : float, optional
+ Fraction of capacity that is available for connection, by default 1
+
+ Returns
+ -------
+ feature_cap : pandas.DataFrame
+ Available Capacity for each transmission feature
+ """
+ try:
+ feature=cls(trans_table,avail_cap_frac=avail_cap_frac)
+
+ feature_cap={}
+ forgid,_infeature._features.items():
+ feature_cap[gid]=feature.available_capacity(gid)
+ exceptException:
+ logger.exception("Error computing available capacity for all "
+ "features in {}".format(cls))
+ raise
+
+ feature_cap=pd.Series(feature_cap)
+ feature_cap.name=SupplyCurveField.TRANS_CAPACITY
+ feature_cap.index.name=SupplyCurveField.TRANS_GID
+ feature_cap=feature_cap.to_frame().reset_index()
+
+ returnfeature_cap
+
+
+
[docs]classTransmissionCosts(TransmissionFeatures):
+"""
+ Class to compute supply curve -> transmission feature costs
+ """
+ def_features_from_table(self,trans_table):
+"""
+ Extract features and their capacity from supply curve transmission
+ mapping table and pre-compute the available capacity of each feature
+
+ Parameters
+ ----------
+ trans_table : pandas.DataFrame
+ DataFrame of transmission features
+
+ Returns
+ -------
+ features : dict
+ Nested dictionary of features (lines, substations, loadcenters)
+ lines : {capacity}
+ substations : {lines}
+ loadcenters : {capacity}
+ """
+
+ features={}
+
+ ifSupplyCurveField.TRANS_CAPACITYnotintrans_table:
+ kwargs={'avail_cap_frac':self._avail_cap_frac}
+ fc=TransmissionFeatures.feature_capacity(trans_table,
+ **kwargs)
+ trans_table=trans_table.merge(fc,on=SupplyCurveField.TRANS_GID)
+
+ trans_features=trans_table.groupby(SupplyCurveField.TRANS_GID)
+ trans_features=trans_features.first()
+ forgid,featureintrans_features.iterrows():
+ name=feature[SupplyCurveField.TRANS_TYPE].lower()
+ feature_dict={'type':name,
+ SupplyCurveField.TRANS_CAPACITY:(
+ feature[SupplyCurveField.TRANS_CAPACITY]
+ )}
+ features[gid]=feature_dict
+
+ returnfeatures
+
+
[docs]defavailable_capacity(self,gid):
+"""
+ Get available capacity for given line
+
+ Parameters
+ ----------
+ gid : int
+ Unique id of feature of interest
+
+ Returns
+ -------
+ avail_cap : float
+ Available capacity = capacity * available fraction
+ default = 100%
+ """
+
+ returnself[gid][SupplyCurveField.TRANS_CAPACITY]
+
+
[docs]@classmethod
+ deffeature_costs(cls,trans_table,capacity=None,line_tie_in_cost=14000,
+ line_cost=2279,station_tie_in_cost=0,
+ center_tie_in_cost=0,sink_tie_in_cost=1e9,
+ avail_cap_frac=1,line_limited=False):
+"""
+ Compute costs for all connections in given transmission table
+
+ Parameters
+ ----------
+ trans_table : str | pandas.DataFrame
+ Path to .csv or .json containing supply curve transmission mapping
+ capacity : float
+ Capacity needed in MW, if None DO NOT check if connection is
+ possible
+ line_tie_in_cost : float, optional
+ Cost of connecting to a transmission line in $/MW,
+ by default 14000
+ line_cost : float, optional
+ Cost of building transmission line during connection in $/MW-km,
+ by default 2279
+ station_tie_in_cost : float, optional
+ Cost of connecting to a substation in $/MW,
+ by default 0
+ center_tie_in_cost : float, optional
+ Cost of connecting to a load center in $/MW,
+ by default 0
+ sink_tie_in_cost : float, optional
+ Cost of connecting to a synthetic load center (infinite sink)
+ in $/MW, by default 1e9
+ avail_cap_frac : float, optional
+ Fraction of capacity that is available for connection, by default 1
+ line_limited : bool, optional
+ Substation connection is limited by maximum capacity of the
+ attached lines, legacy method, by default False
+
+ Returns
+ -------
+ cost : ndarray
+ Cost of transmission in $/MW, if None indicates connection is
+ NOT possible
+ """
+ try:
+ feature=cls(trans_table,
+ line_tie_in_cost=line_tie_in_cost,
+ line_cost=line_cost,
+ station_tie_in_cost=station_tie_in_cost,
+ center_tie_in_cost=center_tie_in_cost,
+ sink_tie_in_cost=sink_tie_in_cost,
+ avail_cap_frac=avail_cap_frac,
+ line_limited=line_limited)
+
+ costs=[]
+ for_,rowintrans_table.iterrows():
+ tm=row.get('transmission_multiplier',1)
+ costs.append(feature.cost(row[SupplyCurveField.TRANS_GID],
+ row[SupplyCurveField.DIST_SPUR_KM],
+ capacity=capacity,
+ transmission_multiplier=tm))
+ exceptException:
+ logger.exception("Error computing costs for all connections in {}"
+ .format(cls))
+ raise
+
+ returnnp.array(costs,dtype='float32')
+# -*- coding: utf-8 -*-
+"""Collection of functions used to hybridize columns in rep profiles meta.
+
+@author: ppinchuk
+"""
+fromreV.utilitiesimportSupplyCurveField
+
+
+
[docs]defaggregate_solar_capacity(h):
+"""Compute the total solar capcity allowed in hybridization.
+
+ Parameters
+ ----------
+ h : `reV.hybrids.Hybridization`
+ Instance of `reV.hybrids.Hybridization` class containing the
+ attribute `hybrid_meta`, which is a DataFrame containing
+ hybridized meta data.
+
+ Returns
+ -------
+ data : Series | None
+ A series of data containing the capacity allowed in the hybrid
+ capacity sum, or `None` if 'hybrid_solar_capacity' already
+ exists.
+
+ Notes
+ -----
+ No limiting is done on the ratio of wind to solar. This method
+ checks for an existing 'hybrid_solar_capacity'. If one does not
+ exist, it is assumed that there is no limit on the solar to wind
+ capacity ratio and the solar capacity is copied into this new
+ column.
+ """
+ iff'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}'inh.hybrid_meta:
+ returnNone
+ returnh.hybrid_meta[f'solar_{SupplyCurveField.CAPACITY_AC_MW}']
+
+
+
[docs]defaggregate_wind_capacity(h):
+"""Compute the total wind capcity allowed in hybridization.
+
+ Parameters
+ ----------
+ h : `reV.hybrids.Hybridization`
+ Instance of `reV.hybrids.Hybridization` class containing the
+ attribute `hybrid_meta`, which is a DataFrame containing
+ hybridized meta data.
+
+ Returns
+ -------
+ data : Series | None
+ A series of data containing the capacity allowed in the hybrid
+ capacity sum, or `None` if 'hybrid_solar_capacity' already
+ exists.
+
+ Notes
+ -----
+ No limiting is done on the ratio of wind to solar. This method
+ checks for an existing 'hybrid_wind_capacity'. If one does not
+ exist, it is assumed that there is no limit on the solar to wind
+ capacity ratio and the wind capacity is copied into this new column.
+ """
+ iff'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}'inh.hybrid_meta:
+ returnNone
+ returnh.hybrid_meta[f'wind_{SupplyCurveField.CAPACITY_AC_MW}']
+
+
+
[docs]defaggregate_capacity(h):
+"""Compute the total capcity by summing the individual capacities.
+
+ Parameters
+ ----------
+ h : `reV.hybrids.Hybridization`
+ Instance of `reV.hybrids.Hybridization` class containing the
+ attribute `hybrid_meta`, which is a DataFrame containing
+ hybridized meta data.
+
+ Returns
+ -------
+ data : Series | None
+ A series of data containing the aggregated capacity, or `None`
+ if the capacity columns are missing.
+ """
+ sc=f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}'
+ wc=f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}'
+ missing_solar_cap=scnotinh.hybrid_meta.columns
+ missing_wind_cap=wcnotinh.hybrid_meta.columns
+ ifmissing_solar_capormissing_wind_cap:
+ returnNone
+
+ total_cap=h.hybrid_meta[sc]+h.hybrid_meta[wc]
+ returntotal_cap
+
+
+
[docs]defaggregate_capacity_factor(h):
+"""Compute the capacity-weighted mean capcity factor.
+
+ Parameters
+ ----------
+ h : `reV.hybrids.Hybridization`
+ Instance of `reV.hybrids.Hybridization` class containing the
+ attribute `hybrid_meta`, which is a DataFrame containing
+ hybridized meta data.
+
+ Returns
+ -------
+ data : Series | None
+ A series of data containing the aggregated capacity, or `None`
+ if the capacity and/or mean_cf columns are missing.
+ """
+
+ sc=f'hybrid_solar_{SupplyCurveField.CAPACITY_AC_MW}'
+ wc=f'hybrid_wind_{SupplyCurveField.CAPACITY_AC_MW}'
+ scf=f'solar_{SupplyCurveField.MEAN_CF_AC}'
+ wcf=f'wind_{SupplyCurveField.MEAN_CF_AC}'
+ missing_solar_cap=scnotinh.hybrid_meta.columns
+ missing_wind_cap=wcnotinh.hybrid_meta.columns
+ missing_solar_mean_cf=scfnotinh.hybrid_meta.columns
+ missing_wind_mean_cf=wcfnotinh.hybrid_meta.columns
+ missing_any=(missing_solar_capormissing_wind_cap
+ ormissing_solar_mean_cformissing_wind_mean_cf)
+ ifmissing_any:
+ returnNone
+
+ solar_cf_weighted=h.hybrid_meta[sc]*h.hybrid_meta[scf]
+ wind_cf_weighted=h.hybrid_meta[wc]*h.hybrid_meta[wcf]
+ total_capacity=aggregate_capacity(h)
+ hybrid_cf=(solar_cf_weighted+wind_cf_weighted)/total_capacity
+ returnhybrid_cf
[docs]@classmethod
+ deffmt(cls,n):
+"""Format an input column name to remove excess chars and whitespace.
+
+ This method should help facilitate the merging of column names
+ between two DataFrames.
+
+ Parameters
+ ----------
+ n : str
+ Input column name.
+
+ Returns
+ -------
+ str
+ The column name with all characters except ascii stripped
+ and all lowercase.
+ """
+ return"".join(cforcinnifcincls.ALLOWED).lower()
+
+
+
[docs]classHybridsData:
+"""Hybrids input data container."""
+
+ def__init__(self,solar_fpath,wind_fpath):
+"""
+ Parameters
+ ----------
+ solar_fpath : str
+ Filepath to rep profile output file to extract solar profiles and
+ summaries from.
+ wind_fpath : str
+ Filepath to rep profile output file to extract wind profiles and
+ summaries from.
+ """
+ self.solar_fpath=solar_fpath
+ self.wind_fpath=wind_fpath
+ self.profile_dset_names=[]
+ self.merge_col_overlap_values=set()
+ self._solar_meta=None
+ self._wind_meta=None
+ self._solar_time_index=None
+ self._wind_time_index=None
+ self._hybrid_time_index=None
+ self.__profile_reg_check=re.compile(PROFILE_DSET_REGEX)
+ self.__solar_cols=self.solar_meta.columns.map(ColNameFormatter.fmt)
+ self.__wind_cols=self.wind_meta.columns.map(ColNameFormatter.fmt)
+
+ @property
+ defsolar_meta(self):
+"""Summary for the solar representative profiles.
+
+ Returns
+ -------
+ solar_meta : pd.DataFrame
+ Summary for the solar representative profiles.
+ """
+ ifself._solar_metaisNone:
+ withResource(self.solar_fpath)asres:
+ self._solar_meta=res.meta
+ returnself._solar_meta
+
+ @property
+ defwind_meta(self):
+"""Summary for the wind representative profiles.
+
+ Returns
+ -------
+ wind_meta : pd.DataFrame
+ Summary for the wind representative profiles.
+ """
+ ifself._wind_metaisNone:
+ withResource(self.wind_fpath)asres:
+ self._wind_meta=res.meta
+ returnself._wind_meta
+
+ @property
+ defsolar_time_index(self):
+"""Get the time index for the solar rep profiles.
+
+ Returns
+ -------
+ solar_time_index : pd.datetimeindex
+ Time index sourced from the solar reV gen file.
+ """
+ ifself._solar_time_indexisNone:
+ withResource(self.solar_fpath)asres:
+ self._solar_time_index=res.time_index
+ returnself._solar_time_index
+
+ @property
+ defwind_time_index(self):
+"""Get the time index for the wind rep profiles.
+
+ Returns
+ -------
+ wind_time_index : pd.datetimeindex
+ Time index sourced from the wind reV gen file.
+ """
+ ifself._wind_time_indexisNone:
+ withResource(self.wind_fpath)asres:
+ self._wind_time_index=res.time_index
+ returnself._wind_time_index
+
+ @property
+ defhybrid_time_index(self):
+"""Get the time index for the hybrid rep profiles.
+
+ Returns
+ -------
+ hybrid_time_index : pd.datetimeindex
+ Time index for the hybrid rep profiles.
+ """
+ ifself._hybrid_time_indexisNone:
+ self._hybrid_time_index=self.solar_time_index.join(
+ self.wind_time_index,how="inner"
+ )
+ returnself._hybrid_time_index
+
+
[docs]defcontains_col(self,col_name):
+"""Check if input column name exists in either meta data set.
+
+ Parameters
+ ----------
+ col_name : str
+ Name of column to check for.
+
+ Returns
+ -------
+ bool
+ Whether or not the column is found in either meta data set.
+ """
+ fmt_name=ColNameFormatter.fmt(col_name)
+ col_in_solar=fmt_nameinself.__solar_cols
+ col_in_wind=fmt_nameinself.__wind_cols
+ returncol_in_solarorcol_in_wind
+
+
[docs]defvalidate(self):
+"""Validate the input data.
+
+ This method checks for a minimum time index length, a unique
+ profile, and unique merge column that overlaps between both data
+ sets.
+
+ """
+ self._validate_time_index()
+ self._validate_num_profiles()
+ self._validate_merge_col_exists()
+ self._validate_unique_merge_col()
+ self._validate_merge_col_overlaps()
+
+ def_validate_time_index(self):
+"""Validate the hybrid time index to be of len >= 8760.
+
+ Raises
+ ------
+ FileInputError
+ If len(time_index) < 8760 for the hybrid profile.
+ """
+ iflen(self.hybrid_time_index)<8760:
+ msg=(
+ "The length of the merged time index ({}) is less than "
+ "8760. Please ensure that the input profiles have a "
+ "time index that overlaps >= 8760 times."
+ )
+ e=msg.format(len(self.hybrid_time_index))
+ logger.error(e)
+ raiseFileInputError(e)
+
+ def_validate_num_profiles(self):
+"""Validate the number of input profiles.
+
+ Raises
+ ------
+ FileInputError
+ If # of rep_profiles > 1.
+ """
+ forfpin[self.solar_fpath,self.wind_fpath]:
+ withResource(fp)asres:
+ profile_dset_names=[
+ nforninres.dsetsifself.__profile_reg_check.match(n)
+ ]
+ ifnotprofile_dset_names:
+ msg=(
+ "Did not find any data sets matching the regex: "
+ "{!r} in {!r}. Please ensure that the profile data "
+ "exists and that the data set is named correctly."
+ )
+ e=msg.format(PROFILE_DSET_REGEX,fp)
+ logger.error(e)
+ raiseFileInputError(e)
+ iflen(profile_dset_names)>1:
+ msg=("Found more than one profile in {!r}: {}. "
+ "This module is not intended for hybridization of "
+ "multiple representative profiles. Please re-run "
+ "on a single aggregated profile.")
+ e=msg.format(fp,profile_dset_names)
+ logger.error(e)
+ raiseFileInputError(e)
+ self.profile_dset_names+=profile_dset_names
+
+ def_validate_merge_col_exists(self):
+"""Validate the existence of the merge column.
+
+ Raises
+ ------
+ FileInputError
+ If merge column is missing from either the solar or
+ the wind meta data.
+ """
+ msg=(
+ "Cannot hybridize: merge column {!r} missing from the "
+ "{} meta data! ({!r})"
+ )
+
+ mc=ColNameFormatter.fmt(MERGE_COLUMN)
+ forcols,fp,resinzip(
+ [self.__solar_cols,self.__wind_cols],
+ [self.solar_fpath,self.wind_fpath],
+ ["solar","wind"],
+ ):
+ ifmcnotincols:
+ e=msg.format(MERGE_COLUMN,res,fp)
+ logger.error(e)
+ raiseFileInputError(e)
+
+ def_validate_unique_merge_col(self):
+"""Validate the existence of unique values in the merge column.
+
+ Raises
+ ------
+ FileInputError
+ If merge column contains duplicate values in either the solar or
+ the wind meta data.
+ """
+ msg=(
+ "Duplicate {}s were found. This is likely due to resource "
+ "class binning, which is not supported at this time. "
+ "Please re-run supply curve aggregation without "
+ "resource class binning and ensure there are no duplicate "
+ "values in {!r}. File: {!r}"
+ )
+
+ mc=ColNameFormatter.fmt(MERGE_COLUMN)
+ fords,cols,fpinzip(
+ [self.solar_meta,self.wind_meta],
+ [self.__solar_cols,self.__wind_cols],
+ [self.solar_fpath,self.wind_fpath],
+ ):
+ merge_col=ds.columns[cols==mc].item()
+ ifnotds[merge_col].is_unique:
+ e=msg.format(merge_col,merge_col,fp)
+ logger.error(e)
+ raiseFileInputError(e)
+
+ def_validate_merge_col_overlaps(self):
+"""Validate the existence of overlap in the merge column values.
+
+ Raises
+ ------
+ FileInputError
+ If merge column values do not overlap between the tow input files.
+ """
+ mc=ColNameFormatter.fmt(MERGE_COLUMN)
+ merge_col=self.solar_meta.columns[self.__solar_cols==mc].item()
+ solar_vals=set(self.solar_meta[merge_col].values)
+ merge_col=self.wind_meta.columns[self.__wind_cols==mc].item()
+ wind_vals=set(self.wind_meta[merge_col].values)
+ self.merge_col_overlap_values=solar_vals&wind_vals
+
+ ifnotself.merge_col_overlap_values:
+ msg=(
+ "No overlap detected in the values of {!r} across the "
+ "input files. Please ensure that at least one of the "
+ "{!r} values is the same for input files {!r} and {!r}"
+ )
+ e=msg.format(
+ merge_col,merge_col,self.solar_fpath,self.wind_fpath
+ )
+ logger.error(e)
+ raiseFileInputError(e)
+
+
+
[docs]classMetaHybridizer:
+"""Framework to handle hybridization of meta data."""
+
+ _INTERNAL_COL_PREFIX="_h_internal"
+
+ def__init__(
+ self,
+ data,
+ allow_solar_only=False,
+ allow_wind_only=False,
+ fillna=None,
+ limits=None,
+ ratio_bounds=None,
+ ratio="solar_capacity/wind_capacity",
+ ):
+"""
+ Parameters
+ ----------
+ data : `HybridsData`
+ Instance of `HybridsData` containing input data to
+ hybridize.
+ allow_solar_only : bool, optional
+ Option to allow SC points with only solar capacity
+ (no wind). By default, ``False``.
+ allow_wind_only : bool, optional
+ Option to allow SC points with only wind capacity
+ (no solar), By default, ``False``.
+ fillna : dict, optional
+ Dictionary containing column_name, fill_value pairs
+ representing any fill values that should be applied after
+ merging the wind and solar meta. Note that column names will
+ likely have to be prefixed with ``solar`` or ``wind``.
+ By default, ``None``.
+ limits : dict, optional
+ Option to specify mapping (in the form of a dictionary) of
+ {colum_name: max_value} representing the upper limit
+ (maximum value) for the values of a column in the merged
+ meta. For example, `limits={'solar_capacity': 100}` would
+ limit all the values of the solar capacity in the merged
+ meta to a maximum value of 100. This limit is applied
+ *BEFORE* ratio calculations. The names of the columns should
+ match the column names in the merged meta, so they are
+ likely prefixed with ``solar`` or ``wind`. By default,
+ ``None`` (no limits applied).
+ ratio_bounds : tuple, optional
+ Option to set ratio bounds (in two-tuple form) on the
+ columns of the `ratio` input. For example,
+ `ratio_bounds=(0.5, 1.5)` would adjust the values of both of
+ the `ratio` columns such that their ratio is always between
+ half and double (e.g., no value would be more than double
+ the other). To specify a single ratio value, use the same
+ value as the upper and lower bound. For example,
+ `ratio_bounds=(1, 1)` would adjust the values of both of the
+ `ratio` columns such that their ratio is always equal.
+ By default, ``None`` (no limit on the ratio).
+ ratio : str, optional
+ Option to specify the columns used to calculate the ratio
+ that is limited by the `ratio_bounds` input. This input is a
+ string in the form
+ "numerator_column_name/denominator_column_name".
+ For example, `ratio='solar_capacity/wind_capacity'` would
+ limit the ratio of the solar to wind capacities as specified
+ by the `ratio_bounds` input. If `ratio_bounds` is ``None``,
+ this input does nothing. The names of the columns should be
+ prefixed with one of the prefixes defined as class
+ variables. By default ``'solar_capacity/wind_capacity'``.
+ """
+ self.data=data
+ self._allow_solar_only=allow_solar_only
+ self._allow_wind_only=allow_wind_only
+ self._fillna={**DEFAULT_FILL_VALUES,**(fillnaor{})}
+ self._limits=limitsor{}
+ self._ratio_bounds=ratio_bounds
+ self._ratio=ratio
+ self._hybrid_meta=None
+ self.__hybrid_meta_cols=None
+ self.__col_name_map=None
+ self.__solar_rpi_n="{}_solar_rpidx".format(self._INTERNAL_COL_PREFIX)
+ self.__wind_rpi_n="{}_wind_rpidx".format(self._INTERNAL_COL_PREFIX)
+
+ @property
+ defhybrid_meta(self):
+"""Hybridized summary for the representative profiles.
+
+ Returns
+ -------
+ hybrid_meta : pd.DataFrame
+ Summary for the hybridized representative profiles.
+ At the very least, this has a column that the data was merged on.
+ """
+ ifself._hybrid_metaisNoneorself.__hybrid_meta_colsisNone:
+ returnself._hybrid_meta
+ returnself._hybrid_meta[self.__hybrid_meta_cols]
+
+
[docs]defvalidate_input(self):
+"""Validate the input parameters.
+
+ This method validates that the input limit, fill, and ratio columns
+ are formatted correctly.
+ """
+ self._validate_limits_cols_prefixed()
+ self._validate_fillna_cols_prefixed()
+ self._validate_ratio_input()
+
+ def_validate_limits_cols_prefixed(self):
+"""Ensure the limits columns are formatted correctly.
+
+ This check is important because the limiting happens
+ after the meta has been merged (so columns are already prefixed),
+ but before the hybrid columns are computed. As a result, the limits
+ columns _must_ have a valid prefix.
+
+ Raises
+ ------
+ InputError
+ If limits columns are not prefixed correctly.
+ """
+ forcolinself._limits:
+ self.__validate_col_prefix(
+ col,(SOLAR_PREFIX,WIND_PREFIX),input_name="limits"
+ )
+
+ @staticmethod
+ def__validate_col_prefix(col,prefixes,input_name):
+"""Validate the the col starts with the correct prefix."""
+
+ missing=[notcol.startswith(p)forpinprefixes]
+ ifall(missing):
+ msg=(
+ "Input {0} column {1!r} does not start with a valid "
+ "prefix: {2!r}. Please ensure that the {0} column "
+ "names specify the correct resource prefix."
+ )
+ e=msg.format(input_name,col,prefixes)
+ logger.error(e)
+ raiseInputError(e)
+
+ def_validate_fillna_cols_prefixed(self):
+"""Ensure the fillna columns are formatted correctly.
+
+ This check is important because the fillna step happens
+ after the meta has been merged (so columns are already prefixed),
+ but before the hybrid columns are computed. As a result, the fillna
+ columns _must_ have a valid prefix.
+
+ Raises
+ ------
+ InputError
+ If fillna columns are not prefixed correctly.
+ """
+ forcolinself._fillna:
+ self.__validate_col_prefix(
+ col,(SOLAR_PREFIX,WIND_PREFIX),input_name="fillna"
+ )
+
+ def_validate_ratio_input(self):
+"""Validate the ratio input parameters.
+
+ This method validates that the input ratio columns are formatted
+ correctly and exist in the input data. It also verifies that
+ the `ratio_bounds` is correctly formatted.
+ """
+ ifself._ratio_boundsisNone:
+ return
+
+ self._validate_ratio_bounds()
+ self._validate_ratio_type()
+ self._validate_ratio_format()
+ self._validate_ratio_cols_prefixed()
+ self._validate_ratio_cols_exist()
+
+ def_validate_ratio_bounds(self):
+"""Ensure the ratio value is input correctly.
+
+ Raises
+ ------
+ InputError
+ If ratio is not a len 2 container of floats.
+ """
+
+ try:
+ iflen(self._ratio_bounds)!=2:
+ msg=(
+ "Length of input for ratio_bounds is {} - but is "
+ "required to be of length 2. Please make sure this "
+ "input is a len 2 container of floats. If you would "
+ "like to specify a single ratio value, use the same "
+ "float for both limits (i.e. ratio_bounds=(1, 1))."
+ )
+ e=msg.format(len(self._ratio_bounds))
+ logger.error(e)
+ raiseInputError(e)
+ exceptTypeError:
+ msg=(
+ "Input for ratio_bounds not understood: {!r}. "
+ "Please make sure this value is a len 2 container "
+ "of floats."
+ )
+ e=msg.format(self._ratio_bounds)
+ logger.error(e)
+ raiseInputError(e)fromNone
+
+ def_validate_ratio_type(self):
+"""Ensure that the ratio input is a string.
+
+ Raises
+ ------
+ InputError
+ If `ratio` is not a string.
+ """
+ ifnotisinstance(self._ratio,str):
+ msg=(
+ "Ratio input type {} not understood. Please make sure "
+ "the ratio input is a string in the form "
+ "'numerator_column_name/denominator_column_name'. Ratio "
+ "input: {!r}"
+ )
+ e=msg.format(type(self._ratio),self._ratio)
+ logger.error(e)
+ raiseInputError(e)
+
+ def_validate_ratio_format(self):
+"""Validate that the ratio input format is correct and can be parsed.
+
+ Raises
+ ------
+ InputError
+ If the '/' character is missing or of there are too many
+ '/' characters.
+ """
+ if"/"notinself._ratio:
+ msg=(
+ "Ratio input {} does not contain the '/' character. "
+ "Please make sure the ratio input is a string in the form "
+ "'numerator_column_name/denominator_column_name'"
+ )
+ e=msg.format(self._ratio)
+ logger.error(e)
+ raiseInputError(e)
+
+ iflen(self._ratio_cols)!=2:
+ msg=(
+ "Ratio input {} contains too many '/' characters. Please "
+ "make sure the ratio input is a string in the form "
+ "'numerator_column_name/denominator_column_name'."
+ )
+ e=msg.format(self._ratio)
+ logger.error(e)
+ raiseInputError(e)
+
+ def_validate_ratio_cols_prefixed(self):
+"""Ensure the ratio columns are formatted correctly.
+
+ This check is important because the ratio limit step happens
+ after the meta has been merged (so columns are already prefixed),
+ but before the hybrid columns are computed. As a result, the ratio
+ columns _must_ have a valid prefix.
+
+ Raises
+ ------
+ InputError
+ If ratio columns are not prefixed correctly.
+ """
+
+ forcolinself._ratio_cols:
+ self.__validate_col_prefix(
+ col,(SOLAR_PREFIX,WIND_PREFIX),input_name="ratios"
+ )
+
+ def_validate_ratio_cols_exist(self):
+"""Ensure the ratio columns exist if a ratio is specified.
+
+ Raises
+ ------
+ FileInputError
+ If ratio columns are not found in the meta data.
+ """
+
+ forcolinself._ratio_cols:
+ no_prefix_name="_".join(col.split("_")[1:])
+ ifnotself.data.contains_col(no_prefix_name):
+ msg=(
+ "Input ratios column {!r} not found in either meta "
+ "data! Please check the input files {!r} and {!r}"
+ )
+ e=msg.format(
+ no_prefix_name,self.data.solar_fpath,self.data.wind_fpath
+ )
+ logger.error(e)
+ raiseFileInputError(e)
+
+ @property
+ def_ratio_cols(self):
+"""Get the ratio columns from the ratio input."""
+ ifself._ratioisNone:
+ return[]
+ returnself._ratio.strip().split("/")
+
+
[docs]defhybridize(self):
+"""Combine the solar and wind metas and run hybridize methods."""
+ self._format_meta_pre_merge()
+ self._merge_solar_wind_meta()
+ self._verify_lat_lon_match_post_merge()
+ self._format_meta_post_merge()
+ self._fillna_meta_cols()
+ self._apply_limits()
+ self._limit_by_ratio()
+ self._add_hybrid_cols()
+ self._sort_hybrid_meta_cols()
+
+ def_format_meta_pre_merge(self):
+"""Prepare solar and wind meta for merging."""
+ self.__col_name_map={
+ ColNameFormatter.fmt(c):c
+ forcinself.data.solar_meta.columns.values
+ }
+
+ self._rename_cols(self.data.solar_meta,prefix=SOLAR_PREFIX)
+ self._rename_cols(self.data.wind_meta,prefix=WIND_PREFIX)
+
+ self._save_rep_prof_index_internally()
+
+ @staticmethod
+ def_rename_cols(df,prefix):
+"""Replace column names with the ColNameFormatter.fmt is needed."""
+ df.columns=[
+ ColNameFormatter.fmt(col_name)
+ ifcol_nameinNON_DUPLICATE_COLS
+ else"{}{}".format(prefix,col_name)
+ forcol_nameindf.columns.values
+ ]
+
+ def_save_rep_prof_index_internally(self):
+"""Save rep profiles index in hybrid meta for access later."""
+
+ self.data.solar_meta[self.__solar_rpi_n]=self.data.solar_meta.index
+ self.data.wind_meta[self.__wind_rpi_n]=self.data.wind_meta.index
+
+ def_merge_solar_wind_meta(self):
+"""Merge the wind and solar meta DataFrames."""
+ self._hybrid_meta=self.data.solar_meta.merge(
+ self.data.wind_meta,
+ on=ColNameFormatter.fmt(MERGE_COLUMN),
+ suffixes=[None,"_x"],
+ how=self._merge_type(),
+ )
+
+ def_merge_type(self):
+"""Determine the type of merge to use for meta based on user input."""
+ ifself._allow_solar_onlyandself._allow_wind_only:
+ return'outer'
+ ifself._allow_solar_onlyandnotself._allow_wind_only:
+ return'left'
+ ifnotself._allow_solar_onlyandself._allow_wind_only:
+ return'right'
+ return'inner'
+
+ def_format_meta_post_merge(self):
+"""Format hybrid meta after merging."""
+
+ duplicate_cols=[nforninself._hybrid_meta.columnsif"_x"inn]
+ self._propagate_duplicate_cols(duplicate_cols)
+ self._drop_cols(duplicate_cols)
+ self._hybrid_meta.rename(self.__col_name_map,inplace=True,axis=1)
+ self._hybrid_meta.index.name=HYBRIDS_GID_COL
+
+ def_propagate_duplicate_cols(self,duplicate_cols):
+"""Fill missing column values from outer merge."""
+ forduplicateinduplicate_cols:
+ no_suffix="_".join(duplicate.split("_")[:-1])
+ null_idx=self._hybrid_meta[no_suffix].isnull()
+ non_null_vals=self._hybrid_meta.loc[null_idx,duplicate].values
+ self._hybrid_meta.loc[null_idx,no_suffix]=non_null_vals
+
+ def_drop_cols(self,duplicate_cols):
+"""Drop any remaining duplicate and 'HYBRIDS_GID_COL' columns."""
+ self._hybrid_meta.drop(
+ duplicate_cols+[HYBRIDS_GID_COL],
+ axis=1,
+ inplace=True,
+ errors="ignore",
+ )
+
+ def_sort_hybrid_meta_cols(self):
+"""Sort the columns of the hybrid meta."""
+ self.__hybrid_meta_cols=sorted(
+ [
+ c
+ forcinself._hybrid_meta.columns
+ ifnotc.startswith(self._INTERNAL_COL_PREFIX)
+ ],
+ key=self._column_sorting_key,
+ )
+
+ def_column_sorting_key(self,c):
+"""Helper function to sort hybrid meta columns."""
+ first_index=0
+ ifc.startswith("hybrid"):
+ first_index=1
+ elifc.startswith("solar"):
+ first_index=2
+ elifc.startswith("wind"):
+ first_index=3
+ elifc==MERGE_COLUMN:
+ first_index=-1
+ returnfirst_index,self._hybrid_meta.columns.get_loc(c)
+
+ def_verify_lat_lon_match_post_merge(self):
+"""Verify that all the lat/lon values match post merge."""
+ lat=self._verify_col_match_post_merge(
+ col_name=ColNameFormatter.fmt(SupplyCurveField.LATITUDE)
+ )
+ lon=self._verify_col_match_post_merge(
+ col_name=ColNameFormatter.fmt(SupplyCurveField.LONGITUDE)
+ )
+ ifnotlatornotlon:
+ msg=(
+ "Detected mismatched coordinate values (latitude or "
+ "longitude) post merge. Please ensure that all matching "
+ "values of {!r} correspond to the same values of latitude "
+ "and longitude across the input files {!r} and {!r}"
+ )
+ e=msg.format(
+ MERGE_COLUMN,self.data.solar_fpath,self.data.wind_fpath
+ )
+ logger.error(e)
+ raiseFileInputError(e)
+
+ def_verify_col_match_post_merge(self,col_name):
+"""Verify that all (non-null) values in a column match post merge."""
+ c1,c2=col_name,'{}_x'.format(col_name)
+ ifc1inself._hybrid_meta.columnsandc2inself._hybrid_meta.columns:
+ compare_df=self._hybrid_meta[
+ (self._hybrid_meta[c1].notnull())
+ &(self._hybrid_meta[c2].notnull())
+ ]
+ returnnp.allclose(compare_df[c1],compare_df[c2])
+ returnTrue
+
+ def_fillna_meta_cols(self):
+"""Fill N/A values as specified by user (and internals)."""
+ forcol_name,fill_valueinself._fillna.items():
+ ifcol_nameinself._hybrid_meta.columns:
+ self._hybrid_meta[col_name].fillna(fill_value,inplace=True)
+ else:
+ self.__warn_missing_col(col_name,action="fill")
+
+ self._hybrid_meta[self.__solar_rpi_n].fillna(-1,inplace=True)
+ self._hybrid_meta[self.__wind_rpi_n].fillna(-1,inplace=True)
+
+ @staticmethod
+ def__warn_missing_col(col_name,action):
+"""Warn that a column the user request an action for is missing."""
+ msg=("Skipping {} values for {!r}: Unable to find column "
+ "in hybrid meta. Did you forget to prefix with "
+ "{!r} or {!r}? ")
+ w=msg.format(action,col_name,SOLAR_PREFIX,WIND_PREFIX)
+ logger.warning(w)
+ warn(w,InputWarning)
+
+ def_apply_limits(self):
+"""Clip column values as specified by user."""
+ forcol_name,max_valueinself._limits.items():
+ ifcol_nameinself._hybrid_meta.columns:
+ self._hybrid_meta[col_name].clip(upper=max_value,inplace=True)
+ else:
+ self.__warn_missing_col(col_name,action="limit")
+
+ def_limit_by_ratio(self):
+"""Limit the given pair of ratio columns based on input ratio."""
+
+ ifself._ratio_boundsisNone:
+ return
+
+ numerator_col,denominator_col=self._ratio_cols
+ min_ratio,max_ratio=sorted(self._ratio_bounds)
+
+ overlap_idx=self._hybrid_meta[MERGE_COLUMN].isin(
+ self.data.merge_col_overlap_values
+ )
+
+ numerator_vals=self._hybrid_meta[numerator_col].copy()
+ denominator_vals=self._hybrid_meta[denominator_col].copy()
+
+ ratios=(
+ numerator_vals.loc[overlap_idx]/denominator_vals.loc[overlap_idx]
+ )
+ ratio_too_low=(ratios<min_ratio)&overlap_idx
+ ratio_too_high=(ratios>max_ratio)&overlap_idx
+
+ numerator_vals.loc[ratio_too_high]=(
+ denominator_vals.loc[ratio_too_high].values*max_ratio
+ )
+ denominator_vals.loc[ratio_too_low]=(
+ numerator_vals.loc[ratio_too_low].values/min_ratio
+ )
+
+ h_num_name="hybrid_{}".format(numerator_col)
+ h_denom_name="hybrid_{}".format(denominator_col)
+ self._hybrid_meta[h_num_name]=numerator_vals.values
+ self._hybrid_meta[h_denom_name]=denominator_vals.values
+
+ def_add_hybrid_cols(self):
+"""Add new hybrid columns using registered hybrid methods."""
+ fornew_col_name,methodinHYBRID_METHODS.items():
+ out=method(self)
+ ifoutisnotNone:
+ try:
+ self._hybrid_meta[new_col_name]=out
+ exceptValueErrorase:
+ msg=(
+ "Unable to add {!r} column to hybrid meta. The "
+ "following exception was raised when adding "
+ "the data output by '{}': {!r}."
+ )
+ w=msg.format(new_col_name,method.__name__,e)
+ logger.warning(w)
+ warn(w,OutputWarning)
+
+ @property
+ defsolar_profile_indices_map(self):
+"""Map hybrid to solar rep indices.
+
+ Returns
+ -------
+ hybrid_indices : np.ndarray
+ Index values corresponding to hybrid rep profiles.
+ solar_indices : np.ndarray
+ Index values of the solar rep profiles corresponding
+ to the hybrid rep profile indices.
+ """
+
+ ifself._hybrid_metaisNone:
+ returnnp.array([]),np.array([])
+
+ idxs=self._hybrid_meta[self.__solar_rpi_n].astype(int)
+ idxs=idxs[idxs>=0]
+
+ returnidxs.index.values,idxs.values
+
+ @property
+ defwind_profile_indices_map(self):
+"""Map hybrid to wind rep indices.
+
+ Returns
+ -------
+ hybrid_indices : np.ndarray
+ Index values corresponding to hybrid rep profiles.
+ wind_indices : np.ndarray
+ Index values of the wind rep profiles corresponding
+ to the hybrid rep profile indices.
+ """
+ ifself._hybrid_metaisNone:
+ returnnp.array([]),np.array([])
+
+ idxs=self._hybrid_meta[self.__wind_rpi_n].astype(int)
+ idxs=idxs[idxs>=0]
+
+ returnidxs.index.values,idxs.values
+
+
+
[docs]classHybridization:
+"""Hybridization"""
+
+ def__init__(
+ self,
+ solar_fpath,
+ wind_fpath,
+ allow_solar_only=False,
+ allow_wind_only=False,
+ fillna=None,
+ limits=None,
+ ratio_bounds=None,
+ ratio="solar_capacity/wind_capacity",
+ ):
+"""Framework to handle hybridization of SC and corresponding profiles.
+
+ ``reV`` hybrids computes a "hybrid" wind and solar supply curve,
+ where each supply curve point contains some wind and some solar
+ capacity. Various ratio limits on wind-to-solar farm properties
+ (e.g. wind-to-solar capacity) can be applied during the
+ hybridization process. Hybrid generation profiles are also
+ computed during this process.
+
+ Parameters
+ ----------
+ solar_fpath : str
+ Filepath to rep profile output file to extract solar
+ profiles and summaries from.
+ wind_fpath : str
+ Filepath to rep profile output file to extract wind profiles
+ and summaries from.
+ allow_solar_only : bool, optional
+ Option to allow SC points with only solar capacity
+ (no wind). By default, ``False``.
+ allow_wind_only : bool, optional
+ Option to allow SC points with only wind capacity
+ (no solar). By default, ``False``.
+ fillna : dict, optional
+ Dictionary containing column_name, fill_value pairs
+ representing any fill values that should be applied after
+ merging the wind and solar meta. Note that column names will
+ likely have to be prefixed with ``solar`` or ``wind``.
+ By default ``None``.
+ limits : dict, optional
+ Option to specify mapping (in the form of a dictionary) of
+ {colum_name: max_value} representing the upper limit
+ (maximum value) for the values of a column in the merged
+ meta. For example, ``limits={'solar_capacity': 100}`` would
+ limit all the values of the solar capacity in the merged
+ meta to a maximum value of 100. This limit is applied
+ *BEFORE* ratio calculations. The names of the columns should
+ match the column names in the merged meta, so they are
+ likely prefixed with ``solar`` or ``wind``.
+ By default, ``None`` (no limits applied).
+ ratio_bounds : tuple, optional
+ Option to set ratio bounds (in two-tuple form) on the
+ columns of the ``ratio`` input. For example,
+ ``ratio_bounds=(0.5, 1.5)`` would adjust the values of both
+ of the ``ratio`` columns such that their ratio is always
+ between half and double (e.g., no value would be more than
+ double the other). To specify a single ratio value, use the
+ same value as the upper and lower bound. For example,
+ ``ratio_bounds=(1, 1)`` would adjust the values of both of
+ the ``ratio`` columns such that their ratio is always equal.
+ By default, ``None`` (no limit on the ratio).
+ ratio : str, optional
+ Option to specify the columns used to calculate the ratio
+ that is limited by the `ratio_bounds` input. This input is a
+ string in the form "{numerator_column}/{denominator_column}".
+ For example, ``ratio='solar_capacity/wind_capacity'``
+ would limit the ratio of the solar to wind capacities as
+ specified by the ``ratio_bounds`` input. If ``ratio_bounds``
+ is None, this input does nothing. The names of the columns
+ should be prefixed with one of the prefixes defined as class
+ variables. By default ``'solar_capacity/wind_capacity'``.
+ """
+
+ logger.info(
+ "Running hybridization of rep profiles with solar_fpath: "
+ '"{}"'.format(solar_fpath)
+ )
+ logger.info(
+ "Running hybridization of rep profiles with solar_fpath: "
+ '"{}"'.format(wind_fpath)
+ )
+ logger.info(
+ "Running hybridization of rep profiles with "
+ 'allow_solar_only: "{}"'.format(allow_solar_only)
+ )
+ logger.info(
+ "Running hybridization of rep profiles with "
+ 'allow_wind_only: "{}"'.format(allow_wind_only)
+ )
+ logger.info(
+ 'Running hybridization of rep profiles with fillna: "{}"'.format(
+ fillna
+ )
+ )
+ logger.info(
+ 'Running hybridization of rep profiles with limits: "{}"'.format(
+ limits
+ )
+ )
+ logger.info(
+ "Running hybridization of rep profiles with ratio_bounds: "
+ '"{}"'.format(ratio_bounds)
+ )
+ logger.info(
+ 'Running hybridization of rep profiles with ratio: "{}"'.format(
+ ratio
+ )
+ )
+
+ self.data=HybridsData(solar_fpath,wind_fpath)
+ self.meta_hybridizer=MetaHybridizer(
+ data=self.data,
+ allow_solar_only=allow_solar_only,
+ allow_wind_only=allow_wind_only,
+ fillna=fillna,
+ limits=limits,
+ ratio_bounds=ratio_bounds,
+ ratio=ratio,
+ )
+ self._profiles=None
+ self._validate_input()
+
+ def_validate_input(self):
+"""Validate the user input and input files."""
+ self.data.validate()
+ self.meta_hybridizer.validate_input()
+
+ @property
+ defsolar_meta(self):
+"""Summary for the solar representative profiles.
+
+ Returns
+ -------
+ solar_meta : pd.DataFrame
+ Summary for the solar representative profiles.
+ """
+ returnself.data.solar_meta
+
+ @property
+ defwind_meta(self):
+"""Summary for the wind representative profiles.
+
+ Returns
+ -------
+ wind_meta : pd.DataFrame
+ Summary for the wind representative profiles.
+ """
+ returnself.data.wind_meta
+
+ @property
+ defhybrid_meta(self):
+"""Hybridized summary for the representative profiles.
+
+ Returns
+ -------
+ hybrid_meta : pd.DataFrame
+ Summary for the hybridized representative profiles.
+ At the very least, this has a column that the data was merged on.
+ """
+ returnself.meta_hybridizer.hybrid_meta
+
+ @property
+ defsolar_time_index(self):
+"""Get the time index for the solar rep profiles.
+
+ Returns
+ -------
+ solar_time_index : pd.Datetimeindex
+ Time index sourced from the solar rep profile file.
+ """
+ returnself.data.solar_time_index
+
+ @property
+ defwind_time_index(self):
+"""Get the time index for the wind rep profiles.
+
+ Returns
+ -------
+ wind_time_index : pd.Datetimeindex
+ Time index sourced from the wind rep profile file.
+ """
+ returnself.data.wind_time_index
+
+ @property
+ defhybrid_time_index(self):
+"""Get the time index for the hybrid rep profiles.
+
+ Returns
+ -------
+ hybrid_time_index : pd.Datetimeindex
+ Time index for the hybrid rep profiles.
+ """
+ returnself.data.hybrid_time_index
+
+ @property
+ defprofiles(self):
+"""Get the arrays of the hybridized representative profiles.
+
+ Returns
+ -------
+ profiles : dict
+ Dict of hybridized representative profiles.
+ """
+ returnself._profiles
+
+
[docs]defrun(self,fout=None,save_hybrid_meta=True):
+"""Run hybridization of profiles and save to disc.
+
+ Parameters
+ ----------
+ fout : str, optional
+ Filepath to output HDF5 file. If ``None``, output data are
+ not written to a file. By default, ``None``.
+ save_hybrid_meta : bool, optional
+ Flag to save hybrid SC table to hybrid rep profile output.
+ By default, ``True``.
+
+ Returns
+ -------
+ str
+ Filepath to output h5 file.
+ """
+
+ self.run_meta()
+ self.run_profiles()
+
+ iffoutisnotNone:
+ self.save_profiles(fout,save_hybrid_meta=save_hybrid_meta)
+
+ logger.info("Hybridization of representative profiles complete!")
+ returnfout
+
+
[docs]defrun_meta(self):
+"""Compute the hybridized profiles.
+
+ Returns
+ -------
+ `Hybridization`
+ Instance of Hybridization object (itself) containing the
+ hybridized meta as an attribute.
+ """
+ self.meta_hybridizer.hybridize()
+ returnself
[docs]classPowerCurve:
+"""A turbine power curve.
+
+ Attributes
+ ----------
+ wind_speed : :obj:`numpy.array`
+ An array containing the wind speeds corresponding to the values
+ in the :attr:`generation` array.
+ generation : :obj:`numpy.array`
+ An array containing the generated power in kW at the corresponding
+ wind speed in the :attr:`wind_speed` array. This input must have
+ at least one positive value, and if a cutoff speed is detected
+ (see `Warnings` section below), then all values above that wind
+ speed must be set to 0.
+
+ Warnings
+ --------
+ This class will attempt to infer a cutoff speed from the
+ ``generation`` input. Specifically, it will look for a transition
+ from the highest rated power down to zero in a single ``wind_speed``
+ step of the power curve. If such a transition is detected, the wind
+ speed corresponding to the zero value will be set as the cutoff
+ speed, and all calculated power curves will be clipped at this
+ speed. If your input power curve contains a cutoff speed, ensure
+ that it adheres to the expected pattern of dropping from max rated
+ power to zero power in a single wind speed step.
+ """
+
+ def__init__(self,wind_speed,generation):
+"""
+ Parameters
+ ----------
+ wind_speed : array_like
+ An iterable containing the wind speeds corresponding to the
+ generated power values in ``generation`` input. The input
+ values should all be non-zero.
+ generation : array_like
+ An iterable containing the generated power in kW at the
+ corresponding wind speed in the ``wind_speed`` input. This
+ input must have at least one positive value, and if a cutoff
+ speed is detected (see `Warnings` section below), then all
+ values above that wind speed must be set to 0.
+ """
+ self.wind_speed=np.array(wind_speed)
+ self.generation=np.array(generation)
+ self._cutoff_wind_speed=None
+ self._cutin_wind_speed=None
+ self.i_cutoff=None
+
+ _validate_arrays_not_empty(self,
+ array_names=['wind_speed','generation'])
+ self._validate_wind_speed()
+ self._validate_generation()
+
+ def_validate_wind_speed(self):
+"""Validate that the input wind speed is non-negative. """
+ ifnot(self.wind_speed>=0).all():
+ msg=("Invalid wind speed input: Contains negative values! - {}"
+ .format(self.wind_speed))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_generation(self):
+"""Validate the input generation. """
+ ifnot(self.generation>0).any():
+ msg=("Invalid generation input: Found no positive values! - {}"
+ .format(self.generation))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ if0<self.cutoff_wind_speed<np.inf:
+ wind_speeds_above_cutoff=np.where(self.wind_speed
+ >=self.cutoff_wind_speed)
+ cutoff_wind_speed_ind=wind_speeds_above_cutoff[0].min()
+ if(self.generation[cutoff_wind_speed_ind:]).any():
+ msg=("Invalid generation input: Found non-zero values above "
+ "cutoff! - {}".format(self.generation))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ @property
+ defcutin_wind_speed(self):
+"""The detected cut-in wind speed at which power generation begins
+
+ Returns
+ --------
+ float
+ """
+ ifself._cutin_wind_speedisNone:
+ ind=np.where(self.generation>0)[0][0]
+ ifind>0:
+ self._cutin_wind_speed=self.wind_speed[ind-1]
+ else:
+ self._cutin_wind_speed=0
+ returnself._cutin_wind_speed
+
+ @property
+ defcutoff_wind_speed(self):
+"""The detected cutoff wind speed at which the power generation is zero
+
+ Returns
+ --------
+ float | np.inf
+ """
+ ifself._cutoff_wind_speedisNone:
+ ind=np.argmax(self.generation[::-1])
+ # pylint: disable=chained-comparison
+ ifind>0andself.generation[-ind]<=0:
+ self.i_cutoff=len(self.generation)-ind
+ self._cutoff_wind_speed=self.wind_speed[-ind]
+ else:
+ self._cutoff_wind_speed=np.inf
+ returnself._cutoff_wind_speed
+
+ @property
+ defrated_power(self):
+"""Get the rated power (max power) of the turbine power curve. The
+ units are dependent on the input power curve but this is typically in
+ units of kW.
+
+ Returns
+ -------
+ float
+ """
+ returnnp.max(self.generation)
+
+ def__eq__(self,other):
+ returnnp.isclose(self.generation,other).all()
+
+ def__ne__(self,other):
+ returnnotnp.isclose(self.generation,other).all()
+
+ def__lt__(self,other):
+ returnself.generation<other
+
+ def__le__(self,other):
+ returnself.generation<=other
+
+ def__gt__(self,other):
+ returnself.generation>other
+
+ def__ge__(self,other):
+ returnself.generation>=other
+
+ def__len__(self):
+ returnlen(self.generation)
+
+ def__getitem__(self,index):
+ returnself.generation[index]
+
+
[docs]def__call__(self,wind_speed):
+"""Calculate the power curve value for the given ``wind_speed``.
+
+ Parameters
+ ----------
+ wind_speed : int | float | list | array_like
+ Wind speed value corresponding to the desired power curve
+ value.
+
+ Returns
+ -------
+ float | :obj:`numpy.array`
+ The power curve value(s) for the input wind speed(s).
+ """
+ ifisinstance(wind_speed,(int,float)):
+ wind_speed=np.array([wind_speed])
+ power_generated=np.interp(wind_speed,self.wind_speed,
+ self.generation)
+ ifself.cutoff_wind_speed:
+ power_generated[wind_speed>=self.cutoff_wind_speed]=0
+ returnpower_generated
+
+
+
[docs]classPowerCurveLosses:
+"""A converter between annual losses and power curve transformation.
+
+ Given a target annual loss value, this class facilitates the
+ calculation of a power curve transformation such that the annual
+ generation losses incurred by using the transformed power curve when
+ compared to the original (non-transformed) power curve match the
+ target loss as close as possible.
+
+ The underlying assumption for this approach is that some types of
+ losses can be realized by a transformation of the power curve (see
+ the values of :obj:`TRANSFORMATIONS` for details on all of the
+ power curve transformations that have been implemented).
+
+ The advantage of this approach is that, unlike haircut losses (where
+ a single loss value is applied across the board to all generation),
+ the losses are distributed non-uniformly across the power curve. For
+ example, even in the overly simplified case of a horizontal
+ translation of the power curve (which is only physically realistic
+ for certain types of losses like blade degradation), the losses are
+ distributed primarily across region 2 of the power curve (the steep,
+ almost linear, portion where the generation rapidly increases). This
+ means that, unlike with haircut losses, generation is able to reach
+ max rated power (albeit at a greater wind speed).
+
+ Attributes
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The original Power Curve.
+ wind_resource : :obj:`numpy.array`
+ An array containing the wind speeds (i.e. wind speed
+ distribution) for the site at which the power curve will be
+ used. This distribution is used to calculate the annual
+ generation of the original power curve as well as any additional
+ calculated power curves. The generation values are then compared
+ in order to calculate the loss resulting from a transformed
+ power curve.
+ weights : :obj:`numpy.array`
+ An array of the same length as ``wind_resource`` containing
+ weights to apply to each generation value calculated for the
+ corresponding wind speed.
+ """
+
+ def__init__(self,power_curve,wind_resource,weights=None,site=None):
+"""
+ Parameters
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The "original" power curve to be adjusted.
+ wind_resource : array_like
+ An iterable containing the wind speeds measured at the site
+ where this power curve will be applied to calculate
+ generation. These values are used to calculate the loss
+ resulting from a transformed power curve compared to the
+ generation of the original power curve. The input
+ values should all be non-zero, and the units of
+ should match the units of the ``power_curve`` input
+ (typically, m/s).
+ weights : array_like, optional
+ An iterable of the same length as ``wind_resource``
+ containing weights to apply to each generation value
+ calculated for the corresponding wind speed.
+ site : int | str, optional
+ Site number (gid) for debugging and logging.
+ By default, ``None``.
+ """
+
+ self.power_curve=power_curve
+ self.wind_resource=np.array(wind_resource)
+ ifweightsisNone:
+ self.weights=np.ones_like(self.wind_resource)
+ else:
+ self.weights=np.array(weights)
+ self._power_gen=None
+ self.site="[unknown]"ifsiteisNoneelsesite
+
+ _validate_arrays_not_empty(self,
+ array_names=['wind_resource','weights'])
+ self._validate_wind_resource()
+ self._validate_weights()
+
+ def_validate_wind_resource(self):
+"""Validate that the input wind resource is non-negative. """
+ ifnot(self.wind_resource>=0).all():
+ msg=("Invalid wind resource input for site {}: Contains "
+ "negative values! - {}"
+ .format(self.site,self.wind_resource))
+ msg=msg.format(self.wind_resource)
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_weights(self):
+"""Validate that the input weights size matches the wind resource. """
+ ifself.wind_resource.size!=self.weights.size:
+ msg=("Invalid weights input: Does not match size of wind "
+ "resource for site {}! - {} vs {}"
+ .format(self.site,self.weights.size,
+ self.wind_resource.size))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+
[docs]defannual_losses_with_transformed_power_curve(
+ self,transformed_power_curve
+ ):
+"""Calculate the annual losses from a transformed power curve.
+
+ This function uses the wind resource data that the object was
+ initialized with to calculate the total annual power generation
+ with a transformed power curve. This generation is compared with
+ the generation of the original (non-transformed) power curve to
+ compute the total annual losses as a result of the
+ transformation.
+
+ Parameters
+ ----------
+ transformed_power_curve : :obj:`PowerCurve`
+ A transformed power curve. The power generated with this
+ power curve will be compared with the power generated by the
+ "original" power curve to calculate annual losses.
+
+ Returns
+ -------
+ float
+ Total losses (%) as a result of a the power curve
+ transformation.
+ """
+ power_gen_with_losses=transformed_power_curve(self.wind_resource)
+ power_gen_with_losses*=self.weights
+ power_gen_with_losses=power_gen_with_losses.sum()
+ return(1-power_gen_with_losses/self.power_gen_no_losses)*100
[docs]deffit(self,target,transformation):
+"""Fit a power curve transformation.
+
+ This function fits a transformation to the input power curve
+ (the one used to initialize the object) to generate an annual
+ loss percentage closest to the ``target``. The losses are
+ computed w.r.t the generation of the original (non-transformed)
+ power curve.
+
+ Parameters
+ ----------
+ target : float
+ Target value for annual generation losses (%).
+ transformation : PowerCurveTransformation
+ A PowerCurveTransformation class representing the power
+ curve transformation to use.
+
+ Returns
+ -------
+ :obj:`numpy.array`
+ An array containing a transformed power curve that most
+ closely yields the ``target`` annual generation losses.
+
+ Warns
+ -----
+ reVLossesWarning
+ If the fit did not meet the target annual losses to within
+ 1%.
+
+ Warnings
+ --------
+ This function attempts to find an optimal transformation for the
+ power curve such that the annual generation losses match the
+ ``target`` value, but there is no guarantee that a close match
+ can be found, if it even exists. Therefore, it is possible that
+ the losses resulting from the transformed power curve will not
+ match the ``target``. This is especially likely if the
+ ``target`` is large or if the input power curve and/or wind
+ resource is abnormal.
+ """
+ transformation=transformation(self.power_curve)
+ fit_var=minimize_scalar(self._obj,
+ args=(target,transformation),
+ bounds=transformation.optm_bounds,
+ method='bounded').x
+
+ iffit_var>np.max(transformation.bounds):
+ msg=('Transformation "{}" for site {} resulted in fit parameter '
+ '{} greater than the max bound of {}. Limiting to the max '
+ 'bound, but the applied losses may not be correct.'
+ .format(transformation,self.site,fit_var,
+ np.max(transformation.bounds)))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+ fit_var=np.max(transformation.bounds)
+
+ iffit_var<np.min(transformation.bounds):
+ msg=('Transformation "{}" for site {} resulted in fit parameter '
+ '{} less than the min bound of {}. Limiting to the min '
+ 'bound, but the applied losses may not be correct.'
+ .format(transformation,self.site,fit_var,
+ np.min(transformation.bounds)))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+ fit_var=np.min(transformation.bounds)
+
+ error=self._obj(fit_var,target,transformation)
+
+ iferror>1:
+ new_power_curve=transformation.apply(fit_var)
+ losses=self.annual_losses_with_transformed_power_curve(
+ new_power_curve)
+ msg=("Unable to find a transformation for site {} such that the "
+ "losses meet the target within 1%! Obtained fit with loss "
+ "percentage {}% when target was {}%. Consider using a "
+ "different transformation or reducing the target losses!"
+ .format(self.site,losses,target))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+
+ returntransformation.apply(fit_var)
+
+ @property
+ defpower_gen_no_losses(self):
+"""float: Total power generation from original power curve."""
+ ifself._power_genisNone:
+ self._power_gen=self.power_curve(self.wind_resource)
+ self._power_gen*=self.weights
+ self._power_gen=self._power_gen.sum()
+ returnself._power_gen
+
+
+
[docs]classPowerCurveLossesInput:
+"""Power curve losses specification.
+
+ This class stores and validates information about the desired losses
+ from a given type of power curve transformation. In particular, the
+ target loss percentage must be provided. This input is then
+ validated to be used power curve transformation fitting.
+
+ """
+
+ REQUIRED_KEYS={'target_losses_percent'}
+"""Required keys in the input specification dictionary."""
+
+ def__init__(self,specs):
+"""
+
+ Parameters
+ ----------
+ specs : dict
+ A dictionary containing specifications for the power curve
+ losses. This dictionary must contain the following keys:
+
+ - ``target_losses_percent``
+ An integer or float value representing the
+ total percentage of annual energy production that
+ should be lost due to the power curve transformation.
+ This value must be in the range [0, 100].
+
+ The input dictionary can also provide the following optional
+ keys:
+
+ - ``transformation`` - by default, ``horizontal_translation``
+ A string representing the type of transformation to
+ apply to the power curve. This sting must be one of
+ the keys of :obj:`TRANSFORMATIONS`. See the relevant
+ transformation class documentation for detailed
+ information on that type of power curve
+ transformation.
+
+
+ """
+ self._specs=specs
+ self._transformation_name=self._specs.get('transformation',
+ 'exponential_stretching')
+ self._validate()
+
+ def_validate(self):
+"""Validate the input specs."""
+ self._validate_required_keys_exist()
+ self._validate_transformation()
+ self._validate_percentage()
+
+ def_validate_required_keys_exist(self):
+"""Raise an error if any required keys are missing."""
+ missing_keys=[nnotinself._specsforninself.REQUIRED_KEYS]
+ ifany(missing_keys):
+ msg=("The following required keys are missing from the power "
+ "curve losses specification: {}"
+ .format(sorted(missing_keys)))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_transformation(self):
+"""Validate that the transformation exists in TRANSFORMATIONS. """
+ ifself._transformation_namenotinTRANSFORMATIONS:
+ msg=("Transformation {!r} not understood! "
+ "Input must be one of: {} "
+ .format(self._transformation_name,
+ list(TRANSFORMATIONS.keys())))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_percentage(self):
+"""Validate that the percentage is in the range [0, 100]. """
+ ifnot0<=self.target<=100:
+ msg=("Percentage of annual energy production loss to be "
+ "attributed to the power curve transformation must be in "
+ "the range [0, 100], but got {} for transformation {!r}"
+ .format(self.target,self._transformation_name))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def__repr__(self):
+ specs=self._specs.copy()
+ specs.update({'transformation':self._transformation_name})
+ specs_as_str=", ".join(["{}={!r}".format(k,v)
+ fork,vinspecs.items()])
+ return"PowerCurveLossesInput({})".format(specs_as_str)
+
+ @property
+ deftarget(self):
+"""int or float: Target loss percentage due to transformation."""
+ returnself._specs['target_losses_percent']
+
+ @property
+ deftransformation(self):
+"""PowerCurveTransformation: Power curve transformation."""
+ returnTRANSFORMATIONS[self._transformation_name]
+
+
+
[docs]classPowerCurveWindResource:
+"""Wind resource data for calculating power curve shift."""
+
+ def__init__(self,temperature,pressure,wind_speed):
+"""Power Curve Wind Resource.
+
+ Parameters
+ ----------
+ temperature : array_like
+ An iterable representing the temperatures at a single site
+ (in C). Must be the same length as the `pressure` and
+ `wind_speed` inputs.
+ pressure : array_like
+ An iterable representing the pressures at a single site
+ (in PA or ATM). Must be the same length as the `temperature`
+ and `wind_speed` inputs.
+ wind_speed : array_like
+ An iterable representing the wind speeds at a single site
+ (in m/s). Must be the same length as the `temperature` and
+ `pressure` inputs.
+ """
+ self._temperatures=np.array(temperature)
+ self._pressures=np.array(pressure)
+ self._wind_speeds=np.array(wind_speed)
+ self.wind_speed_weights=None
+
+
[docs]defwind_resource_for_site(self):
+"""Extract scaled wind speeds at the resource site.
+
+ Get the wind speeds for this site, accounting for the scaling
+ done in SAM [1]_ based on air pressure [2]_. These wind speeds
+ can then be used to sample the power curve and obtain generation
+ values.
+
+ Returns
+ -------
+ array-like
+ Array of scaled wind speeds.
+
+ References
+ ----------
+ .. [1] Scaling done in SAM: https://tinyurl.com/2uzjawpe
+ .. [2] SAM Wind Power Reference Manual for explanations on
+ generation and air density calculations (pp. 18):
+ https://tinyurl.com/2p8fjba6
+
+ """
+ ifself._pressures.max()<2:# units are ATM
+ pressures_pascal=self._pressures*101325.027383
+ elifself._pressures.min()>1e4:# units are PA
+ pressures_pascal=self._pressures
+ else:
+ msg=("Unable to determine pressure units: pressure values "
+ "found in the range {:.2f} to {:.2f}. Please make "
+ "sure input pressures are in units of PA or ATM"
+ .format(self._pressures.min(),self._pressures.max()))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ temperatures_K=self._temperatures+273.15# originally in celsius
+ specific_gas_constant_dry_air=287.058# units: J / kg / K
+ sea_level_air_density=1.225# units: kg/m**3 at 15 degrees celsius
+
+ site_air_densities=pressures_pascal/(specific_gas_constant_dry_air
+ *temperatures_K)
+ weights=(sea_level_air_density/site_air_densities)**(1/3)
+ returnself._wind_speeds/weights
+
+
+class_PowerCurveWindDistribution:
+"""`PowerCurveWindResource` interface mocker for wind distributions. """
+
+ def__init__(self,speeds,weights):
+"""Power Curve Wind Resource for Wind Distributions.
+
+ Parameters
+ ----------
+ speeds : array_like
+ An iterable representing the wind speeds at a single site
+ (in m/s). Must be the same length as the `weights` input.
+ weights : array_like
+ An iterable representing the wind speed weights at a single
+ site. Must be the same length as the `speeds` input.
+ """
+ self.wind_speeds=np.array(speeds)
+ self.wind_speed_weights=np.array(weights)
+
+
+
[docs]defadjust_power_curve(power_curve,resource_data,target_losses,site=None):
+"""Adjust power curve to account for losses.
+
+ This function computes a new power curve that accounts for the
+ loss percentage specified from the target loss.
+
+ Parameters
+ ----------
+ power_curve : :obj:`PowerCurve`
+ Power curve to be adjusted to match target losses.
+ resource_data : :obj:`PowerCurveWindResource`
+ Resource data for the site being investigated.
+ target_losses : :obj:`PowerCurveLossesInput`
+ Target loss and power curve shift info.
+ site : int | str, optional
+ Site number (gid) for debugging and logging.
+ By default, ``None``.
+
+ Returns
+ -------
+ :obj:`PowerCurve`
+ Power Curve shifted to meet the target losses. Power Curve is
+ not adjusted if all wind speeds are above the cutout or below
+ the cutin speed.
+
+ See Also
+ --------
+ :obj:`PowerCurveLosses` : Power curve re-calculation.
+ """
+ site="[unknown]"ifsiteisNoneelsesite
+
+ if(resource_data.wind_speeds<=power_curve.cutin_wind_speed).all():
+ msg=("All wind speeds for site {} are below the wind speed "
+ "cutin ({} m/s). No power curve adjustments made!"
+ .format(site,power_curve.cutin_wind_speed))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+ returnpower_curve
+
+ if(resource_data.wind_speeds>=power_curve.cutoff_wind_speed).all():
+ msg=("All wind speeds for site {} are above the wind speed "
+ "cutoff ({} m/s). No power curve adjustments made!"
+ .format(site,power_curve.cutoff_wind_speed))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+ returnpower_curve
+
+ pc_losses=PowerCurveLosses(power_curve,resource_data.wind_speeds,
+ resource_data.wind_speed_weights,site=site)
+
+ logger.debug("Transforming power curve using the {} transformation to "
+ "meet {}% loss target..."
+ .format(target_losses.transformation,target_losses.target))
+
+ new_curve=pc_losses.fit(target_losses.target,
+ target_losses.transformation)
+ logger.debug("Transformed power curve: {}".format(list(new_curve)))
+ returnnew_curve
+
+
+
[docs]classPowerCurveLossesMixin:
+"""Mixin class for :class:`reV.SAM.generation.AbstractSamWind`.
+
+ Warnings
+ --------
+ Using this class for anything except as a mixin for
+ :class:`~reV.SAM.generation.AbstractSamWind` may result in
+ unexpected results and/or errors.
+ """
+
+ POWER_CURVE_CONFIG_KEY='reV_power_curve_losses'
+"""Specify power curve loss target in the config file using this key."""
+
+
[docs]defadd_power_curve_losses(self):
+"""Adjust power curve in SAM config file to account for losses.
+
+ This function reads the information in the
+ ``reV_power_curve_losses`` key of the ``sam_sys_inputs``
+ dictionary and computes a new power curve that accounts for the
+ loss percentage specified from that input. If no power curve
+ loss info is specified in ``sam_sys_inputs``, the power curve
+ will not be adjusted.
+
+ See Also
+ --------
+ :func:`adjust_power_curve` : Power curve shift calculation.
+ """
+ loss_input=self._user_power_curve_input()
+ ifnotloss_input:
+ return
+
+ resource=self.wind_resource_from_input()
+ site=getattr(self,'site',"[unknown]")
+ new_curve=adjust_power_curve(self.input_power_curve,resource,
+ loss_input,site=site)
+ self.sam_sys_inputs['wind_turbine_powercurve_powerout']=new_curve
+
+ def_user_power_curve_input(self):
+"""Get power curve loss info from config. """
+ power_curve_losses_info=self.sam_sys_inputs.pop(
+ self.POWER_CURVE_CONFIG_KEY,None
+ )
+ ifpower_curve_losses_infoisNone:
+ return
+
+ # site-specific info is input as str
+ ifisinstance(power_curve_losses_info,str):
+ power_curve_losses_info=json.loads(power_curve_losses_info)
+
+ loss_input=PowerCurveLossesInput(power_curve_losses_info)
+ ifloss_input.target<=0:
+ logger.debug("Power curve target loss is 0. Skipping power curve "
+ "transformation.")
+ return
+
+ returnloss_input
+
+ @property
+ definput_power_curve(self):
+""":obj:`PowerCurve`: Original power curve for site. """
+ wind_speed=self.sam_sys_inputs['wind_turbine_powercurve_windspeeds']
+ generation=self.sam_sys_inputs['wind_turbine_powercurve_powerout']
+ returnPowerCurve(wind_speed,generation)
+
+
[docs]defwind_resource_from_input(self):
+"""Collect wind resource and weights from inputs.
+
+ Returns
+ -------
+ :obj:`PowerCurveWindResource`
+ Wind resource used to compute power curve shift.
+
+ Raises
+ ------
+ reVLossesValueError
+ If power curve losses are not compatible with the
+ 'wind_resource_model_choice'.
+ """
+ ifself['wind_resource_model_choice']==0:
+ temperatures,pressures,wind_speeds,__,=map(
+ np.array,zip(*self['wind_resource_data']['data'])
+ )
+ returnPowerCurveWindResource(temperatures,pressures,wind_speeds)
+ elifself['wind_resource_model_choice']==2:
+ wrd=np.array(self['wind_resource_distribution'])
+ return_PowerCurveWindDistribution(wrd[:,0],wrd[:,-1])
+ else:
+ msg=("reV power curve losses cannot be used with "
+ "'wind_resource_model_choice' = {}"
+ .format(self['wind_resource_model_choice']))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+
+
[docs]classAbstractPowerCurveTransformation(ABC):
+"""Abstract base class for power curve transformations.
+
+ **This class is not meant to be instantiated**.
+
+ This class provides an interface for power curve transformations,
+ which are meant to more realistically represent certain types of
+ losses when compared to simple haircut losses (i.e. constant loss
+ value applied at all points on the power curve).
+
+ If you would like to implement your own power curve transformation,
+ you should subclass this class and implement the :meth:`apply`
+ method and the :attr:`bounds` property. See the documentation for
+ each of these below for more details.
+
+ Attributes
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The "original" input power curve.
+ """
+
+ def__init__(self,power_curve):
+"""Abstract Power Curve Transformation class.
+
+ Parameters
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The turbine power curve. This input is treated as the
+ "original" power curve.
+ """
+ self.power_curve=power_curve
+ self._transformed_generation=None
+
+ def_validate_non_zero_generation(self,new_curve):
+"""Ensure new power curve has some non-zero generation."""
+ mask=(self.power_curve.wind_speed
+ <=self.power_curve.cutoff_wind_speed)
+ min_expected_power_gen=self.power_curve[self.power_curve>0].min()
+ ifnot(new_curve[mask]>min_expected_power_gen).any():
+ msg=("Calculated power curve is invalid. No power generation "
+ "below the cutoff wind speed ({} m/s) detected. Target "
+ "loss percentage may be too large! Please try again with "
+ "a lower target value."
+ .format(self.power_curve.cutoff_wind_speed))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_same_cutoff(self,new_curve):
+"""Validate that the new power curve has the same high-wind cutout as
+ the original curve."""
+ old_cut=self.power_curve.cutoff_wind_speed
+ new_cut=new_curve.cutoff_wind_speed
+ ifold_cut!=new_cut:
+ msg=('Original power curve windspeed cutout is {}m/s and new '
+ 'curve cutout is {}m/s. Something went wrong!'
+ .format(old_cut,new_cut))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+
[docs]@abstractmethod
+ defapply(self,transformation_var):
+"""Apply a transformation to the original power curve.
+
+ Parameters
+ ----------
+ transformation_var : : float
+ A single variable controlling the "strength" of the
+ transformation. The :obj:`PowerCurveLosses` object will
+ run an optimization using this variable to fit the target
+ annual losses incurred with the transformed power curve
+ compared to the original power curve using the given wind
+ resource distribution.
+
+ Returns
+ -------
+ :obj:`PowerCurve`
+ An new power curve containing the generation values from the
+ transformed power curve.
+
+ Raises
+ ------
+ NotImplementedError
+ If the transformation implementation did not set the
+ ``_transformed_generation`` attribute.
+
+ Notes
+ -----
+ When implementing a new transformation, override this method and
+ set the ``_transformed_generation`` protected attribute to be
+ the generation corresponding to the transformed power curve.
+ Then, call ``super().apply(transformation_var)`` in order to
+ apply cutout speed curtailment and validation for the
+ transformed power curve. For example, here is the implementation
+ for a transformation that shifts the power curve horizontally::
+
+ self._transformed_generation = self.power_curve(
+ self.power_curve.wind_speed - transformation_var
+ )
+ return super().apply(transformation_var)
+
+ """
+ ifself._transformed_generationisNone:
+ msg=("Transformation implementation for {}.apply did not set "
+ "the `_transformed_generation` attribute."
+ .format(type(self).__name__))
+ logger.error(msg)
+ raiseNotImplementedError(msg)
+
+ ifnotnp.isinf(self.power_curve.cutoff_wind_speed):
+ mask=(self.power_curve.wind_speed
+ >=self.power_curve.cutoff_wind_speed)
+ self._transformed_generation[mask]=0
+ new_curve=PowerCurve(self.power_curve.wind_speed,
+ self._transformed_generation)
+ self._validate_non_zero_generation(new_curve)
+
+ i_max=np.argmax(self._transformed_generation)
+ i_cutoff=self.power_curve.i_cutoff
+ rated_power=self.power_curve.rated_power
+ self._transformed_generation[i_max:i_cutoff]=rated_power
+
+ new_curve=PowerCurve(self.power_curve.wind_speed,
+ self._transformed_generation)
+ self._validate_non_zero_generation(new_curve)
+ self._validate_same_cutoff(new_curve)
+ returnnew_curve
+
+ @property
+ @abstractmethod
+ defbounds(self):
+"""tuple: true Bounds on the ``transformation_var``."""
+
+ @property
+ defoptm_bounds(self):
+"""Bounds for scipy optimization, sometimes more generous than the
+ actual fit parameter bounds which are enforced after the
+ optimization."""
+ returnself.bounds
+
+
+
[docs]classHorizontalTranslation(AbstractPowerCurveTransformation):
+"""Utility for applying horizontal power curve translations.
+
+ The mathematical representation of this transformation is:
+
+ .. math:: P_{transformed}(u) = P_{original}(u - t),
+
+ where :math:`P_{transformed}` is the transformed power curve,
+ :math:`P_{original}` is the original power curve, :math:`u` is
+ the wind speed, and :math:`t` is the transformation variable
+ (horizontal translation amount).
+
+ This kind of power curve transformation is simplistic, and should
+ only be used for a small handful of applicable turbine losses
+ (i.e. blade degradation). See ``Warnings`` for more details.
+
+ The losses in this type of transformation are distributed primarily
+ across region 2 of the power curve (the steep, almost linear,
+ portion where the generation rapidly increases):
+
+ .. image:: ../../../examples/rev_losses/horizontal_translation.png
+ :align: center
+
+ Attributes
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The "original" input power curve.
+
+ Warnings
+ --------
+ This kind of power curve translation is not generally realistic.
+ Using this transformation as a primary source of losses (i.e. many
+ different kinds of losses bundled together) is extremely likely to
+ yield unrealistic results!
+ """
+
+
[docs]defapply(self,transformation_var):
+"""Apply a horizontal translation to the original power curve.
+
+ This function shifts the original power curve horizontally,
+ along the "wind speed" (x) axis, by the given amount. Any power
+ above the cutoff speed (if one was detected) is truncated after
+ the transformation.
+
+ Parameters
+ ----------
+ transformation_var : float
+ The amount to shift the original power curve by, in wind
+ speed units (typically, m/s).
+
+ Returns
+ -------
+ :obj:`PowerCurve`
+ An new power curve containing the generation values from the
+ shifted power curve.
+ """
+ self._transformed_generation=self.power_curve(
+ self.power_curve.wind_speed-transformation_var
+ )
+ returnsuper().apply(transformation_var)
+
+ @property
+ defbounds(self):
+"""tuple: true Bounds on the power curve shift (different from the
+ optimization boundaries)"""
+ min_ind=np.where(self.power_curve)[0][0]
+ max_ind=np.where(self.power_curve[::-1])[0][0]
+ max_shift=(self.power_curve.wind_speed[-max_ind-1]
+ -self.power_curve.wind_speed[min_ind])
+ return(0,max_shift)
+
+
+
[docs]classLinearStretching(AbstractPowerCurveTransformation):
+"""Utility for applying a linear stretch to the power curve.
+
+ The mathematical representation of this transformation is:
+
+ .. math:: P_{transformed}(u) = P_{original}(u/t),
+
+ where :math:`P_{transformed}` is the transformed power curve,
+ :math:`P_{original}` is the original power curve, :math:`u` is
+ the wind speed, and :math:`t` is the transformation variable
+ (wind speed multiplier).
+
+ The losses in this type of transformation are distributed primarily
+ across regions 2 and 3 of the power curve. In particular, losses are
+ smaller for wind speeds closer to the cut-in speed, and larger for
+ speeds close to rated power:
+
+ .. image:: ../../../examples/rev_losses/linear_stretching.png
+ :align: center
+
+ Attributes
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The "original" input power curve.
+ """
+
+
[docs]defapply(self,transformation_var):
+"""Apply a linear stretch to the original power curve.
+
+ This function stretches the original power curve along the
+ "wind speed" (x) axis. Any power above the cutoff speed (if one
+ was detected) is truncated after the transformation.
+
+ Parameters
+ ----------
+ transformation_var : float
+ The linear multiplier of the wind speed scaling.
+
+ Returns
+ -------
+ :obj:`PowerCurve`
+ An new power curve containing the generation values from the
+ shifted power curve.
+ """
+ self._transformed_generation=self.power_curve(
+ self.power_curve.wind_speed/transformation_var
+ )
+ returnsuper().apply(transformation_var)
+
+ @property
+ defbounds(self):
+"""tuple: true Bounds on the wind speed multiplier (different from the
+ optimization boundaries)"""
+ min_ind_pc=np.where(self.power_curve)[0][0]
+ min_ind_ws=np.where(self.power_curve.wind_speed>1)[0][0]
+ min_ws=self.power_curve.wind_speed[max(min_ind_pc,min_ind_ws)]
+ max_ws=min(self.power_curve.wind_speed.max(),
+ self.power_curve.cutoff_wind_speed)
+ max_multiplier=np.ceil(max_ws/min_ws)
+ return(1,max_multiplier)
+
+
+
[docs]classExponentialStretching(AbstractPowerCurveTransformation):
+"""Utility for applying an exponential stretch to the power curve.
+
+ The mathematical representation of this transformation is:
+
+ .. math:: P_{transformed}(u) = P_{original}(u^{1/t}),
+
+ where :math:`P_{transformed}` is the transformed power curve,
+ :math:`P_{original}` is the original power curve, :math:`u` is
+ the wind speed, and :math:`t` is the transformation variable
+ (wind speed exponent).
+
+ The losses in this type of transformation are distributed primarily
+ across regions 2 and 3 of the power curve. In particular, losses are
+ smaller for wind speeds closer to the cut-in speed, and larger for
+ speeds close to rated power:
+
+ .. image:: ../../../examples/rev_losses/exponential_stretching.png
+ :align: center
+
+ Attributes
+ ----------
+ power_curve : :obj:`PowerCurve`
+ The "original" input power curve.
+ """
+
+
[docs]defapply(self,transformation_var):
+"""Apply an exponential stretch to the original power curve.
+
+ This function stretches the original power curve along the
+ "wind speed" (x) axis. Any power above the cutoff speed (if one
+ was detected) is truncated after the transformation.
+
+ Parameters
+ ----------
+ transformation_var : float
+ The exponent of the wind speed scaling.
+
+ Returns
+ -------
+ :obj:`PowerCurve`
+ An new power curve containing the generation values from the
+ shifted power curve.
+ """
+ self._transformed_generation=self.power_curve(
+ self.power_curve.wind_speed**(1/transformation_var)
+ )
+ returnsuper().apply(transformation_var)
+
+ @property
+ defbounds(self):
+"""tuple: Bounds on the wind speed exponent."""
+ min_ind_pc=np.where(self.power_curve)[0][0]
+ min_ind_ws=np.where(self.power_curve.wind_speed>1)[0][0]
+ min_ws=self.power_curve.wind_speed[max(min_ind_pc,min_ind_ws)]
+ max_ws=min(self.power_curve.wind_speed.max(),
+ self.power_curve.cutoff_wind_speed)
+ max_exponent=np.ceil(np.log(max_ws)/np.log(min_ws))
+ return(1,max_exponent)
+
+ @property
+ defoptm_bounds(self):
+"""Bounds for scipy optimization, sometimes more generous than the
+ actual fit parameter bounds which are enforced after the
+ optimization."""
+ return(0.5,self.bounds[1])
[docs]classOutage:
+"""A specific type of outage.
+
+ This class stores and validates information about a single type of
+ outage. In particular, the number of outages, duration, percentage
+ of farm down, and the allowed months for scheduling the outage
+ must all be provided. These inputs are then validated so that they
+ can be used in instances of scheduling objects.
+
+ """
+
+ REQUIRED_KEYS={'count',
+ 'duration',
+ 'percentage_of_capacity_lost',
+ 'allowed_months'}
+"""Required keys in the input specification dictionary."""
+
+ def__init__(self,specs):
+"""
+
+ Parameters
+ ----------
+ specs : dict
+ A dictionary containing specifications for this outage. This
+ dictionary must contain the following keys:
+
+ - `count`
+ An integer value representing the total number of
+ times this outage should be scheduled. This number
+ should be larger than 0.
+ - `duration`
+ An integer value representing the total number of
+ consecutive hours that this outage should take. This
+ value must be larger than 0 and less than the number
+ of hours in the allowed months.
+ - `percentage_of_capacity_lost`
+ An integer or float value representing the total
+ percentage of the total capacity that will be lost
+ for the duration of the outage. This value must be
+ in the range (0, 100].
+ - `allowed_months`
+ A list of month names corresponding to the allowed
+ months for the scheduled outages. Month names can be
+ unformatted and can be specified using 3-letter
+ month abbreviations.
+
+ The input dictionary can also provide the following optional
+ keys:
+
+ - `allow_outage_overlap` - by default, ``True``
+ A bool flag indicating whether or not this outage is
+ allowed to overlap with other outages, including
+ itself. It is recommended to set this value to
+ ``True`` whenever possible, as it allows for more
+ flexible scheduling.
+ - `name` - by default, string containing init parameters
+ A unique name for the outage, used for more
+ descriptive error messages.
+
+ """
+ self._specs=specs
+ self._full_month_names=None
+ self._total_available_hours=None
+ self._name=None
+ self._validate()
+
+ def_validate(self):
+"""Validate the input specs."""
+ self._validate_required_keys_exist()
+ self._validate_count()
+ self._validate_and_convert_to_full_name_months()
+ self._validate_duration()
+ self._validate_percentage()
+
+ def_validate_required_keys_exist(self):
+"""Raise an error if any required keys are missing."""
+ missing_keys=[nforninself.REQUIRED_KEYSifnnotinself._specs]
+ ifany(missing_keys):
+ msg=("The following required keys are missing from the Outage "
+ "specification: {}".format(sorted(missing_keys)))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_count(self):
+"""Validate that the total number of outages is an integer. """
+ ifnotisinstance(self.count,int):
+ msg=("Number of outages must be an integer, but got {} for {}"
+ .format(self.count,self.name))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ ifself.count<1:
+ msg=("Number of outages must be greater than 0, but got "
+ "{} for {}".format(self.count,self.name))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_and_convert_to_full_name_months(self):
+"""Validate month input and convert to full month names. """
+ months=convert_to_full_month_names(self._specs['allowed_months'])
+ known_months,unknown_months=filter_unknown_month_names(months)
+
+ ifunknown_months:
+ msg=("The following month names were not understood: {}. Please "
+ "use either the full month name or the standard 3-letter "
+ "month abbreviation. For more info, see the month name "
+ "documentation for the python standard package `calendar`."
+ .format(unknown_months))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+
+ ifnotknown_months:
+ msg=("No known month names were provided! Please use either the "
+ "full month name or the standard 3-letter month "
+ "abbreviation. For more info, see the month name "
+ "documentation for the python standard package `calendar`. "
+ "Received input: {!r}"
+ .format(self._specs['allowed_months']))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ self._full_month_names=list(set(known_months))
+
+ def_validate_duration(self):
+"""Validate that the duration is between 0 and the max total. """
+ ifnotisinstance(self.duration,int):
+ msg=("Duration must be an integer number of hours, "
+ "but got {} for {}".format(self.duration,self.name))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ ifnot1<=self.duration<=self.total_available_hours:
+ msg=("Duration of outage must be between 1 and the total "
+ "available hours based on allowed month input ({} for "
+ "a total hour count of {}), but got {} for {}"
+ .format(self.allowed_months,self.total_available_hours,
+ self.percentage_of_capacity_lost,self.name))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def_validate_percentage(self):
+"""Validate that the percentage is in the range (0, 100]. """
+ ifnot0<self.percentage_of_capacity_lost<=100:
+ msg=("Percentage of farm down during outage must be in the "
+ "range (0, 100], but got {} for {}"
+ .format(self.percentage_of_capacity_lost,self.name))
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+ def__repr__(self):
+ return"Outage({!r})".format(self._specs)
+
+ def__str__(self):
+ ifself._nameisNone:
+ self._name=self._specs.get('name')orself._default_name()
+ returnself._name
+
+ def_default_name(self):
+"""Generate a default name for the outage."""
+ specs=self._specs.copy()
+ specs.update({'allowed_months':self.allowed_months,
+ 'allow_outage_overlap':self.allow_outage_overlap})
+ specs_as_str=", ".join(["{}={}".format(k,v)
+ fork,vinspecs.items()])
+ return"Outage({})".format(specs_as_str)
+
+ @property
+ defcount(self):
+"""int: Total number of times outage should be scheduled."""
+ returnself._specs['count']
+
+ @property
+ defduration(self):
+"""int: Total number of consecutive hours per outage."""
+ returnself._specs['duration']
+
+ @property
+ defpercentage_of_capacity_lost(self):
+"""int | float: Percent of capacity taken down per outage."""
+ returnself._specs['percentage_of_capacity_lost']
+
+ @property
+ defallowed_months(self):
+"""list: Months during which outage can be scheduled."""
+ returnself._full_month_names
+
+ @property
+ defallow_outage_overlap(self):
+"""bool: Indicator for overlap with other outages."""
+ returnself._specs.get('allow_outage_overlap',True)
+
+ @property
+ defname(self):
+"""str: Name of the outage."""
+ returnself._specs.get('name',str(self))
+
+ @property
+ deftotal_available_hours(self):
+"""int: Total number of hours available based on allowed months."""
+ ifself._total_available_hoursisNone:
+ self._total_available_hours=len(
+ hourly_indices_for_months(self.allowed_months))
+ returnself._total_available_hours
+
+
+
[docs]classOutageScheduler:
+"""A scheduler for multiple input outages.
+
+ Given a list of information about different types of desired
+ outages, this class leverages the stochastic scheduling routines of
+ :class:`SingleOutageScheduler` to calculate the total losses due to
+ the input outages on an hourly basis.
+
+ Attributes
+ ----------
+ outages : :obj:`list` of :obj:`Outages <Outage>`
+ The user-provided list of :obj:`Outages <Outage>` containing
+ info about all types of outages to be scheduled.
+ seed : :obj:`int`
+ The seed value used to seed the random generator in order
+ to produce random but reproducible losses. This is useful
+ for ensuring that stochastically scheduled losses vary
+ between different sites (i.e. that randomly scheduled
+ outages in two different location do not match perfectly on
+ an hourly basis).
+ total_losses : :obj:`np.array`
+ An array (of length 8760) containing the per-hour total loss
+ percentage resulting from the stochastically scheduled outages.
+ This array contains only zero values before the
+ :meth:`~OutageScheduler.calculate` method is run.
+ can_schedule_more : :obj:`np.array`
+ A boolean array (of length 8760) indicating wether or not more
+ losses can be scheduled for a given hour. This array keeps track
+ of all the scheduling conflicts between input outages.
+
+ Warnings
+ --------
+ It is possible that not all outages input by the user will be
+ scheduled. This can happen when there is not enough time allowed
+ for all of the input outages. To avoid this issue, always be sure to
+ allow a large enough month range for long outages that take up a big
+ portion of the farm and try to allow outage overlap whenever
+ possible.
+
+ See Also
+ --------
+ :class:`SingleOutageScheduler` : Single outage scheduler.
+ :class:`Outage` : Specifications for a single outage.
+ """
+
+ def__init__(self,outages,seed=0):
+"""
+ Parameters
+ ----------
+ outages : list of :obj:`Outages <Outage>`
+ A list of :obj:`Outages <Outage>`, where each :obj:`Outage`
+ contains info about a single type of outage. See the
+ documentation of :class:`Outage` for a description of the
+ required keys of each outage dictionary.
+ seed : int, optional
+ An integer value used to seed the random generator in order
+ to produce random but reproducible losses. This is useful
+ for ensuring that stochastically scheduled losses vary
+ between different sites (i.e. that randomly scheduled
+ outages in two different location do not match perfectly on
+ an hourly basis). By default, the seed is set to 0.
+ """
+ self.outages=outages
+ self.seed=seed
+ self.total_losses=np.zeros(8760)
+ self.can_schedule_more=np.full(8760,True)
+
+
[docs]defcalculate(self):
+"""Calculate total losses from stochastically scheduled outages.
+
+ This function calls :meth:`SingleOutageScheduler.calculate`
+ on every outage input (sorted by largest duration and then
+ largest number of outages) and returns the aggregate the losses
+ from the result.
+
+ Returns
+ -------
+ :obj:`np.array`
+ An array (of length 8760) containing the per-hour total loss
+ percentage resulting from the stochastically scheduled
+ outages.
+ """
+ sorted_outages=sorted(self.outages,
+ key=lambdao:(o.duration,
+ o.count,
+ o.percentage_of_capacity_lost,
+ sum(sum(map(ord,name))
+ forname
+ ino.allowed_months),
+ o.allow_outage_overlap))
+ foroutageinsorted_outages[::-1]:
+ self.seed+=1
+ SingleOutageScheduler(outage,self).calculate()
+ returnself.total_losses
+
+
+
[docs]classSingleOutageScheduler:
+"""A scheduler for a single outage.
+
+ Given information about a single type of outage, this class
+ facilitates the (randomized) scheduling of all requested instances
+ of the outage. See :meth:`SingleOutageScheduler.calculate` for
+ specific details about the scheduling process.
+
+ Attributes
+ ----------
+ outage : :obj:`Outage`
+ The user-provided :obj:`Outage` containing info about the outage
+ to be scheduled.
+ scheduler : :obj:`OutageScheduler`
+ A scheduler object that keeps track of the total hourly losses
+ from the input outage as well as any other outages it has
+ already scheduled.
+ can_schedule_more : :obj:`np.array`
+ A boolean array (of length 8760) indicating wether or not more
+ losses can be scheduled for a given hour. This is specific
+ to the input outage only.
+
+ Warnings
+ --------
+ It is possible that not all outages input by the user can be
+ scheduled. This can happen when there is not enough time allowed
+ for all of the input outages. To avoid this issue, always be sure to
+ allow a large enough month range for long outages that take up a big
+ portion of the farm and try to allow outage overlap whenever
+ possible.
+
+ See Also
+ --------
+ :class:`OutageScheduler` : Scheduler for multiple outages.
+ :class:`Outage` : Specifications for a single outage.
+ """
+
+ MAX_ITER=10_000
+"""Max number of extra attempts to schedule outages."""
+
+ def__init__(self,outage,scheduler):
+"""
+
+ Parameters
+ ----------
+ outage : Outage
+ An outage object containing info about the outage to be
+ scheduled.
+ scheduler : OutageScheduler
+ A scheduler object that keeps track of the total hourly
+ losses from the input outage as well as any other outages
+ it has already scheduled.
+ """
+ self.outage=outage
+ self.scheduler=scheduler
+ self.can_schedule_more=np.full(8760,False)
+ self._scheduled_outage_inds=[]
+
+
[docs]defcalculate(self):
+"""Calculate losses from stochastically scheduled outages.
+
+ This function attempts to schedule outages according to the
+ specification provided in the :obj:`Outage` input. Specifically,
+ it checks the available hours based on the main
+ :obj:`Scheduler <OutageScheduler>` (which may have other outages
+ already scheduled) and attempts to randomly add new outages with
+ the specified duration and percent of losses. The function
+ terminates when the desired number of outages (specified by
+ :attr:`Outage.count`) have been successfully scheduled, or when
+ the number of attempts exceeds
+ :attr:`~SingleOutageScheduler.MAX_ITER` + :attr:`Outage.count`.
+
+ Warns
+ -----
+ reVLossesWarning
+ If the number of requested outages could not be scheduled.
+ """
+ self.update_when_can_schedule_from_months()
+
+ foriter_indinrange(self.outage.count+self.MAX_ITER):
+ self.update_when_can_schedule()
+ ifnotself.can_schedule_more.any():
+ break
+ seed=self.scheduler.seed+iter_ind
+ outage_slice=self.find_random_outage_slice(seed=seed)
+ ifself.can_schedule_more[outage_slice].all():
+ self.schedule_losses(outage_slice)
+ iflen(self._scheduled_outage_inds)==self.outage.count:
+ break
+
+ iflen(self._scheduled_outage_inds)<self.outage.count:
+ iflen(self._scheduled_outage_inds)==0:
+ msg_start="Could not schedule any requested outages"
+ else:
+ msg_start=("Could only schedule {} out of {} requested "
+ "outages"
+ .format(len(self._scheduled_outage_inds),
+ self.outage.count))
+ msg=("{} after a max of {:,} iterations. You are likely "
+ "attempting to schedule a lot of long outages or a lot "
+ "of short outages with a large percentage of the farm at "
+ "a time. Please adjust the outage specifications and try "
+ "again"
+ .format(msg_start,self.outage.count+self.MAX_ITER))
+ logger.warning(msg)
+ warnings.warn(msg,reVLossesWarning)
+
+
[docs]defupdate_when_can_schedule_from_months(self):
+"""
+ Update :attr:`can_schedule_more` using :attr:`Outage.allowed_months`.
+
+ This function sets the :attr:`can_schedule_more` bool array to
+ `True` for all of the months in :attr:`Outage.allowed_months`.
+ """
+ inds=hourly_indices_for_months(self.outage.allowed_months)
+ self.can_schedule_more[inds]=True
+
+
[docs]defupdate_when_can_schedule(self):
+"""Update :attr:`can_schedule_more` using :obj:`OutageScheduler`.
+
+ This function sets the :attr:`can_schedule_more` bool array to
+ `True` wherever :attr:`OutageScheduler.can_schedule_more` is
+ also `True` and wherever the losses from this outage would not
+ cause the :attr:`OutageScheduler.total_losses` to exceed 100%.
+ """
+ self.can_schedule_more&=self.scheduler.can_schedule_more
+ ifself.outage.allow_outage_overlap:
+ total_new_losses=(self.scheduler.total_losses
+ +self.outage.percentage_of_capacity_lost)
+ losses_will_not_exceed_100=total_new_losses<=100
+ self.can_schedule_more&=losses_will_not_exceed_100
+ else:
+ self.can_schedule_more&=self.scheduler.total_losses==0
+
+
[docs]deffind_random_outage_slice(self,seed=None):
+"""Find a random slot of time for this type of outage.
+
+ This function randomly selects a starting time for this outage
+ given the allowed times in :attr:`can_schedule_more`. It does
+ **not** verify that the outage can be scheduled for the entire
+ requested duration.
+
+ Parameters
+ ----------
+ seed : int, optional
+ Integer used to seed the :func:`np.random.choice` call.
+ If :obj:`None`, seed is not used.
+
+ Returns
+ -------
+ :obj:`slice`
+ A slice corresponding to the random slot of time for this
+ type of outage.
+ """
+ ifseedisnotNone:
+ np.random.seed(seed)
+ outage_ind=np.random.choice(np.where(self.can_schedule_more)[0])
+ returnslice(outage_ind,outage_ind+self.outage.duration)
+
+
[docs]defschedule_losses(self,outage_slice):
+"""Schedule the input outage during the given slice of time.
+
+ Given a slice in the hourly loss array, add the losses from this
+ outage (which is equivalent to scheduling them).
+
+ Parameters
+ ----------
+ outage_slice : slice
+ A slice corresponding to the slot of time to schedule this
+ outage.
+ """
+ self._scheduled_outage_inds.append(outage_slice.start)
+ self.scheduler.total_losses[outage_slice]+=(
+ self.outage.percentage_of_capacity_lost)
+ ifnotself.outage.allow_outage_overlap:
+ self.scheduler.can_schedule_more[outage_slice]=False
+
+
+
[docs]classScheduledLossesMixin:
+"""Mixin class for :class:`reV.SAM.generation.AbstractSamGeneration`.
+
+ Warning
+ -------
+ Using this class for anything except as a mixin for
+ :class:`~reV.SAM.generation.AbstractSamGeneration` may result in
+ unexpected results and/or errors.
+ """
+
+ OUTAGE_CONFIG_KEY='reV_outages'
+"""Specify outage information in the config file using this key."""
+ OUTAGE_SEED_CONFIG_KEY='reV_outages_seed'
+"""Specify a randomizer seed in the config file using this key."""
+
+
[docs]defadd_scheduled_losses(self,resource=None):
+"""Add stochastically scheduled losses to SAM config file.
+
+ This function reads the information in the ``reV_outages`` key
+ of the ``sam_sys_inputs`` dictionary and computes stochastically
+ scheduled losses from that input. If the value for
+ ``reV_outages`` is a string, it must have been generated by
+ calling :func:`json.dumps` on the list of dictionaries
+ containing outage specifications. Otherwise, the outage
+ information is expected to be a list of dictionaries containing
+ outage specifications. See :class:`Outage` for a description of
+ the specifications allowed for each outage. The scheduled losses
+ are passed to SAM via the ``hourly`` key to signify which hourly
+ capacity factors should be adjusted with outage losses. If no
+ outage info is specified in ``sam_sys_inputs``, no scheduled
+ losses are added.
+
+ Parameters
+ ----------
+ resource : pd.DataFrame, optional
+ Time series resource data for a single location with a
+ pandas DatetimeIndex. The ``year`` value of the index will
+ be used to seed the stochastically scheduled losses. If
+ `None`, no yearly seed will be used.
+
+ See Also
+ --------
+ :class:`Outage` : Single outage specification.
+
+ Notes
+ -----
+ The scheduled losses are passed to SAM via the ``hourly`` key to
+ signify which hourly capacity factors should be adjusted with
+ outage losses. If the user specifies other hourly adjustment
+ factors via the ``hourly`` key, the effect is combined. For
+ example, if the user inputs a 33% hourly adjustment factor and
+ reV schedules an outage for 70% of the farm down for the same
+ hour, then the resulting adjustment factor is
+
+ .. math: 1 - [(1 - 70/100) * (1 - 33/100)] = 0.799
+
+ This means the generation will be reduced by ~80%, because the
+ user requested 33% losses for the 30% the farm that remained
+ operational during the scheduled outage (i.e. 20% remaining of
+ the original generation).
+ """
+
+ outages=self._user_outage_input()
+ ifnotoutages:
+ return
+
+ self._set_base_seed(resource)
+
+ logger.debug("Adding the following stochastically scheduled outages: "
+ "{}".format(outages))
+ logger.debug("Scheduled outages seed: {}".format(self.outage_seed))
+
+ scheduler=OutageScheduler(outages,seed=self.outage_seed)
+ hourly_outages=scheduler.calculate()
+ self._add_outages_to_sam_inputs(hourly_outages)
+
+ logger.debug("Hourly adjustment factors after scheduled outages: {}"
+ .format(list(self.sam_sys_inputs['hourly'])))
+
+ def_user_outage_input(self):
+"""Get outage and seed info from config. """
+ outage_specs=self.sam_sys_inputs.pop(self.OUTAGE_CONFIG_KEY,None)
+ ifoutage_specsisNone:
+ return
+
+ # site-specific info is input as str
+ ifisinstance(outage_specs,str):
+ outage_specs=json.loads(outage_specs)
+
+ outages=[Outage(spec)forspecinoutage_specs]
+ returnoutages
+
+ def_set_base_seed(self,resource):
+"""Set the base seed base don user input. """
+ self.__base_seed=0
+ ifresourceisnotNone:
+ self.__base_seed+=int(resource.index.year.values[0])
+ self.__base_seed+=self.sam_sys_inputs.pop(
+ self.OUTAGE_SEED_CONFIG_KEY,0)
+
+ def_add_outages_to_sam_inputs(self,outages):
+"""Add the hourly adjustment factors to config, checking user input."""
+
+ hourly_mult=1-outages/100
+
+ user_hourly_input=self.sam_sys_inputs.pop('hourly',[0]*8760)
+ user_hourly_mult=1-np.array(user_hourly_input)/100
+
+ final_hourly_mult=hourly_mult*user_hourly_mult
+ self.sam_sys_inputs['hourly']=(1-final_hourly_mult)*100
+
+ @property
+ defoutage_seed(self):
+"""int: A value to use as the seed for the outage losses. """
+ # numpy seeds must be between 0 and 2**32 - 1
+ returnself._seed_from_inputs()%2**32
+
+ def_seed_from_inputs(self):
+"""Get seed value from inputs. """
+ try:
+ returnint(self.meta.name)+self.__base_seed
+ except(AttributeError,TypeError,ValueError):
+ pass
+
+ try:
+ returnhash(tuple(self.meta))+self.__base_seed
+ except(AttributeError,TypeError):
+ pass
+
+ returnself.__base_seed
+# -*- coding: utf-8 -*-
+"""reV-losses utilities.
+
+"""
+importcalendar
+importlogging
+
+importnumpyasnp
+
+fromreV.utilities.exceptionsimportreVLossesValueError
+
+logger=logging.getLogger(__name__)
+
+# 1900 is just a representative year, since a year input is required
+DAYS_PER_MONTH=[calendar.monthrange(1900,i)[1]foriinrange(1,13)]
+FIRST_DAY_INDEX_OF_MONTH=np.cumsum([0]+DAYS_PER_MONTH[:-1])
+
+
+
[docs]defconvert_to_full_month_names(month_names):
+"""Format an iterable of month names to match those in :mod:`calendar`.
+
+ This function will format each input name to match the formatting
+ in :obj:`calendar.month_name` (upper case, no extra whitespace), and
+ it will convert all abbreviations to full month names. No other
+ assumptions are made about the inputs, so an input string " abc "
+ will get formatted and passed though as "Abc".
+
+ Parameters
+ ----------
+ month_names : iter
+ An iterable of strings representing the input month names.
+ Month names can be unformatted and contain 3-letter month
+ abbreviations.
+
+ Returns
+ -------
+ :obj:`list`
+ A list of month names matching the formatting of
+ :obj:`calendar.month_name` (upper case, no extra whitespace).
+ Abbreviations are also converted to a full month name.
+
+ Examples
+ --------
+ >>> input_names = ['March', ' aprIl ', 'Jun', 'jul', ' abc ']
+ >>> convert_to_full_month_names(input_names)
+ ['March', 'April', 'June', 'July', 'Abc']
+ """
+ formatted_names=[]
+ fornameinmonth_names:
+ month_name=format_month_name(name)
+ month_name=full_month_name_from_abbr(month_name)ormonth_name
+ formatted_names.append(month_name)
+ returnformatted_names
+
+
+
[docs]deffilter_unknown_month_names(month_names):
+"""Split the input into known and unknown month names.
+
+ Parameters
+ ----------
+ month_names : iter
+ An iterable of strings representing the input month names. Month
+ names must match the formatting in :obj:`calendar.month_name`
+ (upper case, no extra whitespace), otherwise they will be placed
+ into the ``unknown_months`` return list.
+
+ Returns
+ -------
+ known_months : :obj:`list`
+ List of known month names.
+ unknown_months : :obj:`list`
+ List of unknown month names.
+ """
+ known_months,unknown_months=[],[]
+ fornameinmonth_names:
+ ifnameincalendar.month_name:
+ known_months.append(name)
+ else:
+ unknown_months.append(name)
+
+ returnknown_months,unknown_months
+
+
+
[docs]defhourly_indices_for_months(month_names):
+"""Convert month names into a list of hourly indices.
+
+ Given a list of month names, this function will return a list
+ of indices such that any index value corresponds to an hour within
+ the input months.
+
+ Parameters
+ ----------
+ month_names : iter
+ An iterable of month names for the desired starting indices.
+ The month names must match the formatting in
+ :obj:`calendar.month_name` (upper case, no extra whitespace),
+ otherwise their hourly indices will not be included in the
+ output.
+
+ Returns
+ -------
+ :obj:`list`
+ A list of hourly index values such that any index corresponds to
+ an hour within the input months.
+ """
+
+ indices=[]
+ forindinsorted(month_indices(month_names)):
+ start_index=FIRST_DAY_INDEX_OF_MONTH[ind]*24
+ hours_in_month=DAYS_PER_MONTH[ind]*24
+ indices+=list(range(start_index,start_index+hours_in_month))
+
+ returnindices
+
+
+
[docs]defmonth_indices(month_names):
+"""Convert input month names to an indices (0-11) of the months.
+
+ Parameters
+ ----------
+ month_names : iter
+ An iterable of month names for the desired starting indices.
+ The month names must match the formatting in
+ :obj:`calendar.month_name` (upper case, no extra whitespace),
+ otherwise their index will not be included in the output.
+
+ Returns
+ -------
+ :obj:`set`
+ A set of month indices for the input month names. Unknown
+ month indices (-1) are removed.
+ """
+ return{month_index(name)fornameinmonth_names}-{-1}
+
+
+
[docs]defmonth_index(month_name):
+"""Convert a month name (as string) to an index (0-11) of the month.
+
+ Parameters
+ ----------
+ month_name : str
+ Name of month to corresponding to desired index. This input
+ must match the formatting in :obj:`calendar.month_name`
+ (upper case, no extra whitespace).
+
+ Returns
+ -------
+ :obj:`int`
+ The 0-index of the month, or -1 if the month name is not
+ understood.
+
+ Examples
+ --------
+ >>> month_index("June")
+ 5
+ >>> month_index("July")
+ 6
+ >>> month_index("Jun")
+ -1
+ >>> month_index("july")
+ -1
+ """
+ formonth_indinrange(12):
+ ifcalendar.month_name[month_ind+1]==month_name:
+ returnmonth_ind
+
+ return-1
+
+
+
[docs]defformat_month_name(month_name):
+"""Format a month name to match the names in the :mod:`calendar` module.
+
+ In particular, any extra spaces at the beginning or end of the
+ string are stripped, and the name is converted to a title (first
+ letter is uppercase).
+
+ Parameters
+ ----------
+ month_name : str
+ Name of month.
+
+ Returns
+ -------
+ :obj:`str`
+ Name of month, formatted to match the month names in the
+ :mod:`calendar` module.
+
+ Examples
+ --------
+ >>> format_month_name("June")
+ "June"
+ >>> format_month_name("aprIl")
+ "April"
+ >>> format_month_name(" aug ")
+ "Aug"
+ """
+ returnmonth_name.strip().title()
+
+
+
[docs]deffull_month_name_from_abbr(month_name):
+"""Convert a month abbreviation to a full month name.
+
+ Parameters
+ ----------
+ month_name : str
+ Abbreviated month name. Must be one of:
+
+ - "Jan"
+ - "Feb"
+ - "Mar"
+ - "Apr"
+ - "May"
+ - "Jun"
+ - "Jul"
+ - "Aug"
+ - "Sep"
+ - "Oct"
+ - "Nov"
+ - "Dec"
+
+ If the input does not match one of these, this function returns
+ :obj:`None`.
+
+
+ Returns
+ -------
+ :obj:`str` | :obj:`None`
+ Unabbreviated month name, or :obj:`None` if input abbreviation
+ is not understood.
+
+ Examples
+ --------
+ >>> full_month_name_from_abbr("Jun")
+ "June"
+ >>> full_month_name_from_abbr("June") is None
+ True
+ >>> full_month_name_from_abbr('Abcdef') is None
+ True
+ """
+ formonth_indexinrange(1,13):
+ ifcalendar.month_abbr[month_index]==month_name:
+ returncalendar.month_name[month_index]
+
+
+def_validate_arrays_not_empty(obj,array_names=None):
+"""Validate that the input data arrays are not empty. """
+ array_names=array_namesor[]
+ fornameinarray_names:
+ try:
+ arr=getattr(obj,name)
+ exceptAttributeError:
+ continue
+ ifnotarr.size:
+ msg="Invalid {} input: Array is empty! - {}"
+ msg=msg.format(name.replace('_',' '),arr)
+ logger.error(msg)
+ raisereVLossesValueError(msg)
+
+# -*- coding: utf-8 -*-
+# pylint: disable=no-member
+"""
+reV-NRWAL analysis module.
+
+This module runs reV data through the NRWAL compute library. This code was
+first developed to use a custom offshore wind LCOE equation library but has
+since been refactored to analyze any equation library in NRWAL.
+
+Everything in this module operates on the spatiotemporal resolution of the reV
+generation output file. This is usually the wind or solar resource resolution
+but could be the supply curve resolution after representative profiles is run.
+"""
+importlogging
+fromwarningsimportwarn
+
+importnumpyasnp
+importpandasaspd
+
+fromreV.generation.generationimportGen
+fromreV.handlers.outputsimportOutputs
+fromreV.utilitiesimportSiteDataField,ResourceMetaField,log_versions
+fromreV.utilities.exceptionsimport(
+ DataShapeError,
+ OffshoreWindInputError,
+ OffshoreWindInputWarning,
+)
+
+logger=logging.getLogger(__name__)
+
+
+
[docs]classRevNrwal:
+"""RevNrwal"""
+
+ DEFAULT_META_COLS=(SiteDataField.CONFIG,)
+"""Columns from the `site_data` table to join to the output meta data"""
+
+ def__init__(self,gen_fpath,site_data,sam_files,nrwal_configs,
+ output_request,save_raw=True,
+ meta_gid_col=str(ResourceMetaField.GID),# str() to fix docs
+ site_meta_cols=None):
+"""Framework to handle reV-NRWAL analysis.
+
+ ``reV`` NRWAL analysis runs ``reV`` data through the NRWAL
+ compute library. Everything in this module operates on the
+ spatiotemporal resolution of the ``reV`` generation output file
+ (usually the wind or solar resource resolution but could also be
+ the supply curve resolution after representative profiles is
+ run).
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Full filepath to HDF5 file with ``reV`` generation or
+ rep_profiles output. Anything in the `output_request` input
+ is added to and/or manipulated within this file.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ input can also be ``"PIPELINE"`` to parse the output of
+ one of the previous step and use it as input to this call.
+ However, note that duplicate executions of ``reV``
+ commands prior to this one within the pipeline may
+ invalidate this parsing, meaning the `gen_fpath` input
+ will have to be specified manually.
+
+ site_data : str | pd.DataFrame
+ Site-specific input data for NRWAL calculation.If this input
+ is a string, it should be a path that points to a CSV file.
+ Otherwise, this input should be a DataFrame with
+ pre-extracted site data. Rows in this table should match
+ the `meta_gid_col` in the `gen_fpath` meta data input
+ sites via a ``gid`` column. A ``config`` column must also be
+ provided that corresponds to the `nrwal_configs` input. Only
+ sites with a gid in this file's ``gid`` column will be run
+ through NRWAL.
+ sam_files : dict | str
+ A dictionary mapping SAM input configuration ID(s) to SAM
+ configuration(s). Keys are the SAM config ID(s) which
+ correspond to the keys in the `nrwal_configs` input. Values
+ for each key are either a path to a corresponding SAM
+ config file or a full dictionary of SAM config inputs. For
+ example::
+
+ sam_files = {
+ "default": "/path/to/default/sam.json",
+ "onshore": "/path/to/onshore/sam_config.yaml",
+ "offshore": {
+ "sam_key_1": "sam_value_1",
+ "sam_key_2": "sam_value_2",
+ ...
+ },
+ ...
+ }
+
+ This input can also be a string pointing to a single SAM
+ config file. In this case, the ``config`` column of the
+ CSV points input should be set to ``None`` or left out
+ completely. See the documentation for the ``reV`` SAM class
+ (e.g. :class:`reV.SAM.generation.WindPower`,
+ :class:`reV.SAM.generation.PvWattsv8`,
+ :class:`reV.SAM.generation.Geothermal`, etc.) for
+ documentation on the allowed and/or required SAM config file
+ inputs.
+ nrwal_configs : dict
+ A dictionary mapping SAM input configuration ID(s) to NRWAL
+ configuration(s). Keys are the SAM config ID(s) which
+ correspond to the keys in the `sam_files` input. Values
+ for each key are either a path to a corresponding NRWAL YAML
+ or JSON config file or a full dictionary of NRWAL config
+ inputs. For example::
+
+ nrwal_configs = {
+ "default": "/path/to/default/nrwal.json",
+ "onshore": "/path/to/onshore/nrwal_config.yaml",
+ "offshore": {
+ "nrwal_key_1": "nrwal_value_1",
+ "nrwal_key_2": "nrwal_value_2",
+ ...
+ },
+ ...
+ }
+
+ output_request : list | tuple
+ List of output dataset names to be written to the
+ `gen_fpath` file. Any key from the NRWAL configs or any of
+ the inputs (site_data or sam_files) is available to be
+ exported as an output dataset. If you want to manipulate a
+ dset like ``cf_mean`` from `gen_fpath` and include it in the
+ `output_request`, you should set ``save_raw=True`` and then
+ use ``cf_mean_raw`` in the NRWAL equations as the input.
+ This allows you to define an equation in the NRWAL configs
+ for a manipulated ``cf_mean`` output that can be included in
+ the `output_request` list.
+ save_raw : bool, optional
+ Flag to save an initial ("raw") copy of input datasets from
+ `gen_fpath` that are also part of the `output_request`. For
+ example, if you request ``cf_mean`` in output_request but
+ also manipulate the ``cf_mean`` dataset in the NRWAL
+ equations, the original ``cf_mean`` will be archived under
+ the ``cf_mean_raw`` dataset in `gen_fpath`.
+ By default, ``True``.
+ meta_gid_col : str, optional
+ Column label in the source meta data from `gen_fpath` that
+ contains the unique gid identifier. This will be joined to
+ the site_data ``gid`` column. By default, ``"gid"``.
+ site_meta_cols : list | tuple, optional
+ Column labels from `site_data` to be added to the meta data
+ table in `gen_fpath`. If ``None``, only the columns in
+ :attr:`DEFAULT_META_COLS` will be added. Any columns
+ requested via this input will be considered *in addition to*
+ the :attr:`DEFAULT_META_COLS`. By default, ``None``.
+ """
+
+ log_versions(logger)
+
+ # delayed NRWAL import to cause less errors with old reV installs
+ # if not running nrwal.
+ fromNRWALimportNrwalConfig
+
+ self._meta_gid_col=meta_gid_col
+ self._gen_fpath=gen_fpath
+ self._site_data=site_data
+ self._output_request=output_request
+ self._meta_out=None
+ self._time_index=None
+ self._save_raw=save_raw
+ self._nrwal_inputs=self._out=None
+
+ self._nrwal_configs={
+ k:NrwalConfig(v)fork,vinnrwal_configs.items()
+ }
+
+ self._site_meta_cols=site_meta_cols
+ ifself._site_meta_colsisNone:
+ self._site_meta_cols=list(self.DEFAULT_META_COLS)
+ else:
+ self._site_meta_cols=list(self._site_meta_cols)
+ self._site_meta_cols+=list(self.DEFAULT_META_COLS)
+ self._site_meta_cols=list(set(self._site_meta_cols))
+
+ self._site_data=self._parse_site_data()
+ self._meta_source=self._parse_gen_data()
+ self._analysis_gids,self._site_data=self._parse_analysis_gids()
+
+ pc=Gen.get_pc(
+ self._site_data[[SiteDataField.GID,SiteDataField.CONFIG]],
+ points_range=None,sam_configs=sam_files,tech='windpower')
+ self._project_points=pc.project_points
+
+ self._sam_sys_inputs=self._parse_sam_sys_inputs()
+ meta_gids=self.meta_source[self._meta_gid_col].values
+ logger.info(
+ 'Finished initializing NRWAL analysis module for "{}" '
+ "{} through {} with {} total generation points and "
+ "{} NRWAL analysis points.".format(
+ self._meta_gid_col,
+ meta_gids.min(),
+ meta_gids.max(),
+ len(self.meta_source),
+ len(self.analysis_gids),
+ )
+ )
+
+ def_parse_site_data(self,required_columns=(SiteDataField.GID,
+ SiteDataField.CONFIG)):
+"""Parse the site-specific spatial input data file
+
+ Parameters
+ ----------
+ required_columns : tuple | list
+ List of column names that must be in the site_data in
+ order to run the reV NRWAL module.
+
+ Returns
+ -------
+ site_data : pd.DataFrame
+ Dataframe of extracted site_data. Each row is an analysis point and
+ columns are spatial data inputs.
+ """
+
+ ifisinstance(self._site_data,str):
+ self._site_data=pd.read_csv(self._site_data)
+
+ if"dist_l_to_ts"inself._site_data:
+ ifself._site_data["dist_l_to_ts"].sum()>0:
+ w=(
+ 'Possible incorrect Offshore data input! "dist_l_to_ts" '
+ "(distance land to transmission) input is non-zero. "
+ "Most reV runs set this to zero and input the cost "
+ "of transmission from landfall tie-in to "
+ "transmission feature in the supply curve module."
+ )
+ logger.warning(w)
+ warn(w,OffshoreWindInputWarning)
+
+ forcinrequired_columns:
+ ifcnotinself._site_data:
+ msg='Did not find required "{}" column in site_data!'.format(
+ c
+ )
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ self._site_data=self._site_data.sort_values(SiteDataField.GID)
+
+ returnself._site_data
+
+ def_parse_gen_data(self):
+"""Parse generation data and get meta data
+
+ Returns
+ -------
+ meta : pd.DataFrame
+ Full meta data from gen_fpath.
+ """
+
+ withOutputs(self._gen_fpath,mode="r")asout:
+ meta=out.meta
+
+ msg=(
+ 'Could not find "{}" column in source generation h5 file '
+ "meta data! Available cols: {}".format(
+ self._meta_gid_col,meta.columns.values.tolist()
+ )
+ )
+ assertself._meta_gid_colinmeta,msg
+
+ # currently an assumption of sorted gids in the reV gen output
+ msg="Source capacity factor meta data is not ordered!"
+ meta_gids=list(meta[self._meta_gid_col])
+ assertmeta_gids==sorted(meta_gids),msg
+
+ returnmeta
+
+ def_parse_analysis_gids(self):
+"""Check the intersection of the generation gids and the site_data
+ input gids.
+
+ Returns
+ -------
+ analysis_gids : np.ndarray
+ Array indicating which sites in the source meta data to process
+ with NRWAL. This is the intersection of the gids in the generation
+ meta data and the gids in the site_data input.
+ site_data : pd.DataFrame
+ The site_data table reduced to only those gids that are in the
+ analysis_gids
+ """
+
+ meta_gids=self.meta_source[self._meta_gid_col].values
+
+ missing=~np.isin(meta_gids,self._site_data[SiteDataField.GID])
+ ifany(missing):
+ msg=(
+ "{} sites from the generation meta data input were "
+ 'missing from the "site_data" input and will not be '
+ "run through NRWAL: {}".format(
+ missing.sum(),meta_gids[missing]
+ )
+ )
+ logger.info(msg)
+
+ missing=~np.isin(self._site_data[SiteDataField.GID],meta_gids)
+ ifany(missing):
+ missing=self._site_data[SiteDataField.GID].values[missing]
+ msg=('{} sites from the "site_data" input were missing from the '
+ 'generation meta data and will not be run through NRWAL: {}'
+ .format(len(missing),missing))
+ logger.info(msg)
+
+ analysis_gids=(set(meta_gids)
+ &set(self._site_data[SiteDataField.GID]))
+ analysis_gids=np.array(sorted(list(analysis_gids)))
+
+ # reduce the site data table to only those sites being analyzed
+ mask=np.isin(self._site_data[SiteDataField.GID],meta_gids)
+ self._site_data=self._site_data[mask]
+
+ returnanalysis_gids,self._site_data
+
+ def_parse_sam_sys_inputs(self):
+"""Get the SAM system inputs dict from project points.
+
+ Returns
+ -------
+ system_inputs : pd.DataFrame
+ DataFrame of SAM config inputs (columns) for every active nrwal
+ analysis gid (row). Index is resource gids and there is also a
+ column "gid" with the copied gids.
+ """
+
+ system_inputs={}
+
+ forgidinself.analysis_gids:
+ system_inputs[gid]=self._project_points[gid][1]
+
+ system_inputs=pd.DataFrame(system_inputs).T
+ system_inputs=system_inputs.sort_index()
+ system_inputs[SiteDataField.GID]=system_inputs.index.values
+ system_inputs.index.name=SiteDataField.GID
+ mask=system_inputs[SiteDataField.GID].isin(self.analysis_gids)
+ system_inputs=system_inputs[mask]
+
+ returnsystem_inputs
+
+ def_init_outputs(self):
+"""Initialize a dictionary of outputs with dataset names as keys and
+ numpy arrays as values. All datasets are initialized as 1D arrays and
+ must be overwritten if found to be 2D. Only active analysis sites will
+ have data in the output, sites that were not found in the site_data
+ "gid" column will not have data in these output arrays
+
+ Returns
+ -------
+ out : dict
+ Dictionary of output data
+ """
+ out={}
+
+ forkeyinself._output_request:
+ out[key]=np.full(
+ len(self.analysis_gids),np.nan,dtype=np.float32
+ )
+
+ ifkeyinself.gen_dsetsandnotself._save_raw:
+ msg=(
+ 'Output request "{0}" was also found in '
+ "the source gen file but save_raw=False! If "
+ "you are manipulating this "
+ "dset, make sure you set save_raw=False "
+ 'and reference "{0}_raw" as the '
+ 'input in the NRWAL equations and then define "{0}" '
+ "as the final manipulated dataset.".format(key)
+ )
+ logger.warning(msg)
+ warn(msg)
+ elifkeyinself.gen_dsets:
+ msg=(
+ 'Output request "{0}" was also found in '
+ "the source gen file. If you are manipulating this "
+ 'dset, make sure you reference "{0}_raw" as the '
+ 'input in the NRWAL equations and then define "{0}" '
+ "as the final manipulated dataset.".format(key)
+ )
+ logger.info(msg)
+
+ ifkeyinself._nrwal_inputs:
+ out[key]=self._nrwal_inputs[key]
+
+ returnout
+
+ def_preflight_checks(self):
+"""Run some preflight checks on the offshore inputs"""
+ sam_files={
+ k:v
+ fork,vinself._project_points.sam_inputs.items()
+ ifkinself._nrwal_configs
+ }
+
+ forcid,sys_ininsam_files.items():
+ loss1=sys_in.get("wind_farm_losses_percent",0)
+ loss2=sys_in.get("turb_generic_loss",0)
+ ifloss1!=0orloss2!=0:
+ msg=(
+ 'Wind farm loss for config "{}" is not 0. When using '
+ "NRWAL for offshore analysis, consider using gross "
+ "capacity factors from reV generation and applying "
+ "spatially dependent losses from the NRWAL equations"
+ .format(cid)
+ )
+ logger.info(msg)
+
+ available_ids=list(self._nrwal_configs.keys())
+ requested_ids=list(self._site_data[SiteDataField.CONFIG].values)
+ missing=set(requested_ids)-set(available_ids)
+ ifany(missing):
+ msg=(
+ "The following config ids were requested in the offshore "
+ "data input but were not available in the NRWAL config "
+ "input dict: {}".format(missing)
+ )
+ logger.error(msg)
+ raiseOffshoreWindInputError(msg)
+
+ check_gid_order=(self._site_data[SiteDataField.GID].values
+ ==self._sam_sys_inputs[SiteDataField.GID].values)
+ msg='NRWAL site_data and system input dataframe had bad order'
+ assert(check_gid_order).all(),msg
+
+ missing=[cforcinself._site_meta_colsifcnotinself._site_data]
+ ifany(missing):
+ msg=(
+ "Could not find requested NRWAL site data pass through "
+ "columns in offshore input data: {}".format(missing)
+ )
+ logger.error(msg)
+ raiseOffshoreWindInputError(msg)
+
+ def_get_input_data(self):
+"""Get all the input data from the site_data, SAM system configs, and
+ generation h5 file, formatted together in one dictionary for NRWAL.
+
+ Returns
+ -------
+ nrwal_inputs : dict
+ Dictionary mapping required NRWAL input variable names (keys) to 1
+ or 2D arrays of inputs for all the analysis_gids
+ """
+
+ logger.info("Setting up input data for NRWAL...")
+
+ # preconditions for this to work properly
+ assertlen(self._site_data)==len(self.analysis_gids)
+ assertlen(self._sam_sys_inputs)==len(self.analysis_gids)
+
+ all_required=[]
+ forconfig_id,nrwal_configinself._nrwal_configs.items():
+ all_required+=list(nrwal_config.required_inputs)
+ all_required=list(set(all_required))
+
+ missing_vars=[
+ var
+ forvarinnrwal_config.required_inputs
+ ifvarnotinself._site_data
+ andvarnotinself.meta_source
+ andvarnotinself._sam_sys_inputs
+ andvarnotinself.gen_dsets
+ ]
+
+ ifany(missing_vars):
+ msg=(
+ "Could not find required input variables {} "
+ 'for NRWAL config "{}" in either the offshore '
+ "data or the SAM system data!".format(
+ missing_vars,config_id
+ )
+ )
+ logger.error(msg)
+ raiseOffshoreWindInputError(msg)
+
+ meta_data_vars=[
+ varforvarinall_requiredifvarinself.meta_source
+ ]
+ logger.info(
+ "Pulling the following inputs from the gen meta data: {}".format(
+ meta_data_vars
+ )
+ )
+ nrwal_inputs={
+ var:self.meta_source[var].values[self.analysis_mask]
+ forvarinmeta_data_vars
+ }
+
+ site_data_vars=[varforvarinall_required
+ ifvarinself._site_data
+ andvarnotinnrwal_inputs]
+ site_data_vars.append(SiteDataField.CONFIG)
+ logger.info('Pulling the following inputs from the site_data input: {}'
+ .format(site_data_vars))
+ forvarinsite_data_vars:
+ nrwal_inputs[var]=self._site_data[var].values
+
+ sam_sys_vars=[
+ var
+ forvarinall_required
+ ifvarinself._sam_sys_inputsandvarnotinnrwal_inputs
+ ]
+ logger.info(
+ "Pulling the following inputs from the SAM system "
+ "configs: {}".format(sam_sys_vars)
+ )
+ forvarinsam_sys_vars:
+ nrwal_inputs[var]=self._sam_sys_inputs[var].values
+
+ gen_vars=[
+ var
+ forvarinall_required
+ ifvarinself.gen_dsetsandvarnotinnrwal_inputs
+ ]
+ logger.info(
+ "Pulling the following inputs from the generation "
+ "h5 file: {}".format(gen_vars)
+ )
+ withOutputs(self._gen_fpath,mode="r")asf:
+ source_gids=self.meta_source[self._meta_gid_col]
+ gen_gids=np.where(source_gids.isin(self.analysis_gids))[0]
+ forvaringen_vars:
+ shape=f.shapes[var]
+ iflen(shape)==1:
+ nrwal_inputs[var]=f[var,gen_gids]
+ eliflen(shape)==2:
+ nrwal_inputs[var]=f[var,:,gen_gids]
+ else:
+ msg=(
+ 'Data shape for "{}" must be 1 or 2D but '
+ "received: {}".format(var,shape)
+ )
+ logger.error(msg)
+ raiseDataShapeError(msg)
+
+ logger.info("Finished setting up input data for NRWAL!")
+
+ returnnrwal_inputs
+
+ @property
+ deftime_index(self):
+"""Get the source time index."""
+ ifself._time_indexisNone:
+ withOutputs(self._gen_fpath,mode="r")asout:
+ self._time_index=out.time_index
+
+ returnself._time_index
+
+ @property
+ defgen_dsets(self):
+"""Get the available datasets from the gen source file"""
+ withOutputs(self._gen_fpath,mode="r")asout:
+ dsets=out.dsets
+
+ returndsets
+
+ @property
+ defmeta_source(self):
+"""Get the full meta data (onshore + offshore)"""
+ returnself._meta_source
+
+ @property
+ defmeta_out(self):
+"""Get the combined onshore and offshore meta data."""
+ ifself._meta_outisNone:
+ self._meta_out=self._meta_source.copy()
+ forcolinself._site_meta_cols:
+ data=self._nrwal_inputs[col]
+ self._meta_out.loc[self.analysis_mask,col]=data
+
+ returnself._meta_out
+
+ @property
+ defanalysis_mask(self):
+"""Get a boolean array to mask the source generation meta data where
+ True is sites that are to be analyzed by NRWAL.
+
+ Returns
+ -------
+ np.ndarray
+ """
+ mask=np.isin(
+ self.meta_source[self._meta_gid_col],self.analysis_gids
+ )
+ returnmask
+
+ @property
+ defanalysis_gids(self):
+"""Get an array of gids from the source generation meta data that are
+ to-be analyzed by nrwal.
+
+ Returns
+ -------
+ np.ndarray
+ """
+ returnself._analysis_gids
+
+ @property
+ defoutputs(self):
+"""Get a dict of NRWAL outputs. Only active analysis sites will have
+ data in the output, sites that were not found in the site_data "gid"
+ column will not have data in these output arrays"""
+ returnself._out
+
+ def_save_nrwal_out(self,name,nrwal_out,output_mask):
+"""Save a dataset from the nrwal_out dictionary to the self._out
+ attribute
+
+ Parameters
+ ----------
+ name : str
+ Dataset name of the nrwal output to be saved.
+ nrwal_out : dict
+ Output dictionary from a successfully evaluated NrwalConfig object
+ containing the dataset with the input name
+ output_mask : np.ndarray
+ Boolean array showing which gids in self.analysis_gids should be
+ assigned data from this NRWAL output. If not all true, there are
+ probably multiple NrwalConfig objects that map to different sets of
+ gids.
+ """
+ value=nrwal_out[name]
+ value=self._value_to_array(value,name)
+
+ iflen(value.shape)==1:
+ self._out[name][output_mask]=value[output_mask]
+
+ eliflen(value.shape)==2:
+ iflen(self._out[name].shape)==1:
+ ifnotall(np.isnan(self._out[name])):
+ msg=(
+ 'Output dataset "{}" was initialized as 1D but was '
+ "later found to be 2D but was not all NaN!".format(
+ name
+ )
+ )
+ logger.error(msg)
+ raiseDataShapeError(msg)
+
+ # re-initialize the dataset as 2D now that we
+ # know what the output looks like
+ out_shape=(len(self.time_index),len(self.analysis_gids))
+ self._out[name]=np.full(out_shape,np.nan,dtype=np.float32)
+
+ self._out[name][:,output_mask]=value[:,output_mask]
+
+ else:
+ msg=(
+ 'Could not make sense of NRWAL output "{}" '
+ "with shape {}".format(name,value.shape)
+ )
+ logger.error(msg)
+ raiseDataShapeError(msg)
+
+ def_save_nrwal_misc(self,name,nrwal_config,output_mask):
+"""Save miscellaneous output requests from a NRWAL config object (not
+ NRWAL output dictionary) to the self._out attribute.
+
+ Parameters
+ ----------
+ name : str
+ Dataset name of the nrwal output to be saved.
+ nrwal_config : NrwalConfig
+ NrwalConfig object containing NRWAL Equation objects or something
+ else that is to be exported to the outputs
+ output_mask : np.ndarray
+ Boolean array showing which gids in self.analysis_gids should be
+ assigned data from this NRWAL output. If not all true, there are
+ probably multiple NrwalConfig objects that map to different sets of
+ gids.
+ """
+
+ fromNRWALimportEquation
+
+ value=nrwal_config[name]
+
+ ifisinstance(value,Equation):
+ msg=(
+ 'Cannot retrieve Equation "{}" from NRWAL. '
+ "Must be a number!".format(name)
+ )
+ assertnotany(value.variables),msg
+ value=value.eval()
+
+ value=self._value_to_array(value,name)
+ self._out[name][output_mask]=value[output_mask]
+
+ def_value_to_array(self,value,name):
+"""Turn the input into numpy array if it isn't already."""
+ ifnp.issubdtype(type(value),np.number):
+ value*=np.ones(len(self.analysis_gids),dtype=np.float32)
+
+ ifnotisinstance(value,np.ndarray):
+ msg=(
+ 'NRWAL key "{}" returned bad type of "{}", needs to be '
+ "numeric or an output array.".format(name,type(value))
+ )
+ logger.error(msg)
+ raiseTypeError(msg)
+ returnvalue
+
+
[docs]defrun_nrwal(self):
+"""Run analysis via the NRWAL analysis library"""
+
+ self._preflight_checks()
+ self.save_raw_dsets()
+ self._nrwal_inputs=self._get_input_data()
+ self._out=self._init_outputs()
+
+ fori,(cid,nrwal_config)inenumerate(self._nrwal_configs.items()):
+ output_mask=self._site_data[SiteDataField.CONFIG].values==cid
+ logger.info('Running NRWAL config {} of {}: "{}" and applying '
+ 'to {} out of {} total sites'
+ .format(i+1,len(self._nrwal_configs),cid,
+ output_mask.sum(),len(output_mask)))
+
+ nrwal_out=nrwal_config.eval(inputs=self._nrwal_inputs)
+
+ # pylint: disable=C0201
+ fornameinself._out.keys():
+ ifnameinnrwal_out:
+ self._save_nrwal_out(name,nrwal_out,output_mask)
+
+ elifnameinnrwal_config.keys():
+ self._save_nrwal_misc(name,nrwal_config,output_mask)
+
+ elifnamenotinself._nrwal_inputs:
+ msg=(
+ 'Could not find "{}" in the output dict of NRWAL '
+ "config {}".format(name,cid)
+ )
+ logger.warning(msg)
+ warn(msg)
+
+
[docs]defcheck_outputs(self):
+"""Check the nrwal outputs for nan values and raise errors if found."""
+ forname,arrinself._out.items():
+ ifnp.isnan(arr).all():
+ msg=(
+ 'Output array "{}" is all NaN! Probably was not '
+ "found in the available NRWAL keys.".format(name)
+ )
+ logger.warning(msg)
+ warn(msg)
+ elifnp.isnan(arr).any():
+ mask=np.isnan(arr)
+ nan_meta=self.meta_source[self.analysis_mask][mask]
+ nan_gids=nan_meta[self._meta_gid_col].values
+ msg=(
+ "NaN values ({} out of {}) persist in NRWAL "
+ 'output "{}"!'.format(np.isnan(arr).sum(),len(arr),name)
+ )
+ logger.warning(msg)
+ logger.warning(
+ "This is the NRWAL meta that is causing NaN "
+ "outputs: {}".format(nan_meta)
+ )
+ logger.warning(
+ "These are the resource gids causing NaN "
+ "outputs: {}".format(nan_gids)
+ )
+ warn(msg)
+
+
[docs]defsave_raw_dsets(self):
+"""If requested by save_raw=True, archive raw datasets that exist in
+ the gen_fpath file and are also requested in the output_request"""
+ ifself._save_raw:
+ withOutputs(self._gen_fpath,"a")asf:
+ fordsetinself._output_request:
+ dset_raw="{}_raw".format(dset)
+ ifdsetinfanddset_rawnotinf:
+ logger.info(
+ 'Saving raw data from "{}" to "{}"'.format(
+ dset,dset_raw
+ )
+ )
+ f._add_dset(
+ dset_raw,
+ f[dset],
+ f.dtypes[dset],
+ attrs=f.attrs[dset],
+ )
[docs]defwrite_meta_to_csv(self,out_fpath=None):
+"""Combine NRWAL outputs with meta and write to output csv.
+
+ Parameters
+ ----------
+ out_fpath : str, optional
+ Full path to output NRWAL CSV file. The file path does not
+ need to include file ending - it will be added automatically
+ if missing. If ``None``, the generation HDF5 filepath will
+ be converted to a CSV out path by replacing the ".h5" file
+ ending with ".csv". By default, ``None``.
+
+ Returns
+ -------
+ str
+ Path to output file.
+ """
+ ifout_fpathisNone:
+ out_fpath=self._gen_fpath.replace(".h5",".csv")
+ elifnotout_fpath.endswith(".csv"):
+ out_fpath="{}.csv".format(out_fpath)
+
+ logger.info("Writing NRWAL outputs to: {}".format(out_fpath))
+ meta_out=self.meta_out[self.analysis_mask].copy()
+
+ fordset,arrinself._out.items():
+ iflen(arr.shape)!=1orarr.shape[0]!=meta_out.shape[0]:
+ msg=(
+ "Skipping output {!r}: shape {} cannot be combined "
+ "with meta of shape {}!".format(
+ dset,arr.shape,meta_out.shape
+ )
+ )
+ logger.warning(msg)
+ warn(msg)
+ continue
+ meta_out[dset]=arr
+
+ meta_out.to_csv(out_fpath,index=False)
+ logger.info("Finished writing NRWAL outputs to: {}".format(out_fpath))
+ returnout_fpath
+
+
[docs]defrun(self,csv_output=False,out_fpath=None):
+"""Run NRWAL analysis.
+
+ Parameters
+ ----------
+ csv_output : bool, optional
+ Option to write H5 file meta + all requested outputs to
+ CSV file instead of storing in the HDF5 file directly. This
+ can be useful if the same HDF5 file is used for multiple
+ sets of NRWAL runs. Note that all requested output datasets
+ must be 1-dimensional in order to fir within the CSV output.
+
+ .. Important:: This option is not compatible with
+ ``save_raw=True``. If you set ``csv_output=True``, then
+ the `save_raw` option is forced to be ``False``.
+ Therefore, make sure that you do not have any references
+ to "input_dataset_name_raw" in your NRWAL config. If you
+ need to manipulate an input dataset, save it to a
+ different output name in the NRWAL config or manually add
+ an "input_dataset_name_raw" dataset to your generation
+ HDF5 file before running NRWAL.
+
+ By default, ``False``.
+ out_fpath : str, optional
+ This option has no effect if ``csv_output=False``.
+ Otherwise, this should be the full path to output NRWAL CSV
+ file. The file path does not need to include file ending -
+ it will be added automatically if missing. If ``None``, the
+ generation HDF5 filepath will be converted to a CSV out path
+ by replacing the ".h5" file ending with ".csv".
+ By default, ``None``.
+
+ Returns
+ -------
+ str
+ Path to output file.
+ """
+ ifcsv_outputandself._save_raw:
+ msg=(
+ "`save_raw` option not allowed with `csv_output`. Setting"
+ "`save_raw=False`"
+ )
+ logger.warning(msg)
+ warn(msg)
+ self._save_raw=False
+
+ ifany(self.analysis_gids):
+ self.run_nrwal()
+ self.check_outputs()
+ ifcsv_output:
+ out_fp=self.write_meta_to_csv(out_fpath)
+ else:
+ out_fp=self.write_to_gen_fpath()
+
+ logger.info("NRWAL module complete!")
+
+ returnout_fp
[docs]defcli_qa_qc(modules,out_dir,max_workers=None):
+"""Run QA/QC on reV outputs
+
+ ``reV`` QA/QC performs quality assurance checks on ``reV`` output
+ data. Users can specify the type of QA/QC that should be applied
+ to each ``reV`` module.
+
+ Parameters
+ ----------
+ modules : dict
+ Dictionary of modules to QA/QC. Keys should be the names of the
+ modules to QA/QC. The values are dictionaries that represent the
+ config for the respective QA/QC step. Allowed config keys for
+ QA/QC are the "property" attributes of
+ :class:`~reV.qa_qc.qa_qc.QaQcModule`.
+ out_dir : str
+ Path to output directory.
+ max_workers : int, optional
+ Max number of workers to run for QA/QA. If ``None``, uses all
+ CPU cores. By default, ``None``.
+
+ Raises
+ ------
+ ValueError
+ If fpath is not an H5 or CSV file.
+ """
+ formodule,mcfinmodules.items():
+ module_config=QaQcModule(module,mcf,out_dir)
+
+ qa_dir=out_dir
+ ifmodule_config.sub_dirisnotNone:
+ qa_dir=os.path.join(out_dir,module_config.sub_dir)
+
+ ifmodule.lower()=='exclusions':
+ QaQc.exclusions_mask(module_config.fpath,qa_dir,
+ layers_dict=module_config.excl_dict,
+ min_area=module_config.min_area,
+ kernel=module_config.area_filter_kernel,
+ plot_type=module_config.plot_type,
+ cmap=module_config.cmap,
+ plot_step=module_config.plot_step)
+
+ elifmodule_config.fpath.endswith('.h5'):
+ QaQc.h5(module_config.fpath,qa_dir,dsets=module_config.dsets,
+ group=module_config.group,
+ process_size=module_config.process_size,
+ max_workers=max_workers,
+ plot_type=module_config.plot_type,cmap=module_config.cmap)
+
+ elifmodule_config.fpath.endswith('.csv'):
+ QaQc.supply_curve(module_config.fpath,qa_dir,
+ columns=module_config.columns,
+ lcoe=module_config.lcoe,
+ plot_type=module_config.plot_type,
+ cmap=module_config.cmap)
+ else:
+ msg=("Cannot run QA/QC for {}: 'fpath' must be a '*.h5' "
+ "or '*.csv' reV output file, but {} was given!"
+ .format(module,module_config.fpath))
+ logger.error(msg)
+ raiseValueError(msg)
+
+
+qa_qc_command=CLICommandFromFunction(cli_qa_qc,name=str(ModuleName.QA_QC),
+ split_keys=None)
+main=as_click_command(qa_qc_command)
+
+
+@click.group()
+@click.version_option(version=__version__)
+@click.option('-v','--verbose',is_flag=True,
+ help='Flag to turn on debug logging. Default is not verbose.')
+@click.pass_context
+defqa_qc_extra(ctx,verbose):
+"""Execute extra QA/QC utility"""
+ ctx.ensure_object(dict)
+ ctx.obj['VERBOSE']=verbose
+
+
+@qa_qc_extra.group(chain=True)
+@click.option('--out_dir','-o',type=click.Path(),required=True,
+ help="Directory path to save summary tables and plots too")
+@click.option('--log_file','-log',type=click.Path(),default=None,
+ show_default=True,
+ help='File to log to, by default None')
+@click.option('-v','--verbose',is_flag=True,
+ help='Flag to turn on debug logging.')
+@click.pass_context
+defsummarize(ctx,out_dir,log_file,verbose):
+"""
+ Summarize reV data
+ """
+ ctx.obj['OUT_DIR']=out_dir
+ ifany([verbose,ctx.obj['VERBOSE']]):
+ log_level='DEBUG'
+ else:
+ log_level='INFO'
+
+ init_logger('reV',log_file=log_file,log_level=log_level)
+
+
+@summarize.command()
+@click.option('--h5_file','-h5',type=click.Path(exists=True),required=True,
+ help='Path to .h5 file to summarize')
+@click.option('--dsets','-ds',type=STRLIST,default=None,
+ show_default=True,
+ help='Datasets to summarize, by default None')
+@click.option('--group','-grp',type=STR,default=None,
+ show_default=True,
+ help=('Group within h5_file to summarize datasets for, by '
+ 'default None'))
+@click.option('--process_size','-ps',type=INT,default=None,
+ show_default=True,
+ help='Number of sites to process at a time, by default None')
+@click.option('--max_workers','-w',type=INT,default=None,
+ show_default=True,
+ help=('Number of workers to use when summarizing 2D datasets,'
+ ' by default None'))
+@click.pass_context
+defh5(ctx,h5_file,dsets,group,process_size,max_workers):
+"""
+ Summarize datasets in .h5 file
+ """
+ SummarizeH5.run(h5_file,ctx.obj['OUT_DIR'],group=group,dsets=dsets,
+ process_size=process_size,max_workers=max_workers)
+
+
+@summarize.command()
+@click.option('--plot_type','-plt',default='plotly',
+ type=click.Choice(['plot','plotly'],case_sensitive=False),
+ show_default=True,
+ help=(" plot_type of plot to create 'plot' or 'plotly', by "
+ "default 'plot'"))
+@click.option('--cmap','-cmap',type=str,default='viridis',
+ show_default=True,
+ help="Colormap name, by default 'viridis'")
+@click.pass_context
+defscatter_plots(ctx,plot_type,cmap):
+"""
+ create scatter plots from h5 summary tables
+ """
+ QaQc.create_scatter_plots(ctx.obj['OUT_DIR'],plot_type,cmap)
+
+
+@summarize.command()
+@click.option('--sc_table','-sct',type=click.Path(exists=True),
+ required=True,help='Path to .csv containing Supply Curve table')
+@click.option('--columns','-cols',type=STRLIST,default=None,
+ show_default=True,
+ help=('Column(s) to summarize, if None summarize all numeric '
+ 'columns, by default None'))
+@click.pass_context
+defsupply_curve_table(ctx,sc_table,columns):
+"""
+ Summarize Supply Curve Table
+ """
+ ctx.obj['SC_TABLE']=sc_table
+ SummarizeSupplyCurve.run(sc_table,ctx.obj['OUT_DIR'],columns=columns)
+
+
+@summarize.command()
+@click.option('--sc_table','-sct',type=click.Path(exists=True),default=None,
+ show_default=True,
+ help=("Path to .csv containing Supply Curve table, can be "
+ "supplied in 'supply-curve-table'"))
+@click.option('--plot_type','-plt',default='plotly',
+ type=click.Choice(['plot','plotly'],case_sensitive=False),
+ show_default=True,
+ help=(" plot_type of plot to create 'plot' or 'plotly', by "
+ "default 'plot'"))
+@click.option('--lcoe','-lcoe',type=STR,default=SupplyCurveField.MEAN_LCOE,
+ help="LCOE value to plot, by default %(default)s")
+@click.pass_context
+defsupply_curve_plot(ctx,sc_table,plot_type,lcoe):
+"""
+ Plot Supply Curve (cumulative capacity vs LCOE)
+ """
+ ifsc_tableisNone:
+ sc_table=ctx.obj['SC_TABLE']
+
+ SupplyCurvePlot.plot(sc_table,ctx.obj['OUT_DIR'],
+ plot_type=plot_type,lcoe=lcoe)
+
+
+@summarize.command()
+@click.option('--excl_mask','-mask',type=click.Path(exists=True),
+ required=True,
+ help='Path to .npy file containing final exclusions mask')
+@click.option('--plot_type','-plt',default='plotly',
+ type=click.Choice(['plot','plotly'],case_sensitive=False),
+ show_default=True,
+ help=(" plot_type of plot to create 'plot' or 'plotly', by "
+ "default 'plot'"))
+@click.option('--cmap','-cmap',type=str,default='viridis',
+ show_default=True,
+ help="Colormap name, by default 'viridis'")
+@click.option('--plot_step','-step',type=int,default=100,
+ show_default=True,
+ help="Step between points to plot")
+@click.pass_context
+defexclusions_mask(ctx,excl_mask,plot_type,cmap,plot_step):
+"""
+ create heat map of exclusions mask
+ """
+ excl_mask=np.load(excl_mask)
+ ExclusionsMask.plot(excl_mask,ctx.obj['OUT_DIR'],
+ plot_type=plot_type,cmap=cmap,
+ plot_step=plot_step)
+
+
+if__name__=='__main__':
+ try:
+ main(obj={})
+ exceptException:
+ logger.exception('Error running reV QA/QC CLI.')
+ raise
+
[docs]defscatter_plot(self,value,cmap="viridis",out_path=None,**kwargs):
+"""
+ Plot scatter plot of value versus longitude and latitude using
+ pandas.plot.scatter
+
+ Parameters
+ ----------
+ value : str
+ Column name to plot as color
+ cmap : str, optional
+ Matplotlib colormap name, by default 'viridis'
+ out_path : str, optional
+ File path to save plot to, by default None
+ kwargs : dict
+ Additional kwargs for plotting.dataframes.df_scatter
+ """
+ self._check_value(self.summary,value)
+ mplt.df_scatter(self.summary,x=SupplyCurveField.LONGITUDE,
+ y=SupplyCurveField.LATITUDE,c=value,colormap=cmap,
+ filename=out_path,**kwargs)
+
+
[docs]defscatter_plotly(self,value,cmap="Viridis",out_path=None,**kwargs):
+"""
+ Plot scatter plot of value versus longitude and latitude using
+ plotly
+
+ Parameters
+ ----------
+ value : str
+ Column name to plot as color
+ cmap : str | px.color, optional
+ Continuous color scale to use, by default 'Viridis'
+ out_path : str, optional
+ File path to save plot to, can be a .html or static image,
+ by default None
+ kwargs : dict
+ Additional kwargs for plotly.express.scatter
+ """
+ self._check_value(self.summary,value)
+ fig=px.scatter(self.summary,x=SupplyCurveField.LONGITUDE,
+ y=SupplyCurveField.LATITUDE,color=value,
+ color_continuous_scale=cmap,**kwargs)
+ fig.update_layout(font=dict(family="Arial",size=18,color="black"))
+
+ ifout_pathisnotNone:
+ self._save_plotly(fig,out_path)
+
+ fig.show()
+
+ def_extract_sc_data(self,lcoe=SupplyCurveField.MEAN_LCOE):
+"""
+ Extract supply curve data
+
+ Parameters
+ ----------
+ lcoe : str, optional
+ LCOE value to use for supply curve,
+ by default :obj:`SupplyCurveField.MEAN_LCOE`
+
+ Returns
+ -------
+ sc_df : pandas.DataFrame
+ Supply curve data
+ """
+ values=[SupplyCurveField.CAPACITY_AC_MW,lcoe]
+ self._check_value(self.summary,values,scatter=False)
+ sc_df=self.summary[values].sort_values(lcoe)
+ sc_df['cumulative_capacity']=(
+ sc_df[SupplyCurveField.CAPACITY_AC_MW].cumsum()
+ )
+
+ returnsc_df
+
+
[docs]defdist_plot(self,value,out_path=None,**kwargs):
+"""
+ Plot distribution plot of value using seaborn.distplot
+
+ Parameters
+ ----------
+ value : str
+ Column name to plot
+ out_path : str, optional
+ File path to save plot to, by default None
+ kwargs : dict
+ Additional kwargs for plotting.dataframes.dist_plot
+ """
+ self._check_value(self.summary,value,scatter=False)
+ series=self.summary[value]
+ mplt.dist_plot(series,filename=out_path,**kwargs)
+
+
[docs]defdist_plotly(self,value,out_path=None,**kwargs):
+"""
+ Plot histogram of value using plotly
+
+ Parameters
+ ----------
+ value : str
+ Column name to plot
+ out_path : str, optional
+ File path to save plot to, by default None
+ kwargs : dict
+ Additional kwargs for plotly.express.histogram
+ """
+ self._check_value(self.summary,value,scatter=False)
+
+ fig=px.histogram(self.summary,x=value)
+
+ ifout_pathisnotNone:
+ self._save_plotly(fig,out_path,**kwargs)
+
+ fig.show()
+
+
[docs]@classmethod
+ defscatter(
+ cls,
+ summary_csv,
+ out_dir,
+ value,
+ plot_type="plotly",
+ cmap="viridis",
+ **kwargs,
+ ):
+"""
+ Create scatter plot for given value in summary table and save to
+ out_dir
+
+ Parameters
+ ----------
+ summary_csv : str
+ Path to .csv file containing summary table
+ out_dir : str
+ Output directory to save plots to
+ value : str
+ Column name to plot as color
+ plot_type : str, optional
+ plot_type of plot to create 'plot' or 'plotly', by default 'plotly'
+ cmap : str, optional
+ Colormap name, by default 'viridis'
+ kwargs : dict
+ Additional plotting kwargs
+ """
+ splt=cls(summary_csv)
+ ifplot_type=="plot":
+ out_path=os.path.basename(summary_csv).replace(".csv",".png")
+ out_path=os.path.join(out_dir,out_path)
+ splt.scatter_plot(
+ value,cmap=cmap.lower(),out_path=out_path,**kwargs
+ )
+ elifplot_type=="plotly":
+ out_path=os.path.basename(summary_csv).replace(".csv",".html")
+ out_path=os.path.join(out_dir,out_path)
+ splt.scatter_plotly(
+ value,cmap=cmap.capitalize(),out_path=out_path,**kwargs
+ )
+ else:
+ msg=(
+ "plot_type must be 'plot' or 'plotly' but {} was given".format(
+ plot_type
+ )
+ )
+ logger.error(msg)
+ raiseValueError(msg)
+
+
[docs]@classmethod
+ defscatter_all(
+ cls,summary_csv,out_dir,plot_type="plotly",cmap="viridis",**kwargs
+ ):
+"""
+ Create scatter plot for all summary stats in summary table and save to
+ out_dir
+
+ Parameters
+ ----------
+ summary_csv : str
+ Path to .csv file containing summary table
+ out_dir : str
+ Output directory to save plots to
+ plot_type : str, optional
+ plot_type of plot to create 'plot' or 'plotly', by default 'plotly'
+ cmap : str, optional
+ Colormap name, by default 'viridis'
+ kwargs : dict
+ Additional plotting kwargs
+ """
+ splt=cls(summary_csv)
+ splt._data=splt.summary.select_dtypes(include=np.number)
+ datasets=[
+ cforcinsplt.summary.columnsifnotc.startswith(("lat","lon"))
+ ]
+
+ forvalueindatasets:
+ ifplot_type=="plot":
+ out_path="_{}.png".format(value)
+ out_path=os.path.basename(summary_csv).replace(
+ ".csv",out_path
+ )
+ out_path=os.path.join(out_dir,out_path)
+ splt.scatter_plot(
+ value,cmap=cmap.lower(),out_path=out_path,**kwargs
+ )
+ elifplot_type=="plotly":
+ out_path="_{}.html".format(value)
+ out_path=os.path.basename(summary_csv).replace(
+ ".csv",out_path
+ )
+ out_path=os.path.join(out_dir,out_path)
+ splt.scatter_plotly(
+ value,cmap=cmap.capitalize(),out_path=out_path,**kwargs
+ )
+ else:
+ msg=("plot_type must be 'plot' or 'plotly' but {} was given"
+ .format(plot_type))
+ logger.error(msg)
+ raiseValueError(msg)
[docs]classRepresentativeMethods:
+"""Class for organizing the methods to determine representative-ness"""
+
+ def__init__(
+ self,profiles,weights=None,rep_method="meanoid",err_method="rmse"
+ ):
+"""
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+ weights : np.ndarray | list
+ 1D array of weighting factors (multiplicative) for profiles.
+ rep_method : str
+ Method identifier for calculation of the representative profile.
+ err_method : str | None
+ Method identifier for calculation of error from the representative
+ profile (e.g. "rmse", "mae", "mbe"). If this is None, the
+ representative meanoid / medianoid profile will be returned
+ directly
+ """
+ self._rep_method=self.rep_methods[rep_method]
+ self._err_method=self.err_methods[err_method]
+ self._profiles=profiles
+ self._weights=weights
+ self._parse_weights()
+
+ def_parse_weights(self):
+"""Parse the weights attribute. Check shape and make np.array."""
+ ifisinstance(self._weights,(list,tuple)):
+ self._weights=np.array(self._weights)
+
+ ifself._weightsisnotNone:
+ emsg=(
+ "Weighting factors array of length {} does not match "
+ "profiles of shape {}".format(
+ len(self._weights),self._profiles.shape[1]
+ )
+ )
+ assertlen(self._weights)==self._profiles.shape[1],emsg
+
+ @property
+ defrep_methods(self):
+"""Lookup table of representative methods"""
+ methods={
+ "mean":self.meanoid,
+ "meanoid":self.meanoid,
+ "median":self.medianoid,
+ "medianoid":self.medianoid,
+ }
+
+ returnmethods
+
+ @property
+ deferr_methods(self):
+"""Lookup table of error methods"""
+ methods={
+ "mbe":self.mbe,
+ "mae":self.mae,
+ "rmse":self.rmse,
+ None:None,
+ }
+
+ returnmethods
+
+
[docs]@staticmethod
+ defnargmin(arr,n):
+"""Get the index of the Nth min value in arr.
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ 1D array.
+ n : int
+ If n is 0, this returns the location of the min value in arr.
+ If n is 1, this returns the location of the 2nd min value in arr.
+
+ Returns
+ -------
+ i : int
+ Location of the Nth min value in arr.
+ """
+ returnarr.argsort()[:(n+1)][-1]
+
+
[docs]@staticmethod
+ defmeanoid(profiles,weights=None):
+"""Find the mean profile across all sites.
+
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+ weights : np.ndarray | list
+ 1D array of weighting factors (multiplicative) for profiles.
+
+ Returns
+ -------
+ arr : np.ndarray
+ (time, 1) timeseries of the mean of all cf profiles across sites.
+ """
+ ifweightsisNone:
+ arr=profiles.mean(axis=1).reshape((len(profiles),1))
+ else:
+ ifnotisinstance(weights,np.ndarray):
+ weights=np.array(weights)
+
+ arr=(profiles*weights).sum(axis=1)/weights.sum()
+ iflen(arr.shape)==1:
+ arr=np.expand_dims(arr,axis=1)
+
+ returnarr
+
+
[docs]@staticmethod
+ defmedianoid(profiles):
+"""Find the median profile across all sites.
+
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+
+ Returns
+ -------
+ arr : np.ndarray
+ (time, 1) timeseries of the median at every timestep of all
+ cf profiles across sites.
+ """
+ arr=np.median(profiles,axis=1)
+ arr=arr.reshape((len(profiles),1))
+
+ returnarr
+
+
[docs]@classmethod
+ defmbe(cls,profiles,baseline,i_profile=0):
+"""Calculate the mean bias error of profiles vs. a baseline profile.
+
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+ baseline : np.ndarray
+ (time, 1) timeseries of the meanoid or medianoid to which
+ cf profiles should be compared.
+ i_profile : int
+ The index of the represntative profile being saved
+ (for n_profiles). 0 is the most representative profile.
+
+ Returns
+ -------
+ profile : np.ndarray
+ (time, 1) array for the most representative profile
+ i_rep : int
+ Column Index in profiles of the representative profile.
+ """
+ diff=profiles-baseline.reshape((len(baseline),1))
+ mbe=diff.mean(axis=0)
+ i_rep=cls.nargmin(mbe,i_profile)
+
+ returnprofiles[:,i_rep],i_rep
+
+
[docs]@classmethod
+ defmae(cls,profiles,baseline,i_profile=0):
+"""Calculate the mean absolute error of profiles vs. a baseline profile
+
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+ baseline : np.ndarray
+ (time, 1) timeseries of the meanoid or medianoid to which
+ cf profiles should be compared.
+ i_profile : int
+ The index of the represntative profile being saved
+ (for n_profiles). 0 is the most representative profile.
+
+ Returns
+ -------
+ profile : np.ndarray
+ (time, 1) array for the most representative profile
+ i_rep : int
+ Column Index in profiles of the representative profile.
+ """
+ diff=profiles-baseline.reshape((len(baseline),1))
+ mae=np.abs(diff).mean(axis=0)
+ i_rep=cls.nargmin(mae,i_profile)
+
+ returnprofiles[:,i_rep],i_rep
+
+
[docs]@classmethod
+ defrmse(cls,profiles,baseline,i_profile=0):
+"""Calculate the RMSE of profiles vs. a baseline profile
+
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+ baseline : np.ndarray
+ (time, 1) timeseries of the meanoid or medianoid to which
+ cf profiles should be compared.
+ i_profile : int
+ The index of the represntative profile being saved
+ (for n_profiles). 0 is the most representative profile.
+
+ Returns
+ -------
+ profile : np.ndarray
+ (time, 1) array for the most representative profile
+ i_rep : int
+ Column Index in profiles of the representative profile.
+ """
+ rmse=profiles-baseline.reshape((len(baseline),1))
+ rmse**=2
+ rmse=np.sqrt(np.mean(rmse,axis=0))
+ i_rep=cls.nargmin(rmse,i_profile)
+
+ returnprofiles[:,i_rep],i_rep
+
+
[docs]@classmethod
+ defrun(
+ cls,
+ profiles,
+ weights=None,
+ rep_method="meanoid",
+ err_method="rmse",
+ n_profiles=1,
+ ):
+"""Run representative profile methods.
+
+ Parameters
+ ----------
+ profiles : np.ndarray
+ (time, sites) timeseries array of cf profile data.
+ weights : np.ndarray | list
+ 1D array of weighting factors (multiplicative) for profiles.
+ rep_method : str
+ Method identifier for calculation of the representative profile.
+ err_method : str | None
+ Method identifier for calculation of error from the representative
+ profile (e.g. "rmse", "mae", "mbe"). If this is None, the
+ representative meanoid / medianoid profile will be returned
+ directly.
+ n_profiles : int
+ Number of representative profiles to save to fout.
+
+ Returns
+ -------
+ profiles : np.ndarray
+ (time, n_profiles) array for the most representative profile(s)
+ i_reps : list | None
+ List (length of n_profiles) with column Index in profiles of the
+ representative profile(s). If err_method is None, this value is
+ also set to None.
+ """
+ inst=cls(
+ profiles,
+ weights=weights,
+ rep_method=rep_method,
+ err_method=err_method,
+ )
+
+ ifinst._weightsisnotNone:
+ baseline=inst._rep_method(inst._profiles,weights=inst._weights)
+ else:
+ baseline=inst._rep_method(inst._profiles)
+
+ iferr_methodisNone:
+ profiles=baseline
+ i_reps=[None]
+
+ else:
+ profiles=None
+ i_reps=[]
+ foriinrange(n_profiles):
+ p,ir=inst._err_method(inst._profiles,baseline,i_profile=i)
+ ifprofilesisNone:
+ profiles=np.zeros((len(p),n_profiles),dtype=p.dtype)
+
+ profiles[:,i]=p
+ i_reps.append(ir)
+
+ returnprofiles,i_reps
+
+
+
[docs]classRegionRepProfile:
+"""Framework to handle rep profile for one resource region"""
+
+ RES_GID_COL=SupplyCurveField.RES_GIDS
+ GEN_GID_COL=SupplyCurveField.GEN_GIDS
+
+ def__init__(self,gen_fpath,rev_summary,cf_dset='cf_profile',
+ rep_method='meanoid',err_method='rmse',
+ weight=SupplyCurveField.GID_COUNTS,
+ n_profiles=1):
+"""
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to reV gen output file to extract "cf_profile" from.
+ rev_summary : pd.DataFrame
+ Aggregated rev supply curve summary file trimmed to just one
+ region to get a rep profile for.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+ cf_dset : str
+ Dataset name to pull generation profiles from.
+ rep_method : str
+ Method identifier for calculation of the representative profile.
+ err_method : str | None
+ Method identifier for calculation of error from the representative
+ profile (e.g. "rmse", "mae", "mbe"). If this is None, the
+ representative meanoid / medianoid profile will be returned
+ directly
+ weight : str | None
+ Column in rev_summary used to apply weighted mean to profiles.
+ The supply curve table data in the weight column should have
+ weight values corresponding to the res_gids in the same row.
+ n_profiles : int
+ Number of representative profiles to retrieve.
+ """
+
+ self._gen_fpath=gen_fpath
+ self._rev_summary=rev_summary
+ self._cf_dset=cf_dset
+ self._profiles=None
+ self._source_profiles=None
+ self._weights=None
+ self._i_reps=None
+ self._rep_method=rep_method
+ self._err_method=err_method
+ self._weight=weight
+ self._n_profiles=n_profiles
+ self._gen_gids=None
+ self._res_gids=None
+
+ self._init_profiles_weights()
+
+ def_init_profiles_weights(self):
+"""Initialize the base source profiles and weight arrays"""
+ gen_gids=self._get_region_attr(self._rev_summary,self.GEN_GID_COL)
+ res_gids=self._get_region_attr(self._rev_summary,self.RES_GID_COL)
+
+ self._weights=np.ones(len(res_gids))
+ ifself._weightisnotNone:
+ self._weights=self._get_region_attr(
+ self._rev_summary,self._weight
+ )
+
+ df=pd.DataFrame(
+ {
+ self.GEN_GID_COL:gen_gids,
+ self.RES_GID_COL:res_gids,
+ "weights":self._weights,
+ }
+ )
+ df=df.sort_values(self.RES_GID_COL)
+ self._gen_gids=df[self.GEN_GID_COL].values
+ self._res_gids=df[self.RES_GID_COL].values
+ ifself._weightisnotNone:
+ self._weights=df["weights"].values
+ else:
+ self._weights=None
+
+ withResource(self._gen_fpath)asres:
+ meta=res.meta
+
+ assertResourceMetaField.GIDinmeta
+ source_res_gids=meta[ResourceMetaField.GID].values
+ msg=('Resource gids from "gid" column in meta data from "{}" '
+ 'must be sorted! reV generation should always be run with '
+ 'sequential project points.'.format(self._gen_fpath))
+ assertnp.all(source_res_gids[:-1]<=source_res_gids[1:]),msg
+
+ missing=set(self._res_gids)-set(source_res_gids)
+ msg=(
+ "The following resource gids were found in the rev summary "
+ "supply curve file but not in the source generation meta "
+ "data: {}".format(missing)
+ )
+ assertnotany(missing),msg
+
+ unique_res_gids,u_idxs=np.unique(
+ self._res_gids,return_inverse=True
+ )
+ iloc=np.where(np.isin(source_res_gids,unique_res_gids))[0]
+ self._source_profiles=res[self._cf_dset,:,iloc[u_idxs]]
+
+ @property
+ defsource_profiles(self):
+"""Retrieve the cf profile array from the source generation h5 file.
+
+ Returns
+ -------
+ profiles : np.ndarray
+ Timeseries array of cf profile data.
+ """
+ returnself._source_profiles
+
+ @property
+ defweights(self):
+"""Get the weights array
+
+ Returns
+ -------
+ weights : np.ndarray | None
+ Flat array of weight values from the weight column. The supply
+ curve table data in the weight column should have a list of weight
+ values corresponding to the gen_gids list in the same row.
+ """
+ returnself._weights
+
+ @staticmethod
+ def_get_region_attr(rev_summary,attr_name):
+"""Retrieve a flat list of attribute data from a col in rev summary.
+
+ Parameters
+ ----------
+ rev_summary : pd.DataFrame
+ Aggregated rev supply curve summary file trimmed to just one
+ region to get a rep profile for.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+ attr_name : str
+ Column label to extract flattened data from (gen_gids,
+ gid_counts, etc...)
+
+ Returns
+ -------
+ data : list
+ Flat list of data from the column with label "attr_name".
+ Either a list of numbers or strings. Lists of jsonified lists
+ will be unpacked.
+ """
+ data=rev_summary[attr_name].values.tolist()
+
+ ifany(data):
+ ifisinstance(data[0],str):
+ # pylint: disable=simplifiable-condition
+ if("["and"]"indata[0])or("("and")"indata[0]):
+ data=[json.loads(s)forsindata]
+
+ ifisinstance(data[0],(list,tuple)):
+ data=[aforbindataforainb]
+
+ returndata
+
+ def_run_rep_methods(self):
+"""Run the representative profile methods to find the meanoid/medianoid
+ profile and find the profiles most similar."""
+
+ num_profiles=self.source_profiles.shape[1]
+ bad_data_shape=(self.weightsisnotNone
+ andlen(self.weights)!=num_profiles)
+ ifbad_data_shape:
+ e=('Weights column "{}" resulted in {} weight scalars '
+ 'which doesnt match gid column which yields '
+ 'profiles with shape {}.'
+ .format(self._weight,len(self.weights),
+ self.source_profiles.shape))
+ logger.debug('Gids from column "res_gids" with len {}: {}'
+ .format(len(self._res_gids),self._res_gids))
+ logger.debug('Weights from column "{}" with len {}: {}'
+ .format(self._weight,len(self.weights),
+ self.weights))
+ logger.error(e)
+ raiseDataShapeError(e)
+
+ self._profiles,self._i_reps=RepresentativeMethods.run(
+ self.source_profiles,
+ weights=self.weights,
+ rep_method=self._rep_method,
+ err_method=self._err_method,
+ n_profiles=self._n_profiles,
+ )
+
+ @property
+ defrep_profiles(self):
+"""Get the representative profiles of this region."""
+ ifself._profilesisNone:
+ self._run_rep_methods()
+
+ returnself._profiles
+
+ @property
+ defi_reps(self):
+"""Get the representative profile index(es) of this region."""
+ ifself._i_repsisNone:
+ self._run_rep_methods()
+
+ returnself._i_reps
+
+ @property
+ defrep_gen_gids(self):
+"""Get the representative profile gen gids of this region."""
+ gids=self._gen_gids
+ ifself.i_reps[0]isNone:
+ rep_gids=None
+ else:
+ rep_gids=[gids[i]foriinself.i_reps]
+
+ returnrep_gids
+
+ @property
+ defrep_res_gids(self):
+"""Get the representative profile resource gids of this region."""
+ gids=self._res_gids
+ ifself.i_reps[0]isNoneorgidsisNone:
+ rep_gids=[None]
+ else:
+ rep_gids=[gids[i]foriinself.i_reps]
+
+ returnrep_gids
+
+
[docs]@classmethod
+ defget_region_rep_profile(cls,gen_fpath,rev_summary,
+ cf_dset='cf_profile',
+ rep_method='meanoid',
+ err_method='rmse',
+ weight=SupplyCurveField.GID_COUNTS,
+ n_profiles=1):
+"""Class method for parallelization of rep profile calc.
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to reV gen output file to extract "cf_profile" from.
+ rev_summary : pd.DataFrame
+ Aggregated rev supply curve summary file trimmed to just one
+ region to get a rep profile for.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+ cf_dset : str
+ Dataset name to pull generation profiles from.
+ rep_method : str
+ Method identifier for calculation of the representative profile.
+ err_method : str | None
+ Method identifier for calculation of error from the representative
+ profile (e.g. "rmse", "mae", "mbe"). If this is None, the
+ representative meanoid / medianoid profile will be returned
+ directly
+ weight : str | None
+ Column in rev_summary used to apply weighted mean to profiles.
+ The supply curve table data in the weight column should have
+ weight values corresponding to the res_gids in the same row.
+ n_profiles : int
+ Number of representative profiles to retrieve.
+
+ Returns
+ -------
+ rep_profile : np.ndarray
+ (time, n_profiles) array for the most representative profile(s)
+ i_rep : list
+ Column Index in profiles of the representative profile(s).
+ gen_gid_reps : list
+ Generation gid(s) of the representative profile(s).
+ res_gid_reps : list
+ Resource gid(s) of the representative profile(s).
+ """
+ r=cls(
+ gen_fpath,
+ rev_summary,
+ cf_dset=cf_dset,
+ rep_method=rep_method,
+ err_method=err_method,
+ weight=weight,
+ n_profiles=n_profiles,
+ )
+
+ returnr.rep_profiles,r.i_reps,r.rep_gen_gids,r.rep_res_gids
+
+
+
[docs]classRepProfilesBase(ABC):
+"""Abstract utility framework for representative profile run classes."""
+
+ def__init__(self,gen_fpath,rev_summary,reg_cols=None,
+ cf_dset='cf_profile',rep_method='meanoid',
+ err_method='rmse',weight=SupplyCurveField.GID_COUNTS,
+ n_profiles=1):
+"""
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to reV gen output file to extract "cf_profile" from.
+ rev_summary : str | pd.DataFrame
+ Aggregated rev supply curve summary file. Str filepath or full df.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+ reg_cols : str | list | None
+ Label(s) for a categorical region column(s) to extract profiles
+ for. e.g. "state" will extract a rep profile for each unique entry
+ in the "state" column in rev_summary.
+ cf_dset : str
+ Dataset name to pull generation profiles from.
+ rep_method : str
+ Method identifier for calculation of the representative profile.
+ err_method : str | None
+ Method identifier for calculation of error from the representative
+ profile (e.g. "rmse", "mae", "mbe"). If this is None, the
+ representative meanoid / medianoid profile will be returned
+ directly
+ weight : str | None
+ Column in rev_summary used to apply weighted mean to profiles.
+ The supply curve table data in the weight column should have
+ weight values corresponding to the res_gids in the same row.
+ n_profiles : int
+ Number of representative profiles to save to fout.
+ """
+
+ logger.info(
+ 'Running rep profiles with gen_fpath: "{}"'.format(gen_fpath)
+ )
+ logger.info(
+ 'Running rep profiles with rev_summary: "{}"'.format(rev_summary)
+ )
+ logger.info(
+ 'Running rep profiles with region columns: "{}"'.format(reg_cols)
+ )
+ logger.info(
+ 'Running rep profiles with representative method: "{}"'.format(
+ rep_method
+ )
+ )
+ logger.info(
+ 'Running rep profiles with error method: "{}"'.format(err_method)
+ )
+ logger.info(
+ 'Running rep profiles with weight factor: "{}"'.format(weight)
+ )
+
+ self._weight=weight
+ self._n_profiles=n_profiles
+ self._cf_dset=cf_dset
+ self._gen_fpath=gen_fpath
+ self._reg_cols=reg_cols
+
+ self._rev_summary=self._parse_rev_summary(rev_summary)
+
+ self._check_req_cols(self._rev_summary,self._reg_cols)
+ self._check_req_cols(self._rev_summary,self._weight)
+ self._check_req_cols(self._rev_summary,RegionRepProfile.RES_GID_COL)
+ self._check_req_cols(self._rev_summary,RegionRepProfile.GEN_GID_COL)
+
+ self._check_rev_gen(gen_fpath,cf_dset,self._rev_summary)
+ self._time_index=None
+ self._meta=None
+ self._profiles=None
+ self._rep_method=rep_method
+ self._err_method=err_method
+
+ @staticmethod
+ def_parse_rev_summary(rev_summary):
+"""Extract, parse, and check the rev summary table.
+
+ Parameters
+ ----------
+ rev_summary : str | pd.DataFrame
+ Aggregated rev supply curve summary file. Str filepath or full df.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+
+ Returns
+ -------
+ rev_summary : pd.DataFrame
+ Aggregated rev supply curve summary file. Full df.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+ """
+
+ ifisinstance(rev_summary,str):
+ ifos.path.exists(rev_summary)andrev_summary.endswith(".csv"):
+ rev_summary=pd.read_csv(rev_summary)
+ elifos.path.exists(rev_summary)andrev_summary.endswith(".json"):
+ rev_summary=pd.read_json(rev_summary)
+ else:
+ e="Could not parse reV summary file: {}".format(rev_summary)
+ logger.error(e)
+ raiseFileInputError(e)
+ elifnotisinstance(rev_summary,pd.DataFrame):
+ e="Bad input dtype for rev_summary input: {}".format(
+ type(rev_summary)
+ )
+ logger.error(e)
+ raiseTypeError(e)
+
+ returnrev_summary
+
+ @staticmethod
+ def_check_req_cols(df,cols):
+"""Check a dataframe for required columns.
+
+ Parameters
+ ----------
+ df : pd.DataFrame
+ Dataframe to check columns.
+ cols : str | list | tuple
+ Required columns in df.
+ """
+ ifcolsisnotNone:
+ ifisinstance(cols,str):
+ cols=[cols]
+
+ missing=[cforcincolsifcnotindf]
+
+ ifany(missing):
+ e="Column labels not found in rev_summary table: {}".format(
+ missing
+ )
+ logger.error(e)
+ raiseKeyError(e)
+
+ @staticmethod
+ def_check_rev_gen(gen_fpath,cf_dset,rev_summary):
+"""Check rev gen file for requisite datasets.
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to reV gen output file to extract "cf_profile" from.
+ cf_dset : str
+ Dataset name to pull generation profiles from.
+ rev_summary : pd.DataFrame
+ Aggregated rev supply curve summary file. Full df.
+ Must include "res_gids", "gen_gids", and the "weight" column (if
+ weight is not None)
+ """
+ withResource(gen_fpath)asres:
+ dsets=res.datasets
+ ifcf_dsetnotindsets:
+ raiseKeyError(
+ 'reV gen file needs to have "{}" '
+ "dataset to calculate representative profiles!".format(
+ cf_dset
+ )
+ )
+
+ if"time_index"notinstr(dsets):
+ raiseKeyError(
+ 'reV gen file needs to have "time_index" '
+ "dataset to calculate representative profiles!"
+ )
+
+ shape=res.get_dset_properties(cf_dset)[0]
+
+ iflen(rev_summary)>shape[1]:
+ msg=(
+ "WARNING: reV SC summary table has {} sc points and CF "
+ 'dataset "{}" has {} profiles. There should never be more '
+ "SC points than CF profiles.".format(
+ len(rev_summary),cf_dset,shape[1]
+ )
+ )
+ logger.warning(msg)
+ warn(msg)
+
+ def_init_profiles(self):
+"""Initialize the output rep profiles attribute."""
+ self._profiles={
+ k:np.zeros(
+ (len(self.time_index),len(self.meta)),dtype=np.float32
+ )
+ forkinrange(self._n_profiles)
+ }
+
+ @property
+ deftime_index(self):
+"""Get the time index for the rep profiles.
+
+ Returns
+ -------
+ time_index : pd.datetimeindex
+ Time index sourced from the reV gen file.
+ """
+ ifself._time_indexisNone:
+ withResource(self._gen_fpath)asres:
+ ds="time_index"
+ ifparse_year(self._cf_dset,option="bool"):
+ year=parse_year(self._cf_dset,option="raise")
+ ds+="-{}".format(year)
+
+ self._time_index=res._get_time_index(ds,slice(None))
+
+ returnself._time_index
+
+ @property
+ defmeta(self):
+"""Meta data for the representative profiles.
+
+ Returns
+ -------
+ meta : pd.DataFrame
+ Meta data for the representative profiles. At the very least,
+ this has columns for the region and res class.
+ """
+ returnself._meta
+
+ @property
+ defprofiles(self):
+"""Get the arrays of representative CF profiles corresponding to meta.
+
+ Returns
+ -------
+ profiles : dict
+ dict of n_profile-keyed arrays with shape (time, n) for the
+ representative profiles for each region.
+ """
+ returnself._profiles
+
+ def_init_h5_out(
+ self,fout,save_rev_summary=True,scaled_precision=False
+ ):
+"""Initialize an output h5 file for n_profiles
+
+ Parameters
+ ----------
+ fout : str
+ None or filepath to output h5 file.
+ save_rev_summary : bool
+ Flag to save full reV SC table to rep profile output.
+ scaled_precision : bool
+ Flag to scale cf_profiles by 1000 and save as uint16.
+ """
+ dsets=[]
+ shapes={}
+ attrs={}
+ chunks={}
+ dtypes={}
+
+ foriinrange(self._n_profiles):
+ dset="rep_profiles_{}".format(i)
+ dsets.append(dset)
+ shapes[dset]=self.profiles[0].shape
+ chunks[dset]=None
+
+ ifscaled_precision:
+ attrs[dset]={"scale_factor":1000}
+ dtypes[dset]=np.uint16
+ else:
+ attrs[dset]=None
+ dtypes[dset]=self.profiles[0].dtype
+
+ meta=self.meta.copy()
+ forcinmeta.columns:
+ withcontextlib.suppress(ValueError):
+ meta[c]=pd.to_numeric(meta[c])
+
+ Outputs.init_h5(
+ fout,
+ dsets,
+ shapes,
+ attrs,
+ chunks,
+ dtypes,
+ meta,
+ time_index=self.time_index,
+ )
+
+ ifsave_rev_summary:
+ withOutputs(fout,mode="a")asout:
+ rev_sum=to_records_array(self._rev_summary)
+ out._create_dset(
+ "rev_summary",rev_sum.shape,rev_sum.dtype,data=rev_sum
+ )
+
+ def_write_h5_out(self,fout,save_rev_summary=True):
+"""Write profiles and meta to an output file.
+
+ Parameters
+ ----------
+ fout : str
+ None or filepath to output h5 file.
+ save_rev_summary : bool
+ Flag to save full reV SC table to rep profile output.
+ scaled_precision : bool
+ Flag to scale cf_profiles by 1000 and save as uint16.
+ """
+ withOutputs(fout,mode="a")asout:
+ if"rev_summary"inout.datasetsandsave_rev_summary:
+ rev_sum=to_records_array(self._rev_summary)
+ out["rev_summary"]=rev_sum
+
+ foriinrange(self._n_profiles):
+ dset="rep_profiles_{}".format(i)
+ out[dset]=self.profiles[i]
+
+
[docs]defsave_profiles(
+ self,fout,save_rev_summary=True,scaled_precision=False
+ ):
+"""Initialize fout and save profiles.
+
+ Parameters
+ ----------
+ fout : str
+ None or filepath to output h5 file.
+ save_rev_summary : bool
+ Flag to save full reV SC table to rep profile output.
+ scaled_precision : bool
+ Flag to scale cf_profiles by 1000 and save as uint16.
+ """
+
+ self._init_h5_out(
+ fout,
+ save_rev_summary=save_rev_summary,
+ scaled_precision=scaled_precision,
+ )
+ self._write_h5_out(fout,save_rev_summary=save_rev_summary)
+
+ @abstractmethod
+ def_run_serial(self):
+"""Abstract method for serial run method."""
+
+ @abstractmethod
+ def_run_parallel(self):
+"""Abstract method for parallel run method."""
+
+
[docs]@abstractmethod
+ defrun(self):
+"""Abstract method for generic run method."""
+
+
+
[docs]classRepProfiles(RepProfilesBase):
+"""RepProfiles"""
+
+ def__init__(self,gen_fpath,rev_summary,reg_cols,
+ cf_dset='cf_profile',
+ rep_method='meanoid',err_method='rmse',
+ weight=str(SupplyCurveField.GID_COUNTS),# str() to fix docs
+ n_profiles=1,aggregate_profiles=False):
+"""ReV rep profiles class.
+
+ ``reV`` rep profiles compute representative generation profiles
+ for each supply curve point output by ``reV`` supply curve
+ aggregation. Representative profiles can either be a spatial
+ aggregation of generation profiles or actual generation profiles
+ that most closely resemble an aggregated profile (selected based
+ on an error metric).
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to ``reV`` generation output HDF5 file to extract
+ `cf_dset` dataset from.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ path can contain brackets ``{}`` that will be filled in by
+ the `analysis_years` input. Alternatively, this input can
+ be set to ``"PIPELINE"``, which will parse this input from
+ one of these preceding pipeline steps: ``multi-year``,
+ ``collect``, ``generation``, or
+ ``supply-curve-aggregation``. However, note that duplicate
+ executions of any of these commands within the pipeline
+ may invalidate this parsing, meaning the `gen_fpath` input
+ will have to be specified manually.
+
+ rev_summary : str | pd.DataFrame
+ Aggregated ``reV`` supply curve summary file. Must include
+ the following columns:
+
+ - ``res_gids`` : string representation of python list
+ containing the resource GID values corresponding to
+ each supply curve point.
+ - ``gen_gids`` : string representation of python list
+ containing the ``reV`` generation GID values
+ corresponding to each supply curve point.
+ - weight column (name based on `weight` input) : string
+ representation of python list containing the resource
+ GID weights for each supply curve point.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ input can be set to ``"PIPELINE"``, which will parse this
+ input from one of these preceding pipeline steps:
+ ``supply-curve-aggregation`` or ``supply-curve``.
+ However, note that duplicate executions of any of these
+ commands within the pipeline may invalidate this parsing,
+ meaning the `rev_summary` input will have to be specified
+ manually.
+
+ reg_cols : str | list
+ Label(s) for a categorical region column(s) to extract
+ profiles for. For example, ``"state"`` will extract a rep
+ profile for each unique entry in the ``"state"`` column in
+ `rev_summary`. To get a profile for each supply curve point,
+ try setting `reg_cols` to a primary key such as
+ ``"sc_gid"``.
+ cf_dset : str, optional
+ Dataset name to pull generation profiles from. This dataset
+ must be present in the `gen_fpath` HDF5 file. By default,
+ ``"cf_profile"``
+
+ .. Note:: If executing ``reV`` from the command line, this
+ name can contain brackets ``{}`` that will be filled in by
+ the `analysis_years` input (e.g. ``"cf_profile-{}"``).
+
+ rep_method : {'mean', 'meanoid', 'median', 'medianoid'}, optional
+ Method identifier for calculation of the representative
+ profile. By default, ``'meanoid'``
+ err_method : {'mbe', 'mae', 'rmse'}, optional
+ Method identifier for calculation of error from the
+ representative profile. If this input is ``None``, the
+ representative meanoid / medianoid profile will be returned
+ directly. By default, ``'rmse'``.
+ weight : str, optional
+ Column in `rev_summary` used to apply weights when computing
+ mean profiles. The supply curve table data in the weight
+ column should have weight values corresponding to the
+ `res_gids` in the same row (i.e. string representation of
+ python list containing weight values).
+
+ .. Important:: You'll often want to set this value to
+ something other than ``None`` (typically ``"gid_counts"``
+ if running on standard ``reV`` outputs). Otherwise, the
+ unique generation profiles within each supply curve point
+ are weighted equally. For example, if you have a 64x64
+ supply curve point, and one generation profile takes up
+ 4095 (99.98%) 90m cells while a second generation profile
+ takes up only one 90m cell (0.02%), they will contribute
+ *equally* to the meanoid profile unless these weights are
+ specified.
+
+ By default, :obj:`SupplyCurveField.GID_COUNTS`.
+ n_profiles : int, optional
+ Number of representative profiles to save to the output
+ file. By default, ``1``.
+ aggregate_profiles : bool, optional
+ Flag to calculate the aggregate (weighted meanoid) profile
+ for each supply curve point. This behavior is in lieu of
+ finding the single profile per region closest to the
+ meanoid. If you set this flag to ``True``, the `rep_method`,
+ `err_method`, and `n_profiles` inputs will be forcibly set
+ to the default values. By default, ``False``.
+ """
+
+ log_versions(logger)
+ logger.info(
+ "Finding representative profiles that are most similar "
+ "to the weighted meanoid for each supply curve region."
+ )
+
+ ifreg_colsisNone:
+ e=(
+ 'Need to define "reg_cols"! If you want a profile for each '
+ 'supply curve point, try setting "reg_cols" to a primary '
+ 'key such as "sc_gid".'
+ )
+ logger.error(e)
+ raiseValueError(e)
+ ifisinstance(reg_cols,str):
+ reg_cols=[reg_cols]
+ elifnotisinstance(reg_cols,list):
+ reg_cols=list(reg_cols)
+
+ self._aggregate_profiles=aggregate_profiles
+ ifself._aggregate_profiles:
+ logger.info(
+ "Aggregate profiles input set to `True`. Setting "
+ "'rep_method' to `'meanoid'`, 'err_method' to `None`, "
+ "and 'n_profiles' to `1`"
+ )
+ rep_method="meanoid"
+ err_method=None
+ n_profiles=1
+
+ super().__init__(
+ gen_fpath,
+ rev_summary,
+ reg_cols=reg_cols,
+ cf_dset=cf_dset,
+ rep_method=rep_method,
+ err_method=err_method,
+ weight=weight,
+ n_profiles=n_profiles,
+ )
+
+ self._set_meta()
+ self._init_profiles()
+
+ def_set_meta(self):
+"""Set the rep profile meta data with each row being a unique
+ combination of the region columns."""
+ ifself._err_methodisNone:
+ self._meta=self._rev_summary
+ else:
+ self._meta=self._rev_summary.groupby(self._reg_cols)
+ self._meta=(
+ self._meta[SupplyCurveField.TIMEZONE]
+ .apply(lambdax:stats.mode(x,keepdims=True).mode[0])
+ )
+ self._meta=self._meta.reset_index()
+
+ self._meta["rep_gen_gid"]=None
+ self._meta["rep_res_gid"]=None
+
+ def_get_mask(self,region_dict):
+"""Get the mask for a given region and res class.
+
+ Parameters
+ ----------
+ region_dict : dict
+ Column-value pairs to filter the rev summary on.
+
+ Returns
+ -------
+ mask : np.ndarray
+ Boolean mask to filter rev_summary to the appropriate
+ region_dict values.
+ """
+ mask=None
+ fork,vinregion_dict.items():
+ temp=self._rev_summary[k]==v
+ ifmaskisNone:
+ mask=temp
+ else:
+ mask=mask&temp
+
+ returnmask
+
+ def_run_serial(self):
+"""Compute all representative profiles in serial."""
+
+ logger.info(
+ "Running {} rep profile calculations in serial.".format(
+ len(self.meta)
+ )
+ )
+ meta_static=deepcopy(self.meta)
+ fori,rowinmeta_static.iterrows():
+ region_dict={
+ k:vfor(k,v)inrow.to_dict().items()ifkinself._reg_cols
+ }
+ mask=self._get_mask(region_dict)
+
+ ifnotany(mask):
+ logger.warning(
+ "Skipping profile {} out of {} "
+ "for region: {} with no valid mask.".format(
+ i+1,len(meta_static),region_dict
+ )
+ )
+ else:
+ logger.debug(
+ "Working on profile {} out of {} for region: {}".format(
+ i+1,len(meta_static),region_dict
+ )
+ )
+ out=RegionRepProfile.get_region_rep_profile(
+ self._gen_fpath,
+ self._rev_summary[mask],
+ cf_dset=self._cf_dset,
+ rep_method=self._rep_method,
+ err_method=self._err_method,
+ weight=self._weight,
+ n_profiles=self._n_profiles,
+ )
+ profiles,_,ggids,rgids=out
+ logger.info(
+ "Profile {} out of {} complete ""for region: {}".format(
+ i+1,len(meta_static),region_dict
+ )
+ )
+
+ forninrange(profiles.shape[1]):
+ self._profiles[n][:,i]=profiles[:,n]
+
+ ifggidsisNone:
+ self._meta.at[i,"rep_gen_gid"]=None
+ self._meta.at[i,"rep_res_gid"]=None
+ eliflen(ggids)==1:
+ self._meta.at[i,"rep_gen_gid"]=ggids[0]
+ self._meta.at[i,"rep_res_gid"]=rgids[0]
+ else:
+ self._meta.at[i,"rep_gen_gid"]=str(ggids)
+ self._meta.at[i,"rep_res_gid"]=str(rgids)
+
+ def_run_parallel(self,max_workers=None,pool_size=72):
+"""Compute all representative profiles in parallel.
+
+ Parameters
+ ----------
+ max_workers : int | None
+ Number of parallel workers. 1 will run serial, None will use all
+ available.
+ pool_size : int
+ Number of futures to submit to a single process pool for
+ parallel futures.
+ """
+
+ logger.info(
+ "Kicking off {} rep profile futures.".format(len(self.meta))
+ )
+
+ iter_chunks=np.array_split(
+ self.meta.index.values,np.ceil(len(self.meta)/pool_size)
+ )
+ n_complete=0
+ foriter_chunkiniter_chunks:
+ logger.debug("Starting process pool...")
+ futures={}
+ loggers=[__name__,"reV"]
+ withSpawnProcessPool(
+ max_workers=max_workers,loggers=loggers
+ )asexe:
+ foriiniter_chunk:
+ row=self.meta.loc[i,:]
+ region_dict={
+ k:v
+ for(k,v)inrow.to_dict().items()
+ ifkinself._reg_cols
+ }
+
+ mask=self._get_mask(region_dict)
+
+ ifnotany(mask):
+ logger.info(
+ "Skipping profile {} out of {} "
+ "for region: {} with no valid mask.".format(
+ i+1,len(self.meta),region_dict
+ )
+ )
+ else:
+ future=exe.submit(
+ RegionRepProfile.get_region_rep_profile,
+ self._gen_fpath,
+ self._rev_summary[mask],
+ cf_dset=self._cf_dset,
+ rep_method=self._rep_method,
+ err_method=self._err_method,
+ weight=self._weight,
+ n_profiles=self._n_profiles,
+ )
+
+ futures[future]=[i,region_dict]
+
+ forfutureinas_completed(futures):
+ i,region_dict=futures[future]
+ profiles,_,ggids,rgids=future.result()
+ n_complete+=1
+ logger.info(
+ "Future {} out of {} complete "
+ "for region: {}".format(
+ n_complete,len(self.meta),region_dict
+ )
+ )
+ log_mem(logger,log_level="DEBUG")
+
+ forninrange(profiles.shape[1]):
+ self._profiles[n][:,i]=profiles[:,n]
+
+ ifggidsisNone:
+ self._meta.at[i,"rep_gen_gid"]=None
+ self._meta.at[i,"rep_res_gid"]=None
+ eliflen(ggids)==1:
+ self._meta.at[i,"rep_gen_gid"]=ggids[0]
+ self._meta.at[i,"rep_res_gid"]=rgids[0]
+ else:
+ self._meta.at[i,"rep_gen_gid"]=str(ggids)
+ self._meta.at[i,"rep_res_gid"]=str(rgids)
+
+
[docs]defrun(
+ self,
+ fout=None,
+ save_rev_summary=True,
+ scaled_precision=False,
+ max_workers=None,
+ ):
+"""
+ Run representative profiles in serial or parallel and save to disc
+
+ Parameters
+ ----------
+ fout : str, optional
+ Filepath to output HDF5 file. If ``None``, output data are
+ not written to a file. By default, ``None``.
+ save_rev_summary : bool, optional
+ Flag to save full ``reV`` supply curve table to rep profile
+ output. By default, ``True``.
+ scaled_precision : bool, optional
+ Flag to scale `cf_profiles` by 1000 and save as uint16.
+ By default, ``False``.
+ max_workers : int, optional
+ Number of parallel rep profile workers. ``1`` will run
+ serial, while ``None`` will use all available.
+ By default, ``None``.
+ """
+
+ ifmax_workers==1:
+ self._run_serial()
+ else:
+ self._run_parallel(max_workers=max_workers)
+
+ iffoutisnotNone:
+ ifself._aggregate_profiles:
+ logger.info(
+ "Aggregate profiles input set to `True`. Setting "
+ "'save_rev_summary' input to `False`"
+ )
+ save_rev_summary=False
+ self.save_profiles(
+ fout,
+ save_rev_summary=save_rev_summary,
+ scaled_precision=scaled_precision,
+ )
+
+ logger.info("Representative profiles complete!")
+
+ returnfout
[docs]classAggFileHandler(AbstractAggFileHandler):
+"""
+ Framework to handle aggregation file context manager:
+ - exclusions .h5 file
+ - h5 file to be aggregated
+ """
+
+ DEFAULT_H5_HANDLER=Resource
+
+ def__init__(
+ self,
+ excl_fpath,
+ h5_fpath,
+ excl_dict=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ h5_handler=None,
+ ):
+"""
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ h5_fpath : str
+ Filepath to .h5 file to be aggregated
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ by default None
+ area_filter_kernel : str, optional
+ Contiguous area filter method to use on final exclusions mask,
+ by default 'queen'
+ min_area : float, optional
+ Minimum required contiguous area filter in sq-km, by default None
+ h5_handler : rex.Resource | None
+ Optional special handler similar to the rex.Resource handler which
+ is default.
+ """
+ super().__init__(
+ excl_fpath,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ )
+
+ ifh5_handlerisNone:
+ self._h5=Resource(h5_fpath)
+ else:
+ self._h5=h5_handler(h5_fpath)
+
+ @property
+ defh5(self):
+"""
+ Get the h5 file handler object.
+
+ Returns
+ -------
+ _h5 : Outputs
+ reV h5 outputs handler object.
+ """
+ returnself._h5
+
+
[docs]defclose(self):
+"""Close all file handlers."""
+ self._excl.close()
+ self._h5.close()
+
+
+
[docs]classBaseAggregation(ABC):
+"""Abstract supply curve points aggregation framework based on only an
+ exclusion file and techmap."""
+
+ def__init__(
+ self,
+ excl_fpath,
+ tm_dset,
+ excl_dict=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ resolution=64,
+ excl_area=None,
+ res_fpath=None,
+ gids=None,
+ pre_extract_inclusions=False,
+ ):
+"""
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ by default None
+ area_filter_kernel : str, optional
+ Contiguous area filter method to use on final exclusions mask,
+ by default "queen"
+ min_area : float, optional
+ Minimum required contiguous area filter in sq-km,
+ by default None
+ resolution : int, optional
+ SC resolution, must be input in combination with gid. Prefered
+ option is to use the row/col slices to define the SC point instead,
+ by default None
+ excl_area : float, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ gids : list, optional
+ List of supply curve point gids to get summary for (can use to
+ subset if running in parallel), or None for all gids in the SC
+ extent, by default None
+ pre_extract_inclusions : bool, optional
+ Optional flag to pre-extract/compute the inclusion mask from the
+ provided excl_dict, by default False. Typically faster to compute
+ the inclusion mask on the fly with parallel workers.
+ """
+ self._excl_fpath=excl_fpath
+ self._tm_dset=tm_dset
+ self._excl_dict=excl_dict
+ self._resolution=resolution
+ self._area_filter_kernel=area_filter_kernel
+ self._min_area=min_area
+ self._res_fpath=res_fpath
+ self._gids=gids
+ self._pre_extract_inclusions=pre_extract_inclusions
+ self._excl_area=self._get_excl_area(excl_fpath,excl_area=excl_area)
+ self._shape=None
+
+ self._validate_tech_mapping()
+
+ ifpre_extract_inclusions:
+ self._inclusion_mask=(
+ ExclusionMaskFromDict.extract_inclusion_mask(
+ excl_fpath,
+ tm_dset,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ )
+ )
+ else:
+ self._inclusion_mask=None
+
+ def_validate_tech_mapping(self):
+"""Check that tech mapping exists and create it if it doesn't"""
+
+ withExclusionLayers(self._excl_fpath)asf:
+ dsets=f.h5.dsets
+
+ excl_fp_is_str=isinstance(self._excl_fpath,str)
+ tm_in_excl=self._tm_dsetindsets
+ iftm_in_excl:
+ logger.info('Found techmap "{}".'.format(self._tm_dset))
+ elifnottm_in_exclandnotexcl_fp_is_str:
+ msg=(
+ 'Could not find techmap dataset "{}" and cannot run '
+ "techmap with arbitrary multiple exclusion filepaths "
+ "to write to: {}".format(self._tm_dset,self._excl_fpath)
+ )
+ logger.error(msg)
+ raiseRuntimeError(msg)
+ else:
+ logger.info(
+ 'Could not find techmap "{}". Running techmap module.'.format(
+ self._tm_dset
+ )
+ )
+ try:
+ TechMapping.run(
+ self._excl_fpath,self._res_fpath,dset=self._tm_dset
+ )
+ exceptExceptionase:
+ msg=(
+ "TechMapping process failed. Received the "
+ "following error:\n{}".format(e)
+ )
+ logger.exception(msg)
+ raiseRuntimeError(msg)frome
+
+ @property
+ defgids(self):
+"""
+ 1D array of supply curve point gids to aggregate
+
+ Returns
+ -------
+ ndarray
+ """
+ ifself._gidsisNone:
+ withSupplyCurveExtent(
+ self._excl_fpath,resolution=self._resolution
+ )assc:
+ self._gids=sc.valid_sc_points(self._tm_dset)
+ elifnp.issubdtype(type(self._gids),np.number):
+ self._gids=np.array([self._gids])
+ elifnotisinstance(self._gids,np.ndarray):
+ self._gids=np.array(self._gids)
+
+ returnself._gids
+
+ @property
+ defshape(self):
+"""Get the shape of the full exclusions raster.
+
+ Returns
+ -------
+ tuple
+ """
+ ifself._shapeisNone:
+ withSupplyCurveExtent(
+ self._excl_fpath,resolution=self._resolution
+ )assc:
+ self._shape=sc.exclusions.shape
+
+ returnself._shape
+
+ @staticmethod
+ def_get_excl_area(excl_fpath,excl_area=None):
+"""
+ Get exclusion area from excl_fpath pixel area. Confirm that the
+ exclusion area is not None.
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ excl_area : float, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+
+ Returns
+ -------
+ excl_area : float
+ Area of an exclusion pixel in km2
+ """
+ ifexcl_areaisNone:
+ logger.debug(
+ "Setting the exclusion area from the area of a pixel "
+ "in {}".format(excl_fpath)
+ )
+ withExclusionLayers(excl_fpath)asexcl:
+ excl_area=excl.pixel_area
+
+ ifexcl_areaisNone:
+ e=(
+ "No exclusion pixel area was input and could not parse "
+ "area from the exclusion file attributes!"
+ )
+ logger.error(e)
+ raiseSupplyCurveInputError(e)
+
+ returnexcl_area
+
+ @staticmethod
+ def_check_inclusion_mask(inclusion_mask,gids,excl_shape):
+"""
+ Check inclusion mask to ensure it has the proper shape
+
+ Parameters
+ ----------
+ inclusion_mask : np.ndarray | dict | optional
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. This must be either match the full exclusion shape or
+ be a dict lookup of single-sc-point exclusion masks corresponding
+ to the gids input and keyed by gids, by default None which will
+ calculate exclusions on the fly for each sc point.
+ gids : list | ndarray
+ sc point gids corresponding to inclusion mask
+ excl_shape : tuple
+ Full exclusion layers shape
+ """
+ ifisinstance(inclusion_mask,dict):
+ assertlen(inclusion_mask)==len(gids)
+ elifisinstance(inclusion_mask,np.ndarray):
+ assertinclusion_mask.shape==excl_shape
+ elifinclusion_maskisnotNone:
+ msg=(
+ "Expected inclusion_mask to be dict or array but received "
+ "{}".format(type(inclusion_mask))
+ )
+ logger.error(msg)
+ raiseSupplyCurveInputError(msg)
+
+ @staticmethod
+ def_get_gid_inclusion_mask(
+ inclusion_mask,gid,slice_lookup,resolution=64
+ ):
+"""
+ Get inclusion mask for desired gid
+
+ Parameters
+ ----------
+ inclusion_mask : np.ndarray | dict | optional
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. This must be either match the full exclusion shape or
+ be a dict lookup of single-sc-point exclusion masks corresponding
+ to the gids input and keyed by gids, by default None which will
+ calculate exclusions on the fly for each sc point.
+ gid : int
+ sc_point_gid value, used to extract inclusion mask from 2D
+ inclusion array
+ slice_lookup : dict
+ Mapping of sc_point_gids to exclusion/inclusion row and column
+ slices
+ resolution : int, optional
+ supply curve extent resolution, by default 64
+
+ Returns
+ -------
+ gid_inclusions : ndarray | None
+ 2D array of inclusions for desired gid, normalized from 0, excluded
+ to 1 fully included, if inclusion mask is None gid_inclusions
+ is None
+ """
+ gid_inclusions=None
+ ifisinstance(inclusion_mask,dict):
+ gid_inclusions=inclusion_mask[gid]
+ assertgid_inclusions.shape[0]<=resolution
+ assertgid_inclusions.shape[1]<=resolution
+ elifisinstance(inclusion_mask,np.ndarray):
+ row_slice,col_slice=slice_lookup[gid]
+ gid_inclusions=inclusion_mask[row_slice,col_slice]
+ elifinclusion_maskisnotNone:
+ msg=(
+ "Expected inclusion_mask to be dict or array but received "
+ "{}".format(type(inclusion_mask))
+ )
+ logger.error(msg)
+ raiseSupplyCurveInputError(msg)
+
+ returngid_inclusions
+
+ @staticmethod
+ def_parse_gen_index(gen_fpath):
+"""Parse gen outputs for an array of generation gids corresponding to
+ the resource gids.
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to reV generation output .h5 file. This can also be a csv
+ filepath to a project points input file.
+
+ Returns
+ -------
+ gen_index : np.ndarray
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run.
+ """
+
+ ifgen_fpath.endswith(".h5"):
+ withResource(gen_fpath)asf:
+ gen_index=f.meta
+ elifgen_fpath.endswith(".csv"):
+ gen_index=pd.read_csv(gen_fpath)
+ else:
+ msg=(
+ "Could not recognize gen_fpath input, needs to be reV gen "
+ "output h5 or project points csv but received: {}".format(
+ gen_fpath
+ )
+ )
+ logger.error(msg)
+ raiseFileInputError(msg)
+
+ ifResourceMetaField.GIDingen_index:
+ gen_index=gen_index.rename(
+ columns={ResourceMetaField.GID:SupplyCurveField.RES_GIDS}
+ )
+ gen_index[SupplyCurveField.GEN_GIDS]=gen_index.index
+ gen_index=gen_index[
+ [SupplyCurveField.RES_GIDS,SupplyCurveField.GEN_GIDS]
+ ]
+ gen_index=gen_index.set_index(keys=SupplyCurveField.RES_GIDS)
+ gen_index=gen_index.reindex(
+ range(int(gen_index.index.max()+1))
+ )
+ gen_index=gen_index[SupplyCurveField.GEN_GIDS].values
+ gen_index[np.isnan(gen_index)]=-1
+ gen_index=gen_index.astype(np.int32)
+ else:
+ gen_index=None
+
+ returngen_index
+
+
+
[docs]classAggregation(BaseAggregation):
+"""Concrete but generalized aggregation framework to aggregate ANY reV h5
+ file to a supply curve grid (based on an aggregated exclusion grid)."""
+
+ def__init__(
+ self,
+ excl_fpath,
+ tm_dset,
+ *agg_dset,
+ excl_dict=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ resolution=64,
+ excl_area=None,
+ gids=None,
+ pre_extract_inclusions=False,
+ ):
+"""
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ agg_dset : str
+ Dataset to aggreate, can supply multiple datasets. The datasets
+ should be scalar values for each site. This method cannot aggregate
+ timeseries data.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ by default None
+ area_filter_kernel : str, optional
+ Contiguous area filter method to use on final exclusions mask,
+ by default "queen"
+ min_area : float, optional
+ Minimum required contiguous area filter in sq-km,
+ by default None
+ resolution : int, optional
+ SC resolution, must be input in combination with gid. Prefered
+ option is to use the row/col slices to define the SC point instead,
+ by default None
+ excl_area : float, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath,
+ by default None
+ gids : list, optional
+ List of supply curve point gids to get summary for (can use to
+ subset if running in parallel), or None for all gids in the SC
+ extent, by default None
+ pre_extract_inclusions : bool, optional
+ Optional flag to pre-extract/compute the inclusion mask from the
+ provided excl_dict, by default False. Typically faster to compute
+ the inclusion mask on the fly with parallel workers.
+ """
+ log_versions(logger)
+ logger.info("Initializing Aggregation...")
+ logger.debug("Exclusion filepath: {}".format(excl_fpath))
+ logger.debug("Exclusion dict: {}".format(excl_dict))
+
+ super().__init__(
+ excl_fpath,
+ tm_dset,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ resolution=resolution,
+ excl_area=excl_area,
+ gids=gids,
+ pre_extract_inclusions=pre_extract_inclusions,
+ )
+
+ ifisinstance(agg_dset,str):
+ agg_dset=(agg_dset,)
+
+ self._agg_dsets=agg_dset
+
+ def_check_files(self,h5_fpath):
+"""Do a preflight check on input files"""
+
+ ifnotos.path.exists(self._excl_fpath):
+ raiseFileNotFoundError(
+ "Could not find required exclusions file: ""{}".format(
+ self._excl_fpath
+ )
+ )
+
+ ifnotos.path.exists(h5_fpath):
+ raiseFileNotFoundError(
+ "Could not find required h5 file: ""{}".format(h5_fpath)
+ )
+
+ withh5py.File(self._excl_fpath,"r")asf:
+ ifself._tm_dsetnotinf:
+ raiseFileInputError(
+ 'Could not find techmap dataset "{}" '
+ "in exclusions file: {}".format(
+ self._tm_dset,self._excl_fpath
+ )
+ )
+
+ withResource(h5_fpath)asf:
+ fordsetinself._agg_dsets:
+ ifdsetnotinf:
+ raiseFileInputError(
+ 'Could not find provided dataset "{}"'
+ " in h5 file: {}".format(dset,h5_fpath)
+ )
+
+
[docs]@classmethod
+ defrun_serial(
+ cls,
+ excl_fpath,
+ h5_fpath,
+ tm_dset,
+ *agg_dset,
+ agg_method="mean",
+ excl_dict=None,
+ inclusion_mask=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ resolution=64,
+ excl_area=0.0081,
+ gids=None,
+ gen_index=None,
+ ):
+"""
+ Standalone method to aggregate - can be parallelized.
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ h5_fpath : str
+ Filepath to .h5 file to aggregate
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ agg_dset : str
+ Dataset to aggreate, can supply multiple datasets. The datasets
+ should be scalar values for each site. This method cannot aggregate
+ timeseries data.
+ agg_method : str, optional
+ Aggregation method, either mean or sum/aggregate, by default "mean"
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ by default None
+ inclusion_mask : np.ndarray, optional
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. This must be either match the full exclusion shape or
+ be a list of single-sc-point exclusion masks corresponding to the
+ gids input, by default None
+ area_filter_kernel : str, optional
+ Contiguous area filter method to use on final exclusions mask,
+ by default "queen"
+ min_area : float, optional
+ Minimum required contiguous area filter in sq-km,
+ by default None
+ resolution : int, optional
+ SC resolution, must be input in combination with gid. Prefered
+ option is to use the row/col slices to define the SC point instead,
+ by default 0.0081
+ excl_area : float, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath,
+ by default None
+ gids : list, optional
+ List of supply curve point gids to get summary for (can use to
+ subset if running in parallel), or None for all gids in the SC
+ extent, by default None
+ gen_index : np.ndarray, optional
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run, by default None
+
+ Returns
+ -------
+ agg_out : dict
+ Aggregated values for each aggregation dataset
+ """
+ withSupplyCurveExtent(excl_fpath,resolution=resolution)assc:
+ exclusion_shape=sc.exclusions.shape
+ ifgidsisNone:
+ gids=sc.valid_sc_points(tm_dset)
+ elifnp.issubdtype(type(gids),np.number):
+ gids=[gids]
+
+ slice_lookup=sc.get_slice_lookup(gids)
+
+ cls._check_inclusion_mask(inclusion_mask,gids,exclusion_shape)
+
+ # pre-extract handlers so they are not repeatedly initialized
+ file_kwargs={
+ "excl_dict":excl_dict,
+ "area_filter_kernel":area_filter_kernel,
+ "min_area":min_area,
+ }
+ dsets=(
+ *agg_dset,
+ "meta",
+ )
+ agg_out={ds:[]fordsindsets}
+ withAggFileHandler(excl_fpath,h5_fpath,**file_kwargs)asfh:
+ n_finished=0
+ forgidingids:
+ gid_inclusions=cls._get_gid_inclusion_mask(
+ inclusion_mask,gid,slice_lookup,resolution=resolution
+ )
+ try:
+ gid_out=AggregationSupplyCurvePoint.run(
+ gid,
+ fh.exclusions,
+ fh.h5,
+ tm_dset,
+ *agg_dset,
+ agg_method=agg_method,
+ excl_dict=excl_dict,
+ inclusion_mask=gid_inclusions,
+ resolution=resolution,
+ excl_area=excl_area,
+ exclusion_shape=exclusion_shape,
+ close=False,
+ gen_index=gen_index,
+ )
+
+ exceptEmptySupplyCurvePointError:
+ logger.debug(
+ "SC gid {} is fully excluded or does not "
+ "have any valid source data!".format(gid)
+ )
+ exceptExceptionase:
+ msg="SC gid {} failed!".format(gid)
+ logger.exception(msg)
+ raiseRuntimeError(msg)frome
+ else:
+ n_finished+=1
+ logger.debug(
+ "Serial aggregation: "
+ "{} out of {} points complete".format(
+ n_finished,len(gids)
+ )
+ )
+ log_mem(logger)
+ fork,vingid_out.items():
+ agg_out[k].append(v)
+
+ returnagg_out
+
+
[docs]defrun_parallel(
+ self,
+ h5_fpath,
+ agg_method="mean",
+ excl_area=None,
+ max_workers=None,
+ sites_per_worker=100,
+ ):
+"""
+ Aggregate in parallel
+
+ Parameters
+ ----------
+ h5_fpath : str
+ Filepath to .h5 file to aggregate
+ agg_method : str, optional
+ Aggregation method, either mean or sum/aggregate, by default "mean"
+ excl_area : float, optional
+ Area of an exclusion cell (square km), by default None
+ max_workers : int, optional
+ Number of cores to run summary on. None is all available cpus,
+ by default None
+ sites_per_worker : int, optional
+ Number of SC points to process on a single parallel worker,
+ by default 100
+
+ Returns
+ -------
+ agg_out : dict
+ Aggregated values for each aggregation dataset
+ """
+
+ self._check_files(h5_fpath)
+ gen_index=self._parse_gen_index(h5_fpath)
+
+ slice_lookup=None
+ chunks=int(np.ceil(len(self.gids)/sites_per_worker))
+ chunks=np.array_split(self.gids,chunks)
+
+ ifself._inclusion_maskisnotNone:
+ withSupplyCurveExtent(
+ self._excl_fpath,resolution=self._resolution
+ )assc:
+ assertsc.exclusions.shape==self._inclusion_mask.shape
+ slice_lookup=sc.get_slice_lookup(self.gids)
+
+ logger.info(
+ "Running supply curve point aggregation for "
+ "points {} through {} at a resolution of {} "
+ "on {} cores in {} chunks.".format(
+ self.gids[0],
+ self.gids[-1],
+ self._resolution,
+ max_workers,
+ len(chunks),
+ )
+ )
+
+ n_finished=0
+ futures=[]
+ dsets=self._agg_dsets+("meta",)
+ agg_out={ds:[]fordsindsets}
+ loggers=[__name__,"reV.supply_curve.points","reV"]
+ withSpawnProcessPool(max_workers=max_workers,loggers=loggers)asexe:
+ # iterate through split executions, submitting each to worker
+ forgid_setinchunks:
+ # submit executions and append to futures list
+ chunk_incl_masks=None
+ ifself._inclusion_maskisnotNone:
+ chunk_incl_masks={}
+ forgidingid_set:
+ rs,cs=slice_lookup[gid]
+ chunk_incl_masks[gid]=self._inclusion_mask[rs,cs]
+
+ # submit executions and append to futures list
+ futures.append(
+ exe.submit(
+ self.run_serial,
+ self._excl_fpath,
+ h5_fpath,
+ self._tm_dset,
+ *self._agg_dsets,
+ agg_method=agg_method,
+ excl_dict=self._excl_dict,
+ inclusion_mask=chunk_incl_masks,
+ area_filter_kernel=self._area_filter_kernel,
+ min_area=self._min_area,
+ resolution=self._resolution,
+ excl_area=excl_area,
+ gids=gid_set,
+ gen_index=gen_index,
+ )
+ )
+
+ # gather results
+ forfutureinfutures:
+ n_finished+=1
+ logger.info(
+ "Parallel aggregation futures collected: "
+ "{} out of {}".format(n_finished,len(chunks))
+ )
+ fork,vinfuture.result().items():
+ ifv:
+ agg_out[k].extend(v)
+
+ returnagg_out
+
+
[docs]defaggregate(
+ self,
+ h5_fpath,
+ agg_method="mean",
+ max_workers=None,
+ sites_per_worker=100,
+ ):
+"""
+ Aggregate with given agg_method
+
+ Parameters
+ ----------
+ h5_fpath : str
+ Filepath to .h5 file to aggregate
+ agg_method : str, optional
+ Aggregation method, either mean or sum/aggregate, by default "mean"
+ max_workers : int, optional
+ Number of cores to run summary on. None is all available cpus,
+ by default None
+ sites_per_worker : int, optional
+ Number of SC points to process on a single parallel worker,
+ by default 100
+
+ Returns
+ -------
+ agg : dict
+ Aggregated values for each aggregation dataset
+ """
+ ifmax_workersisNone:
+ max_workers=os.cpu_count()
+
+ ifmax_workers==1:
+ self._check_files(h5_fpath)
+ gen_index=self._parse_gen_index(h5_fpath)
+ agg=self.run_serial(
+ self._excl_fpath,
+ h5_fpath,
+ self._tm_dset,
+ *self._agg_dsets,
+ agg_method=agg_method,
+ excl_dict=self._excl_dict,
+ gids=self.gids,
+ inclusion_mask=self._inclusion_mask,
+ area_filter_kernel=self._area_filter_kernel,
+ min_area=self._min_area,
+ resolution=self._resolution,
+ excl_area=self._excl_area,
+ gen_index=gen_index,
+ )
+ else:
+ agg=self.run_parallel(
+ h5_fpath=h5_fpath,
+ agg_method=agg_method,
+ excl_area=self._excl_area,
+ max_workers=max_workers,
+ sites_per_worker=sites_per_worker,
+ )
+
+ ifnotagg["meta"]:
+ e=(
+ "Supply curve aggregation found no non-excluded SC points. "
+ "Please check your exclusions or subset SC GID selection."
+ )
+ logger.error(e)
+ raiseEmptySupplyCurvePointError(e)
+
+ fork,vinagg.items():
+ ifk=="meta":
+ v=pd.concat(v,axis=1).T
+ v=v.sort_values(SupplyCurveField.SC_POINT_GID)
+ v=v.reset_index(drop=True)
+ v.index.name=SupplyCurveField.SC_GID
+ agg[k]=v
+ else:
+ v=np.dstack(v)[0]
+ ifv.shape[0]==1:
+ v=v.flatten()
+
+ agg[k]=v
+
+ returnagg
[docs]@classmethod
+ defrun(
+ cls,
+ excl_fpath,
+ h5_fpath,
+ tm_dset,
+ *agg_dset,
+ excl_dict=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ resolution=64,
+ excl_area=None,
+ gids=None,
+ pre_extract_inclusions=False,
+ agg_method="mean",
+ max_workers=None,
+ sites_per_worker=100,
+ out_fpath=None,
+ ):
+"""Get the supply curve points aggregation summary.
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ h5_fpath : str
+ Filepath to .h5 file to aggregate
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ agg_dset : str
+ Dataset to aggreate, can supply multiple datasets. The datasets
+ should be scalar values for each site. This method cannot aggregate
+ timeseries data.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ by default None
+ area_filter_kernel : str, optional
+ Contiguous area filter method to use on final exclusions mask,
+ by default "queen"
+ min_area : float, optional
+ Minimum required contiguous area filter in sq-km,
+ by default None
+ resolution : int, optional
+ SC resolution, must be input in combination with gid. Prefered
+ option is to use the row/col slices to define the SC point instead,
+ by default None
+ excl_area : float, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath,
+ by default None
+ gids : list, optional
+ List of supply curve point gids to get summary for (can use to
+ subset if running in parallel), or None for all gids in the SC
+ extent, by default None
+ pre_extract_inclusions : bool, optional
+ Optional flag to pre-extract/compute the inclusion mask from the
+ provided excl_dict, by default False. Typically faster to compute
+ the inclusion mask on the fly with parallel workers.
+ agg_method : str, optional
+ Aggregation method, either mean or sum/aggregate, by default "mean"
+ max_workers : int, optional
+ Number of cores to run summary on. None is all available cpus,
+ by default None
+ sites_per_worker : int, optional
+ Number of SC points to process on a single parallel worker,
+ by default 100
+ out_fpath : str, optional
+ Output .h5 file path, by default None
+
+ Returns
+ -------
+ agg : dict
+ Aggregated values for each aggregation dataset
+ """
+
+ agg=cls(
+ excl_fpath,
+ tm_dset,
+ *agg_dset,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ resolution=resolution,
+ excl_area=excl_area,
+ gids=gids,
+ pre_extract_inclusions=pre_extract_inclusions,
+ )
+
+ aggregation=agg.aggregate(
+ h5_fpath=h5_fpath,
+ agg_method=agg_method,
+ max_workers=max_workers,
+ sites_per_worker=sites_per_worker,
+ )
+
+ ifout_fpathisnotNone:
+ agg.save_agg_to_h5(h5_fpath,out_fpath,aggregation)
+
+ returnaggregation
[docs]classCompetitiveWindFarms:
+"""
+ Handle competitive wind farm exclusion during supply curve sorting
+ """
+
+ def__init__(self,wind_dirs,sc_points,n_dirs=2,offshore=False):
+"""
+ Parameters
+ ----------
+ wind_dirs : pandas.DataFrame | str
+ path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+ the neighboring supply curve point gids and power-rose value at
+ each cardinal direction
+ sc_points : pandas.DataFrame | str
+ Supply curve point summary table
+ n_dirs : int, optional
+ Number of prominent directions to use, by default 2
+ offshore : bool
+ Flag as to whether offshore farms should be included during
+ CompetitiveWindFarms
+ """
+ self._wind_dirs=self._parse_wind_dirs(wind_dirs)
+
+ self._sc_gids,self._sc_point_gids,self._mask=self._parse_sc_points(
+ sc_points,offshore=offshore
+ )
+
+ self._offshore=offshore
+
+ valid=np.isin(self.sc_point_gids,self._wind_dirs.index)
+ ifnotnp.all(valid):
+ msg=(
+ "'sc_points contains sc_point_gid values that do not "
+ "correspond to valid 'wind_dirs' sc_point_gids:\n{}".format(
+ self.sc_point_gids[~valid]
+ )
+ )
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ mask=self._wind_dirs.index.isin(self._sc_point_gids.keys())
+ self._wind_dirs=self._wind_dirs.loc[mask]
+ self._upwind,self._downwind=self._get_neighbors(
+ self._wind_dirs,n_dirs=n_dirs
+ )
+
+ def__repr__(self):
+ gids=len(self._upwind)
+ # pylint: disable=unsubscriptable-object
+ neighbors=len(self._upwind.values[0])
+ msg="{} with {} sc_point_gids and {} prominent directions".format(
+ self.__class__.__name__,gids,neighbors
+ )
+
+ returnmsg
+
+ def__getitem__(self,keys):
+"""
+ Map gid for given mapping
+
+ Parameters
+ ----------
+ keys : tuple
+ (gid(s) to extract, gid) pair
+
+ Returns
+ -------
+ gid(s) : int | list
+ Mapped gid(s) for given mapping
+ """
+ ifnotisinstance(keys,tuple):
+ msg=("{} must be a tuple of form (source, gid) where source is: "
+ "{}, '{}', or 'upwind', 'downwind'"
+ .format(keys,SupplyCurveField.SC_GID,
+ SupplyCurveField.SC_POINT_GID))
+ logger.error(msg)
+ raiseValueError(msg)
+
+ source,gid=keys
+ ifsource==SupplyCurveField.SC_POINT_GID:
+ out=self.map_sc_gid_to_sc_point_gid(gid)
+ elifsource==SupplyCurveField.SC_GID:
+ out=self.map_sc_point_gid_to_sc_gid(gid)
+ elifsource=="upwind":
+ out=self.map_upwind(gid)
+ elifsource=="downwind":
+ out=self.map_downwind(gid)
+ else:
+ msg=("{} must be: {}, {}, or 'upwind', "
+ "'downwind'".format(source,SupplyCurveField.SC_GID,
+ SupplyCurveField.SC_POINT_GID))
+ logger.error(msg)
+ raiseValueError(msg)
+
+ returnout
+
+ @property
+ defmask(self):
+"""
+ Supply curve point boolean mask, used for efficient exclusion
+ False == excluded sc_point_gid
+
+ Returns
+ -------
+ ndarray
+ """
+ returnself._mask
+
+ @property
+ defsc_point_gids(self):
+"""
+ Un-masked sc_point_gids
+
+ Returns
+ -------
+ ndarray
+ """
+ sc_point_gids=np.array(list(self._sc_point_gids.keys()),dtype=int)
+ mask=self.mask[sc_point_gids]
+
+ returnsc_point_gids[mask]
+
+ @property
+ defsc_gids(self):
+"""
+ Un-masked sc_gids
+
+ Returns
+ -------
+ ndarray
+ """
+ sc_gids=np.concatenate(
+ [self._sc_point_gids[gid]forgidinself.sc_point_gids]
+ )
+
+ returnsc_gids
+
+ @staticmethod
+ def_parse_table(table):
+"""
+ Extract features and their capacity from supply curve transmission
+ mapping table
+
+ Parameters
+ ----------
+ table : str | pd.DataFrame
+ Path to .csv or .json or DataFrame to parse
+
+ Returns
+ -------
+ table : pandas.DataFrame
+ DataFrame extracted from file path
+ """
+ try:
+ table=parse_table(table)
+ exceptValueErrorasex:
+ logger.error(ex)
+ raise
+
+ returntable
+
+ @classmethod
+ def_parse_wind_dirs(cls,wind_dirs):
+"""
+ Parse prominent direction neighbors
+
+ Parameters
+ ----------
+ wind_dirs : pandas.DataFrame | str
+ Neighboring supply curve point gids and power-rose value at each
+ cardinal direction
+
+ Returns
+ -------
+ wind_dirs : pandas.DataFrame
+ Neighboring supply curve point gids and power-rose value at each
+ cardinal direction for each sc point gid
+ """
+ wind_dirs=cls._parse_table(wind_dirs)
+ wind_dirs=wind_dirs.rename(
+ columns=SupplyCurveField.map_from_legacy())
+
+ wind_dirs=wind_dirs.set_index(SupplyCurveField.SC_POINT_GID)
+ columns=[cforcinwind_dirsifc.endswith(('_gid','_pr'))]
+ wind_dirs=wind_dirs[columns]
+
+ returnwind_dirs
+
+ @classmethod
+ def_parse_sc_points(cls,sc_points,offshore=False):
+"""
+ Parse supply curve point summary table into sc_gid to sc_point_gid
+ mapping and vis-versa.
+
+ Parameters
+ ----------
+ sc_points : pandas.DataFrame | str
+ Supply curve point summary table
+ offshore : bool
+ Flag as to whether offshore farms should be included during
+ CompetitiveWindFarms
+
+ Returns
+ -------
+ sc_gids : pandas.DataFrame
+ sc_gid to sc_point_gid mapping
+ sc_point_gids : pandas.DataFrame
+ sc_point_gid to sc_gid mapping
+ mask : ndarray
+ Mask array to mask excluded sc_point_gids
+ """
+ sc_points=cls._parse_table(sc_points)
+ sc_points=sc_points.rename(
+ columns=SupplyCurveField.map_from_legacy())
+ ifSupplyCurveField.OFFSHOREinsc_pointsandnotoffshore:
+ logger.debug('Not including offshore supply curve points in '
+ 'CompetitiveWindFarm')
+ mask=sc_points[SupplyCurveField.OFFSHORE]==0
+ sc_points=sc_points.loc[mask]
+
+ mask=np.ones(int(1+sc_points[SupplyCurveField.SC_POINT_GID].max()),
+ dtype=bool)
+
+ sc_points=sc_points[[SupplyCurveField.SC_GID,
+ SupplyCurveField.SC_POINT_GID]]
+ sc_gids=sc_points.set_index(SupplyCurveField.SC_GID)
+ sc_gids={k:int(v[0])fork,vinsc_gids.iterrows()}
+
+ groups=sc_points.groupby(SupplyCurveField.SC_POINT_GID)
+ sc_point_gids=groups[SupplyCurveField.SC_GID].unique().to_frame()
+ sc_point_gids={int(k):v[SupplyCurveField.SC_GID]
+ fork,vinsc_point_gids.iterrows()}
+
+ returnsc_gids,sc_point_gids,mask
+
+ @staticmethod
+ def_get_neighbors(wind_dirs,n_dirs=2):
+"""
+ Parse prominent direction neighbors
+
+ Parameters
+ ----------
+ wind_dirs : pandas.DataFrame | str
+ Neighboring supply curve point gids and power-rose value at each
+ cardinal direction for each available sc point gid
+ n_dirs : int, optional
+ Number of prominent directions to use, by default 2
+
+ Returns
+ -------
+ upwind : pandas.DataFrame
+ Upwind neighbor gids for n prominent wind directions
+ downwind : pandas.DataFrame
+ Downwind neighbor gids for n prominent wind directions
+ """
+ cols=[
+ c
+ forcinwind_dirs
+ if(c.endswith("_gid")andnotc.startswith("sc"))
+ ]
+ directions=[c.split("_")[0]forcincols]
+ upwind_gids=wind_dirs[cols].values
+
+ cols=["{}_pr".format(d)fordindirections]
+ neighbor_pr=wind_dirs[cols].values
+
+ neighbors=np.argsort(neighbor_pr)[:,:n_dirs]
+ upwind_gids=np.take_along_axis(upwind_gids,neighbors,axis=1)
+
+ downwind_map={
+ "N":"S",
+ "NE":"SW",
+ "E":"W",
+ "SE":"NW",
+ "S":"N",
+ "SW":"NE",
+ "W":"E",
+ "NW":"SE",
+ }
+ cols=["{}_gid".format(downwind_map[d])fordindirections]
+ downwind_gids=wind_dirs[cols].values
+ downwind_gids=np.take_along_axis(downwind_gids,neighbors,axis=1)
+
+ downwind={}
+ upwind={}
+ fori,gidinenumerate(wind_dirs.index.values):
+ downwind[gid]=downwind_gids[i]
+ upwind[gid]=upwind_gids[i]
+
+ returnupwind,downwind
+
+
[docs]defmap_sc_point_gid_to_sc_gid(self,sc_point_gid):
+"""
+ Map given sc_point_gid to equivalent sc_gid(s)
+
+ Parameters
+ ----------
+ sc_point_gid : int
+ Supply curve point gid to map to equivalent supply curve gid(s)
+
+ Returns
+ -------
+ int | list
+ Equivalent supply curve gid(s)
+ """
+ returnself._sc_point_gids[sc_point_gid]
+
+
[docs]defmap_sc_gid_to_sc_point_gid(self,sc_gid):
+"""
+ Map given sc_gid to equivalent sc_point_gid
+
+ Parameters
+ ----------
+ sc_gid : int
+ Supply curve gid to map to equivalent supply point curve gid
+
+ Returns
+ -------
+ int
+ Equivalent supply point curve gid
+ """
+ returnself._sc_gids[sc_gid]
+
+
[docs]defcheck_sc_gid(self,sc_gid):
+"""
+ Check to see if sc_gid is valid, if so return associated
+ sc_point_gids
+
+ Parameters
+ ----------
+ sc_gid : int
+ Supply curve gid to map to equivalent supply point curve gid
+
+ Returns
+ -------
+ int | None
+ Equivalent supply point curve gid or None if sc_gid is invalid
+ (offshore)
+ """
+ sc_point_gid=None
+ ifsc_gidinself._sc_gids:
+ sc_point_gid=self._sc_gids[sc_gid]
+
+ returnsc_point_gid
+
+
[docs]defmap_upwind(self,sc_point_gid):
+"""
+ Map given sc_point_gid to upwind neighbors
+
+ Parameters
+ ----------
+ sc_point_gid : int
+ Supply point curve gid to get upwind neighbors
+
+ Returns
+ -------
+ int | list
+ upwind neighborings
+ """
+ returnself._upwind[sc_point_gid]
+
+
[docs]defmap_downwind(self,sc_point_gid):
+"""
+ Map given sc_point_gid to downwind neighbors
+
+ Parameters
+ ----------
+ sc_point_gid : int
+ Supply point curve gid to get downwind neighbors
+
+ Returns
+ -------
+ int | list
+ downwind neighborings
+ """
+ returnself._downwind[sc_point_gid]
+
+
[docs]defexclude_sc_point_gid(self,sc_point_gid):
+"""
+ Exclude supply curve point gid, return False if gid is not present
+ in list of available gids to avoid key errors elsewhere
+
+ Parameters
+ ----------
+ sc_point_gid : int
+ supply curve point gid to mask
+
+ Returns
+ -------
+ bool
+ Flag if gid is valid and was masked
+ """
+ ifsc_point_gidinself._sc_point_gids:
+ self._mask[sc_point_gid]=False
+ out=True
+ else:
+ out=False
+
+ returnout
+
+
[docs]defremove_noncompetitive_farm(
+ self,sc_points,sort_on="total_lcoe",downwind=False
+ ):
+"""
+ Remove neighboring sc points for given number of prominent wind
+ directions
+
+ Parameters
+ ----------
+ sc_points : pandas.DataFrame | str
+ Supply curve point summary table
+ sort_on : str, optional
+ column to sort on before excluding neighbors,
+ by default 'total_lcoe'
+ downwind : bool, optional
+ Flag to remove downwind neighbors as well as upwind neighbors,
+ by default False
+
+ Returns
+ -------
+ sc_points : pandas.DataFrame
+ Updated supply curve points after removing non-competative
+ wind farms
+ """
+ sc_points=self._parse_table(sc_points)
+ sc_points=sc_points.rename(
+ columns=SupplyCurveField.map_from_legacy())
+ ifSupplyCurveField.OFFSHOREinsc_pointsandnotself._offshore:
+ mask=sc_points[SupplyCurveField.OFFSHORE]==0
+ sc_points=sc_points.loc[mask]
+
+ sc_points=sc_points.sort_values(sort_on)
+
+ sc_point_gids=sc_points[SupplyCurveField.SC_POINT_GID].values
+ sc_point_gids=sc_point_gids.astype(int)
+
+ foriinrange(len(sc_points)):
+ gid=sc_point_gids[i]
+ ifself.mask[gid]:
+ upwind_gids=self["upwind",gid]
+ forninupwind_gids:
+ self.exclude_sc_point_gid(n)
+
+ ifdownwind:
+ downwind_gids=self["downwind",gid]
+ fornindownwind_gids:
+ self.exclude_sc_point_gid(n)
+
+ sc_gids=self.sc_gids
+ mask=sc_points[SupplyCurveField.SC_GID].isin(sc_gids)
+
+ returnsc_points.loc[mask].reset_index(drop=True)
+
+
[docs]@classmethod
+ defrun(
+ cls,
+ wind_dirs,
+ sc_points,
+ n_dirs=2,
+ offshore=False,
+ sort_on="total_lcoe",
+ downwind=False,
+ out_fpath=None,
+ ):
+"""
+ Exclude given number of neighboring Supply Point gids based on most
+ prominent wind directions
+
+ Parameters
+ ----------
+ wind_dirs : pandas.DataFrame | str
+ path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+ the neighboring supply curve point gids and power-rose value at
+ each cardinal direction
+ sc_points : pandas.DataFrame | str
+ Supply curve point summary table
+ n_dirs : int, optional
+ Number of prominent directions to use, by default 2
+ offshore : bool
+ Flag as to whether offshore farms should be included during
+ CompetitiveWindFarms
+ sort_on : str, optional
+ column to sort on before excluding neighbors,
+ by default 'total_lcoe'
+ downwind : bool, optional
+ Flag to remove downwind neighbors as well as upwind neighbors,
+ by default False
+ out_fpath : str, optional
+ Path to .csv file to save updated sc_points to,
+ by default None
+
+ Returns
+ -------
+ sc_points : pandas.DataFrame
+ Updated supply curve points after removing non-competative
+ wind farms
+ """
+ cwf=cls(wind_dirs,sc_points,n_dirs=n_dirs,offshore=offshore)
+ sc_points=cwf.remove_noncompetitive_farm(
+ sc_points,sort_on=sort_on,downwind=downwind
+ )
+
+ ifout_fpathisnotNone:
+ sc_points.to_csv(out_fpath,index=False)
+
+ returnsc_points
[docs]classLayerMask:
+"""
+ Class to convert exclusion layer to inclusion layer mask
+ """
+
+ def__init__(self,layer,
+ exclude_values=None,
+ exclude_range=(None,None),
+ include_values=None,
+ include_range=(None,None),
+ include_weights=None,
+ force_include_values=None,
+ force_include_range=None,
+ use_as_weights=False,
+ weight=1.0,
+ exclude_nodata=False,
+ nodata_value=None,
+ extent=None,
+ **kwargs):
+"""
+ Parameters
+ ----------
+ layer : str
+ Layer name.
+ exclude_values : int | float | list, optional
+ Single value or list of values to exclude.
+
+ .. Important:: The keyword arguments `exclude_values`,
+ `exclude_range`, `include_values`, `include_range`,
+ `include_weights`, `force_include_values`, and
+ `force_include_range` are all mutually exclusive. Users
+ should supply value(s) for exactly one of these arguments.
+
+ By default, ``None``.
+ exclude_range : list | tuple, optional
+ Two-item list of [min threshold, max threshold] (ends are
+ inclusive) for values to exclude. Mutually exclusive
+ with other inputs (see info in the description of
+ `exclude_values`). By default, ``None``.
+ include_values : int | float | list, optional
+ Single value or list of values to include. Mutually
+ exclusive with other inputs (see info in the description of
+ `exclude_values`). By default, ``None``.
+ include_range : list | tuple, optional
+ Two-item list of [min threshold, max threshold] (ends are
+ inclusive) for values to include. Mutually exclusive with
+ other inputs (see info in the description of
+ `exclude_values`). By default, ``None``.
+ include_weights : dict, optional
+ A dictionary of ``{value: weight}`` pairs, where the
+ ``value`` in the layer that should be included with the
+ given ``weight``. Mutually exclusive with other inputs (see
+ info in the description of `exclude_values`).
+ By default, ``None``.
+ force_include_values : int | float | list, optional
+ Force the inclusion of the given value(s). This input
+ completely replaces anything provided as `include_values`
+ and is mutually exclusive with other inputs (eee info in
+ the description of `exclude_values`). By default, ``None``.
+ force_include_range : list | tuple, optional
+ Force the inclusion of given values in the range
+ [min threshold, max threshold] (ends are inclusive). This
+ input completely replaces anything provided as
+ `include_range` and is mutually exclusive with other inputs
+ (see info in the description of `exclude_values`).
+ By default, ``None``.
+ use_as_weights : bool, optional
+ Option to use layer as final inclusion weights (i.e.
+ 1 = fully included, 0.75 = 75% included, 0.5 = 50% included,
+ etc.). If ``True``, all inclusion/exclusions specifications
+ for the layer are ignored and the raw values (scaled by the
+ `weight` input) are used as inclusion weights.
+ By default, ``False``.
+ weight : float, optional
+ Weight applied to exclusion layer after it is calculated.
+ Can be used, for example, to turn a binary exclusion layer
+ (i.e. data with 0 or 1 values and ``exclude_values=1``
+ input) into partial exclusions by setting the weight to
+ a fraction (e.g. 0.5 for 50% exclusions). By default, ``1``.
+ exclude_nodata : bool, optional
+ Flag to exclude nodata values (`nodata_value`). If
+ ``nodata_value=None`` the `nodata_value` is inferred by
+ :class:`reV.supply_curve.exclusions.ExclusionMask`.
+ By default, ``False``.
+ nodata_value : int | float, optional
+ Nodata value for the layer. If ``None``, the value will be
+ inferred when LayerMask is added to
+ :class:`reV.supply_curve.exclusions.ExclusionMask`.
+ By default, ``None``.
+ extent : dict, optional
+ Optional dictionary with values that can be used to
+ initialize this class (i.e. `layer`, `exclude_values`,
+ `include_range`, etc.). This dictionary should contain the
+ specifications to create a boolean mask that defines the
+ extent to which the original mask should be applied.
+ For example, suppose you specify the input the following
+ way::
+
+ input_dict = {
+ "viewsheds": {
+ "exclude_values": 1,
+ "extent": {
+ "layer": "federal_parks",
+ "include_range": [1, 5]
+ }
+ }
+ }
+
+ for layer_name, kwargs in input_dict.items():
+ layer = LayerMask(layer_name, **kwargs)
+ ...
+
+ This would mean that you are masking out all viewshed layer
+ values equal to 1, **but only where the "federal_parks"
+ layer is equal to 1, 2, 3, 4, or 5**. Outside of these
+ regions (i.e. outside of federal park regions), the viewshed
+ exclusion is **NOT** applied. If the extent mask created by
+ these options is not boolean, an error is thrown (i.e. do
+ not specify `weight` or `use_as_weights`).
+ By default ``None``, which applies the original layer mask
+ to the full extent.
+ **kwargs
+ Optional inputs to maintain legacy kwargs of ``inclusion_*``
+ instead of ``include_*``.
+ """
+
+ self._name=layer
+ self._exclude_values=exclude_values
+ self._exclude_range=exclude_range
+ self._include_values=include_values
+ self._include_range=include_range
+ self._include_weights=include_weights
+ self._force_include=False
+
+ self._parse_legacy_kwargs(kwargs)
+
+ ifforce_include_valuesisnotNone:
+ self._include_values=force_include_values
+ self._force_include=True
+ ifforce_include_rangeisnotNone:
+ self._include_range=force_include_range
+ self._force_include=True
+
+ self._as_weights=use_as_weights
+ self._exclude_nodata=exclude_nodata
+ self.nodata_value=nodata_value
+
+ ifweight>1orweight<0:
+ msg=('Invalid weight ({}) provided for layer {}:'
+ '\nWeight must fall between 0 and 1!'.format(weight,layer))
+ logger.error(msg)
+ raiseValueError(msg)
+
+ self._weight=weight
+ self._mask_type=self._check_mask_type()
+ self.extent=LayerMask(**extent)ifextentisnotNoneelseNone
+
+ def__repr__(self):
+ msg=('{} for "{}" exclusion, of type "{}"'
+ .format(self.__class__.__name__,self.name,self.mask_type))
+
+ returnmsg
+
+ def__getitem__(self,data):
+"""Get the multiplicative inclusion mask.
+
+ Returns
+ -------
+ mask : ndarray
+ Masked exclusion data with weights applied such that 1 is included,
+ 0 is excluded, 0.5 is half included.
+ """
+ returnself._apply_mask(data)
+
+ def_parse_legacy_kwargs(self,kwargs):
+"""Parse legacy kwargs that start with inclusion_* instead of include_*
+
+ Parameters
+ ----------
+ kwargs : dict
+ Optional inputs to maintain legacy kwargs of inclusion_* instead of
+ include_*
+ """
+
+ fork,vinkwargs.items():
+ msg=None
+ ifk=='inclusion_range':
+ self._include_range=v
+ msg='Please use "include_range" instead of "inclusion_range"'
+
+ elifk=='inclusion_weights':
+ self._include_weights=v
+ msg=('Please use "include_weights" instead of '
+ '"inclusion_weights"')
+
+ elifk=='inclusion_values':
+ self._include_values=v
+ msg=('Please use "include_values" instead of '
+ '"inclusion_values"')
+
+ ifmsgisnotNone:
+ warn(msg)
+ logger.warning(msg)
+
+ @property
+ defname(self):
+"""
+ Layer name to extract from exclusions .h5 file
+
+ Returns
+ -------
+ _name : str
+ """
+ returnself._name
+
+ @property
+ defmin_value(self):
+"""Minimum value to include/exclude if include_range or exclude_range
+ was input.
+
+ Returns
+ -------
+ float
+ """
+ if'excl'inself.mask_type:
+ range_var=self._exclude_range
+ else:
+ range_var=self._include_range
+
+ ifall(isinstance(x,(int,float))forxinrange_var):
+ returnmin(range_var)
+ returnrange_var[0]
+
+ @property
+ defmax_value(self):
+"""Maximum value to include/exclude if include_range or exclude_range
+ was input.
+
+ Returns
+ -------
+ float
+ """
+ if'excl'inself.mask_type:
+ range_var=self._exclude_range
+ else:
+ range_var=self._include_range
+
+ ifall(isinstance(x,(int,float))forxinrange_var):
+ returnmax(range_var)
+ returnrange_var[1]
+
+ @property
+ defexclude_values(self):
+"""
+ Values to exclude
+
+ Returns
+ -------
+ _exclude_values : list
+ """
+ returnself._exclude_values
+
+ @property
+ definclude_values(self):
+"""
+ Values to include
+
+ Returns
+ -------
+ _include_values : list
+ """
+ returnself._include_values
+
+ @property
+ definclude_weights(self):
+"""
+ Mapping of values to include and at what weights
+
+ Returns
+ -------
+ dict
+ """
+ returnself._include_weights
+
+ @property
+ defforce_include(self):
+"""
+ Flag to force include mask
+
+ Returns
+ -------
+ _force_include : bool
+ """
+ returnself._force_include
+
+ @property
+ defmask_type(self):
+"""
+ Type of exclusion mask for this layer
+
+ Returns
+ -------
+ str
+ """
+ returnself._mask_type
+
+ def_apply_mask(self,data):
+"""
+ Apply mask function
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+
+ Returns
+ -------
+ data : ndarray
+ Masked exclusion data with weights applied such that 1 is included,
+ 0 is excluded, 0.5 is half included.
+ """
+
+ ifnotself._as_weights:
+ ifself.mask_type=='include_range':
+ func=self._include_range_mask
+ elifself.mask_type=='exclude_range':
+ func=self._exclude_range_mask
+ elifself.mask_type=='exclude':
+ func=self._exclusion_mask
+ elifself.mask_type=='include':
+ func=self._inclusion_mask
+ elifself.mask_type=='include_weights':
+ func=self._weights_mask
+ else:
+ msg=('{} is an invalid mask type: expecting '
+ '"include_range", "exclude_range", "exclude", '
+ '"include", or "include_weights"'
+ .format(self.mask_type))
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ data=func(data)
+
+ data=data.astype('float32')*self._weight
+
+ returndata
+
+ def_check_mask_type(self):
+"""
+ Ensure that the initialization arguments are valid and not
+ contradictory
+
+ Returns
+ -------
+ mask : str
+ Mask type
+ """
+ mask=None
+ ifnotself._as_weights:
+ masks={'include_range':any(iisnotNone
+ foriinself._include_range),
+ 'exclude_range':any(iisnotNone
+ foriinself._exclude_range),
+ 'exclude':self._exclude_valuesisnotNone,
+ 'include':self._include_valuesisnotNone,
+ 'include_weights':self._include_weightsisnotNone}
+ fork,vinmasks.items():
+ ifv:
+ ifmaskisNone:
+ mask=k
+ else:
+ msg=('Only one approach can be used to create the '
+ 'inclusion mask, but you supplied {} and {}'
+ .format(mask,k))
+ logger.error(msg)
+ raiseExclusionLayerError(msg)
+
+ ifmaskisNone:
+ msg=('Exactly one approach must be specified to create the '
+ 'inclusion mask for layer {!r}! Please specify one of: '
+ '`exclude_values`, `exclude_range`, `include_values`, '
+ '`include_range`, `include_weights`, '
+ '`force_include_values`, or `force_include_range`.'
+ .format(self.name))
+ logger.error(msg)
+ raiseExclusionLayerError(msg)
+
+ ifmask=='include_weights'andself._weight<1:
+ msg=("Values are individually weighted when using "
+ "'include_weights', the supplied weight of {} will be "
+ "ignored!".format(self._weight))
+ self._weight=1
+ logger.warning(msg)
+ warn(msg)
+
+ returnmask
+
+ def_exclude_range_mask(self,data):
+"""
+ Mask exclusion layer based on exclude value range
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+
+ Returns
+ -------
+ mask : ndarray
+ Boolean mask of which values to include (True is include).
+ """
+ mask=np.full(data.shape,False)
+ ifself.min_valueisnotNone:
+ mask=data<self.min_value
+
+ ifself.max_valueisnotNone:
+ mask|=data>self.max_value
+
+ mask[data==self.nodata_value]=True
+ ifself._exclude_nodata:
+ mask=mask&(data!=self.nodata_value)
+
+ returnmask
+
+ def_include_range_mask(self,data):
+"""
+ Mask exclusion layer based on include value range
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+
+ Returns
+ -------
+ mask : ndarray
+ Boolean mask of which values to include (True is include).
+ """
+ mask=np.full(data.shape,True)
+ ifself.min_valueisnotNone:
+ mask=data>=self.min_value
+
+ ifself.max_valueisnotNone:
+ mask*=data<=self.max_value
+
+ ifself._exclude_nodataandself.nodata_valueisnotNone:
+ mask=mask&(data!=self.nodata_value)
+
+ returnmask
+
+ def_value_mask(self,data,values,include=True):
+"""
+ Mask exclusion layer based on values to include or exclude
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+ values : list
+ Values to include or exclude.
+ include : boolean
+ Flag as to whether values should be included or excluded.
+ If True, output mask will be True where data == values.
+ If False, output mask will be True where data != values.
+
+ Returns
+ -------
+ mask : ndarray
+ Boolean mask of which values to include (True is include)
+ """
+ mask=np.isin(data,values)
+
+ ifnotinclude:
+ mask=~mask
+
+ # only include if not nodata
+ ifself._exclude_nodataandself.nodata_valueisnotNone:
+ mask=mask&(data!=self.nodata_value)
+
+ returnmask
+
+ def_exclusion_mask(self,data):
+"""
+ Mask exclusion layer based on values to exclude
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+
+ Returns
+ -------
+ mask : ndarray
+ Boolean mask of which values to include (True is include)
+ """
+ mask=self._value_mask(data,self.exclude_values,include=False)
+
+ returnmask
+
+ def_inclusion_mask(self,data):
+"""
+ Mask exclusion layer based on values to include
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+
+ Returns
+ -------
+ mask : ndarray
+ Boolean mask of which values to include (True is include)
+ """
+ mask=self._value_mask(data,self.include_values,include=True)
+
+ returnmask
+
+ def_weights_mask(self,data):
+"""
+ Mask exclusion layer based on the weights for each inclusion value
+
+ Parameters
+ ----------
+ data : ndarray
+ Exclusions data to create mask from
+
+ Returns
+ -------
+ mask : ndarray
+ Percentage of value to include
+ """
+ mask=None
+ forvalue,weightinself.include_weights.items():
+ ifisinstance(value,str):
+ value=float(value)
+
+ weight=np.array([weight],dtype='float32')
+
+ ifmaskisNone:
+ mask=self._value_mask(data,[value],include=True)*weight
+ else:
+ mask+=self._value_mask(data,[value],include=True)*weight
+
+ returnmask
+
+
+
[docs]classExclusionMask:
+"""
+ Class to create final exclusion mask
+ """
+
+ FILTER_KERNELS={
+ 'queen':np.array([[1,1,1],
+ [1,1,1],
+ [1,1,1]]),
+ 'rook':np.array([[0,1,0],
+ [1,1,1],
+ [0,1,0]])}
+
+ def__init__(self,excl_h5,layers=None,min_area=None,
+ kernel='queen',hsds=False,check_layers=False):
+"""
+ Parameters
+ ----------
+ excl_h5 : str | list | tuple
+ Path to one or more exclusions .h5 files
+ layers : list | NoneType
+ list of LayerMask instances for each exclusion layer to combine
+ min_area : float | NoneType
+ Minimum required contiguous area in sq-km
+ kernel : str
+ Contiguous filter method to use on final exclusion
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+ check_layers : bool
+ Run a pre-flight check on each layer to ensure they contain
+ un-excluded values
+ """
+ self._layers={}
+ self._excl_h5=ExclusionLayers(excl_h5,hsds=hsds)
+ self._excl_layers=None
+ self._check_layers=check_layers
+
+ iflayersisnotNone:
+ ifnotisinstance(layers,list):
+ layers=[layers]
+
+ missing=[layer.nameforlayerinlayers
+ iflayer.namenotinself.excl_layers]
+ ifany(missing):
+ msg=("ExclusionMask layers {} are missing from: {}"
+ .format(missing,self._excl_h5))
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ forlayerinlayers:
+ self.add_layer(layer)
+
+ ifkernelin["queen","rook"]:
+ self._min_area=min_area
+ self._kernel=kernel
+ logger.debug('Initializing Exclusions mask with min area of {} '
+ 'km2 and filter kernel "{}".'
+ .format(self._min_area,self._kernel))
+ else:
+ raiseKeyError('kernel must be "queen" or "rook"')
+
+ def__enter__(self):
+ returnself
+
+ def__exit__(self,type,value,traceback):
+ self.close()
+
+ iftypeisnotNone:
+ raise
+
+ def__repr__(self):
+ msg=("{} from {} with {} input layers"
+ .format(self.__class__.__name__,self.excl_h5.h5_file,
+ len(self)))
+
+ returnmsg
+
+ def__len__(self):
+ returnlen(self.layers)
+
+ def__getitem__(self,*ds_slice):
+"""Get the multiplicative inclusion mask.
+
+ Parameters
+ ----------
+ ds_slice : int | slice | list | ndarray
+ What to extract from ds, each arg is for a sequential axis.
+ For example, (slice(0, 64), slice(0, 64)) will extract a 64x64
+ exclusions mask.
+
+ Returns
+ -------
+ mask : ndarray
+ Multiplicative inclusion mask with all layers multiplied together
+ ("and" operation) such that 1 is included, 0 is excluded,
+ 0.5 is half.
+ """
+ returnself._generate_mask(*ds_slice)
+
+
[docs]defclose(self):
+"""
+ Close h5 instance
+ """
+ self.excl_h5.close()
[docs]defadd_layer(self,layer,replace=False):
+"""
+ Add layer to be combined
+
+ Parameters
+ ----------
+ layer : LayerMask
+ LayerMask instance to add to set of layers to be combined
+ """
+
+ iflayer.namenotinself.excl_layers:
+ msg="{} does not exist in {}".format(layer.name,self._excl_h5)
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ iflayer.nameinself.layer_names:
+ msg="{} is already in {}".format(layer.name,self)
+ ifreplace:
+ msg+=" replacing existing layer"
+ logger.warning(msg)
+ warn(msg)
+ else:
+ logger.error(msg)
+ raiseExclusionLayerError(msg)
+
+ layer.nodata_value=self.excl_h5.get_nodata_value(layer.name)
+ ifself._check_layers:
+ ifnotlayer[self.excl_h5[layer.name]].any():
+ msg=("Layer {} is fully excluded!".format(layer.name))
+ logger.error(msg)
+ raiseExclusionLayerError(msg)
+
+ self._layers[layer.name]=layer
+
+ @property
+ defnodata_lookup(self):
+"""Get a dictionary lookup of the nodata values for each layer name.
+
+ Returns
+ -------
+ nodata : dict
+ Lookup keyed by layer name and values are nodata values for the
+ respective layers.
+ """
+ nodata={}
+ forlayer_nameinself.layer_names:
+ nodata[layer_name]=self.excl_h5.get_nodata_value(layer_name)
+
+ returnnodata
+
+ @classmethod
+ def_area_filter(cls,mask,min_area,excl_area,kernel='queen'):
+"""
+ Ensure the contiguous area of included pixels is greater than
+ prescribed minimum in sq-km
+
+ Parameters
+ ----------
+ mask : ndarray
+ Inclusion mask
+ min_area : float
+ Minimum required contiguous area in sq-km
+ kernel : str
+ Kernel type, either 'queen' or 'rook'
+ excl_area : float
+ Area of each exclusion pixel in km^2, assumes 90m resolution
+
+ Returns
+ -------
+ mask : ndarray
+ Updated inclusion mask
+ """
+ s=cls.FILTER_KERNELS[kernel]
+ labels,_=ndimage.label(mask>0,structure=s)
+ l,c=np.unique(labels,return_counts=True)
+
+ min_counts=np.ceil(min_area/excl_area)
+ pos=c[1:]<min_counts
+ bad_labels=l[1:][pos]
+
+ mask[np.isin(labels,bad_labels)]=0
+
+ returnmask
+
+ def_increase_mask_slice(self,ds_slice,n=1):
+"""Increase the mask slice, e.g. from 64x64 to 192x192, to help the
+ contiguous area filter be more accurate.
+
+ Parameters
+ ----------
+ ds_slice : tuple
+ Two entry tuple with x and y slices. Anything else will be passed
+ through unaffected.
+ n : int
+ Number of blocks to increase in each direction. For example,
+ a 64x64 slice with n=1 will increase to 192x192
+ (increases by 64xn in each direction).
+
+ Returns
+ -------
+ new_slice : tuple
+ Two entry tuple with x and y slices with increased dimensions.
+ sub_slice : tuple
+ Two entry tuple with x and y slices to retrieve the original
+ slice out of the bigger slice.
+ """
+ new_slice=ds_slice
+ sub_slice=(slice(None),slice(None))
+
+ ifisinstance(ds_slice,tuple)andlen(ds_slice)==2:
+ y_slice=ds_slice[0]
+ x_slice=ds_slice[1]
+ ifisinstance(x_slice,slice)andisinstance(y_slice,slice):
+ y_diff=n*np.abs(y_slice.stop-y_slice.start)
+ x_diff=n*np.abs(x_slice.stop-x_slice.start)
+
+ y_new_start=int(np.max((0,(y_slice.start-y_diff))))
+ x_new_start=int(np.max((0,(x_slice.start-x_diff))))
+
+ y_new_stop=int(np.min((self.shape[0],
+ (y_slice.stop+y_diff))))
+ x_new_stop=int(np.min((self.shape[1],
+ (x_slice.stop+x_diff))))
+
+ new_slice=(slice(y_new_start,y_new_stop),
+ slice(x_new_start,x_new_stop))
+
+ ify_new_start==y_slice.start:
+ y_sub_start=0
+ else:
+ y_sub_start=int(n*y_diff)
+ ifx_new_start==x_slice.start:
+ x_sub_start=0
+ else:
+ x_sub_start=int(n*x_diff)
+
+ y_sub_stop=y_sub_start+y_diff
+ x_sub_stop=x_sub_start+x_diff
+
+ sub_slice=(slice(y_sub_start,y_sub_stop),
+ slice(x_sub_start,x_sub_stop))
+
+ returnnew_slice,sub_slice
+
+ def_generate_ones_mask(self,ds_slice):
+"""
+ Generate mask of all ones
+
+ Parameters
+ ----------
+ ds_slice : tuple
+ dataset slice of interest along axis 0 and 1
+
+ Returns
+ -------
+ mask : ndarray
+ Array of ones slices down by ds_slice
+ """
+
+ ones_shape=()
+ fori,sinenumerate(self.shape):
+ ifi<len(ds_slice):
+ ax_slice=ds_slice[i]
+ ifnp.issubdtype(type(ax_slice),np.integer):
+ ones_shape+=(ax_slice,)
+ else:
+ ax=np.arange(s,dtype=np.int32)
+ ones_shape+=(len(ax[ax_slice]),)
+ else:
+ ones_shape+=(s,)
+
+ mask=np.ones(ones_shape,dtype='float32')
+
+ returnmask
+
+ def_add_layer_to_mask(self,mask,layer,ds_slice,check_layers,
+ combine_func):
+"""Add layer mask to full mask."""
+ layer_mask=self._compute_layer_mask(layer,ds_slice,check_layers)
+ ifmaskisNone:
+ returnlayer_mask
+
+ returncombine_func(mask,layer_mask,dtype='float32')
+
+ def_compute_layer_mask(self,layer,ds_slice,check_layers=False):
+"""Compute mask for single layer, including extent."""
+ layer_mask=self._masked_layer_data(layer,ds_slice)
+ layer_mask=self._apply_layer_mask_extent(layer,layer_mask,ds_slice)
+
+ logger.debug('Computed exclusions {} for {}. Layer has average value '
+ 'of {:.2f}.'
+ .format(layer,ds_slice,layer_mask.mean()))
+ log_mem(logger,log_level='DEBUG')
+
+ ifcheck_layersandnotlayer_mask.any():
+ msg="Layer {} is fully excluded!".format(layer.name)
+ logger.error(msg)
+ raiseExclusionLayerError(msg)
+
+ returnlayer_mask
+
+ def_apply_layer_mask_extent(self,layer,layer_mask,ds_slice):
+"""Apply extent to layer mask, if any."""
+ iflayer.extentisNone:
+ returnlayer_mask
+
+ layer_extent=self._masked_layer_data(layer.extent,ds_slice)
+ ifnotnp.array_equal(layer_extent,layer_extent.astype(bool)):
+ msg=("Extent layer must be boolean (i.e. 0 and 1 values "
+ "only)! Please check your extent definition for layer "
+ "{} to ensure you are producing a boolean layer!"
+ .format(layer.name))
+ logger.error(msg)
+ raiseExclusionLayerError(msg)
+
+ logger.debug("Filtering mask for layer %s down to specified extent",
+ layer.name)
+ layer_mask=np.where(layer_extent,layer_mask,1)
+ returnlayer_mask
+
+ def_masked_layer_data(self,layer,ds_slice):
+"""Extract masked data for layer."""
+ returnlayer[self.excl_h5[(layer.name,)+ds_slice]]
+
+ def_generate_mask(self,*ds_slice,check_layers=False):
+"""
+ Generate multiplicative inclusion mask from exclusion layers.
+
+ Parameters
+ ----------
+ ds_slice : int | slice | list | ndarray
+ What to extract from ds, each arg is for a sequential axis.
+ For example, (slice(0, 64), slice(0, 64)) will extract a 64x64
+ exclusions mask.
+ check_layers : bool
+ Check each layer as each layer is extracted to ensure they contain
+ un-excluded values. This should only really be True if ds_slice is
+ for the full inclusion mask. Otherwise, this could raise an error
+ for a fully excluded mask for just one excluded SC point.
+
+ Returns
+ -------
+ mask : ndarray
+ Multiplicative inclusion mask with all layers multiplied together
+ ("and" operation) such that 1 is included, 0 is excluded,
+ 0.5 is half.
+ """
+
+ mask=None
+ ds_slice,sub_slice=self._parse_ds_slice(ds_slice)
+
+ ifself.layers:
+ force_include=[]
+ forlayerinself.layers:
+ iflayer.force_include:
+ force_include.append(layer)
+ else:
+ mask=self._add_layer_to_mask(mask,layer,ds_slice,
+ check_layers,
+ combine_func=np.minimum)
+ forlayerinforce_include:
+ mask=self._add_layer_to_mask(mask,layer,ds_slice,
+ check_layers,
+ combine_func=np.maximum)
+
+ ifself._min_areaisnotNone:
+ mask=self._area_filter(mask,self._min_area,
+ self._excl_h5.pixel_area,
+ kernel=self._kernel)
+ mask=mask[sub_slice]
+ else:
+ ifself._min_areaisnotNone:
+ ds_slice=sub_slice
+
+ mask=self._generate_ones_mask(ds_slice)
+
+ returnmask
+
+ def_parse_ds_slice(self,ds_slice):
+"""Parse a dataset slice to make it the proper dimensions and also
+ optionally increase the dataset slice to make the contiguous area
+ filter more accurate
+
+ Parameters
+ ----------
+ ds_slice : int | slice | list | ndarray
+ What to extract from ds, each arg is for a sequential axis.
+ For example, (slice(0, 64), slice(0, 64)) will extract a 64x64
+ exclusions mask.
+
+ Returns
+ -------
+ ds_slice : tuple
+ Two entry tuple with x and y slices with increased dimensions.
+ sub_slice : tuple
+ Two entry tuple with x and y slices to retrieve the original
+ slice out of the bigger slice.
+ """
+
+ iflen(ds_slice)==1&isinstance(ds_slice[0],tuple):
+ ds_slice=ds_slice[0]
+
+ sub_slice=None
+ ifself._min_areaisnotNone:
+ ds_slice,sub_slice=self._increase_mask_slice(ds_slice,n=1)
+
+ returnds_slice,sub_slice
+
+
[docs]@classmethod
+ defrun(cls,excl_h5,layers=None,min_area=None,
+ kernel='queen',hsds=False):
+"""
+ Create inclusion mask from given layers
+
+ Parameters
+ ----------
+ excl_h5 : str | list | tuple
+ Path to one or more exclusions .h5 files
+ layers : list | NoneType
+ list of LayerMask instances for each exclusion layer to combine
+ min_area : float | NoneType
+ Minimum required contiguous area in sq-km
+ kernel : str
+ Contiguous filter method to use on final exclusion
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+
+ Returns
+ -------
+ mask : ndarray
+ Full inclusion mask
+ """
+ withcls(excl_h5,layers=layers,min_area=min_area,
+ kernel=kernel,hsds=hsds)asf:
+ mask=f.mask
+
+ returnmask
+
+
+
[docs]classExclusionMaskFromDict(ExclusionMask):
+"""
+ Class to initialize ExclusionMask from a dictionary defining layers
+ """
+
+ def__init__(self,excl_h5,layers_dict=None,min_area=None,
+ kernel='queen',hsds=False,check_layers=False):
+"""
+ Parameters
+ ----------
+ excl_h5 : str | list | tuple
+ Path to one or more exclusions .h5 files
+ layers_dict : dict | NoneType
+ Dictionary of LayerMask arugments {layer: {kwarg: value}}
+ min_area : float | NoneType
+ Minimum required contiguous area in sq-km
+ kernel : str
+ Contiguous filter method to use on final exclusion
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+ check_layers : bool
+ Run a pre-flight check on each layer to ensure they contain
+ un-excluded values
+ """
+ iflayers_dictisnotNone:
+ layers=[]
+ forlayer,kwargsinlayers_dict.items():
+ layers.append(LayerMask(layer,**kwargs))
+ else:
+ layers=None
+
+ super().__init__(excl_h5,layers=layers,min_area=min_area,
+ kernel=kernel,hsds=hsds,check_layers=check_layers)
+
+
[docs]@classmethod
+ defextract_inclusion_mask(cls,excl_fpath,tm_dset,excl_dict=None,
+ area_filter_kernel='queen',min_area=None):
+"""
+ Extract the full inclusion mask from excl_fpath using the given
+ exclusion layers and whether or not to run a minimum area filter
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ area_filter_kernel : str, optional
+ Contiguous area filter method to use on final exclusions mask,
+ by default "queen"
+ min_area : float, optional
+ Minimum required contiguous area filter in sq-km,
+ by default None
+
+ Returns
+ -------
+ inclusion_mask : ndarray
+ Pre-computed 2D inclusion mask (normalized with expected range:
+ [0, 1], where 1 is included and 0 is excluded)
+ """
+ logger.info('Pre-extracting full exclusion mask, this could take '
+ 'up to 30min for a large exclusion config...')
+ withcls(excl_fpath,layers_dict=excl_dict,check_layers=False,
+ min_area=min_area,kernel=area_filter_kernel)asf:
+ inclusion_mask=f._generate_mask(...,check_layers=True)
+ tm_mask=f._excl_h5[tm_dset]==-1
+ inclusion_mask[tm_mask]=0
+
+ logger.info('Finished extracting full exclusion mask.')
+ logger.info('The full exclusion mask has {:.2f}% of area included.'
+ .format(100*inclusion_mask.sum()
+ /inclusion_mask.size))
+
+ ifinclusion_mask.sum()==0:
+ msg='The exclusions inputs resulted in a fully excluded mask!'
+ logger.error(msg)
+ raiseSupplyCurveInputError(msg)
+
+ returninclusion_mask
+
+
[docs]@classmethod
+ defrun(cls,excl_h5,layers_dict=None,min_area=None,
+ kernel='queen',hsds=False):
+"""
+ Create inclusion mask from given layers dictionary
+
+ Parameters
+ ----------
+ excl_h5 : str | list | tuple
+ Path to one or more exclusions .h5 files
+ layers_dict : dict | NoneType
+ Dictionary of LayerMask arugments {layer: {kwarg: value}}
+ min_area : float | NoneType
+ Minimum required contiguous area in sq-km
+ kernel : str
+ Contiguous filter method to use on final exclusion
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+
+ Returns
+ -------
+ mask : ndarray
+ Full inclusion mask
+ """
+ withcls(excl_h5,layers_dict=layers_dict,min_area=min_area,
+ kernel=kernel,hsds=hsds)asf:
+ mask=f.mask
+
+ returnmask
+
+
+
[docs]classFrictionMask(ExclusionMask):
+"""Class to handle exclusion-style friction layer."""
+
+ def__init__(self,fric_h5,fric_dset,hsds=False,check_layers=False):
+"""
+ Parameters
+ ----------
+ fric_h5 : str
+ Path to friction layer .h5 file (same format as exclusions file)
+ fric_dset : str
+ Friction layer dataset in fric_h5
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+ check_layers : bool
+ Run a pre-flight check on each layer to ensure they contain
+ un-excluded values
+ """
+ self._fric_dset=fric_dset
+ L=[LayerMask(fric_dset,use_as_weights=True,exclude_nodata=False)]
+ super().__init__(fric_h5,layers=L,min_area=None,hsds=hsds,
+ check_layers=check_layers)
+
+ def_generate_mask(self,*ds_slice):
+"""
+ Generate multiplicative friction layer mask.
+
+ Parameters
+ ----------
+ ds_slice : int | slice | list | ndarray
+ What to extract from ds, each arg is for a sequential axis.
+ For example, (slice(0, 64), slice(0, 64)) will extract a 64x64
+ exclusions mask.
+
+ Returns
+ -------
+ mask : ndarray
+ Multiplicative friction layer mask with nodata values set to 1.
+ """
+
+ mask=None
+ iflen(ds_slice)==1&isinstance(ds_slice[0],tuple):
+ ds_slice=ds_slice[0]
+
+ layer_slice=(self._layers[self._fric_dset].name,)+ds_slice
+ mask=self._layers[self._fric_dset][self.excl_h5[layer_slice]]
+ mask[(mask==self._layers[self._fric_dset].nodata_value)]=1
+
+ returnmask
+
+
[docs]@classmethod
+ defrun(cls,excl_h5,fric_dset,hsds=False):
+"""
+ Create inclusion mask from given layers dictionary
+
+ Parameters
+ ----------
+ fric_h5 : str
+ Path to friction layer .h5 file (same format as exclusions file)
+ fric_dset : str
+ Friction layer dataset in fric_h5
+ hsds : bool
+ Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
+ behind HSDS
+
+ Returns
+ -------
+ mask : ndarray
+ Full inclusion mask
+ """
+ L=[LayerMask(fric_dset,use_as_weights=True,exclude_nodata=False)]
+ withcls(excl_h5,*L,min_area=None,hsds=hsds)asf:
+ mask=f.mask
+
+ returnmask
[docs]classSupplyCurveExtent:
+"""Supply curve full extent framework. This class translates the 90m
+ exclusion grid to the aggregated supply curve resolution."""
+
+ def__init__(self,f_excl,resolution=64):
+"""
+ Parameters
+ ----------
+ f_excl : str | list | tuple | ExclusionLayers
+ File path(s) to the exclusions grid, or pre-initialized
+ ExclusionLayers. The exclusions dictate the SC analysis extent.
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ """
+
+ logger.debug(
+ "Initializing SupplyCurveExtent with res {} from: {}".format(
+ resolution,f_excl
+ )
+ )
+
+ ifnotisinstance(resolution,int):
+ raiseSupplyCurveInputError(
+ "Supply Curve resolution needs to be "
+ "an integer but received: {}".format(type(resolution))
+ )
+
+ ifisinstance(f_excl,(str,list,tuple)):
+ self._excl_fpath=f_excl
+ self._excls=ExclusionLayers(f_excl)
+ elifisinstance(f_excl,ExclusionLayers):
+ self._excl_fpath=f_excl.h5_file
+ self._excls=f_excl
+ else:
+ raiseSupplyCurveInputError(
+ "SupplyCurvePoints needs an "
+ "exclusions file path, or "
+ "ExclusionLayers handler but "
+ "received: {}".format(type(f_excl))
+ )
+
+ self._excl_shape=self.exclusions.shape
+ # limit the resolution to the exclusion shape.
+ self._res=int(np.min(list(self.excl_shape)+[resolution]))
+
+ self._n_rows=None
+ self._n_cols=None
+ self._cols_of_excl=None
+ self._rows_of_excl=None
+ self._excl_row_slices=None
+ self._excl_col_slices=None
+ self._latitude=None
+ self._longitude=None
+ self._points=None
+
+ self._sc_col_ind,self._sc_row_ind=np.meshgrid(
+ np.arange(self.n_cols),np.arange(self.n_rows)
+ )
+ self._sc_col_ind=self._sc_col_ind.flatten()
+ self._sc_row_ind=self._sc_row_ind.flatten()
+
+ logger.debug(
+ "Initialized SupplyCurveExtent with shape {} from "
+ "exclusions with shape {}".format(self.shape,self.excl_shape)
+ )
+
+ def__len__(self):
+"""Total number of supply curve points."""
+ returnself.n_rows*self.n_cols
+
+ def__enter__(self):
+ returnself
+
+ def__exit__(self,type,value,traceback):
+ self.close()
+ iftypeisnotNone:
+ raise
+
+ def__getitem__(self,gid):
+"""Get SC extent meta data corresponding to an SC point gid."""
+ ifgid>=len(self):
+ raiseKeyError(
+ "SC extent with {} points does not contain SC "
+ "point gid {}.".format(len(self),gid)
+ )
+
+ returnself.points.loc[gid]
+
+
[docs]defclose(self):
+"""Close all file handlers."""
+ self._excls.close()
+
+ @property
+ defshape(self):
+"""Get the Supply curve shape tuple (n_rows, n_cols).
+
+ Returns
+ -------
+ shape : tuple
+ 2-entry tuple representing the full supply curve extent.
+ """
+
+ return(self.n_rows,self.n_cols)
+
+ @property
+ defexclusions(self):
+"""Get the exclusions object.
+
+ Returns
+ -------
+ _excls : ExclusionLayers
+ ExclusionLayers h5 handler object.
+ """
+ returnself._excls
+
+ @property
+ defresolution(self):
+"""Get the 1D resolution.
+
+ Returns
+ -------
+ _res : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ """
+ returnself._res
+
+ @property
+ defexcl_shape(self):
+"""Get the shape tuple of the exclusion file raster.
+
+ Returns
+ -------
+ tuple
+ """
+ returnself._excl_shape
+
+ @property
+ defexcl_rows(self):
+"""Get the unique row indices identifying the exclusion points.
+
+ Returns
+ -------
+ excl_rows : np.ndarray
+ Array of exclusion row indices.
+ """
+ returnnp.arange(self.excl_shape[0])
+
+ @property
+ defexcl_cols(self):
+"""Get the unique column indices identifying the exclusion points.
+
+ Returns
+ -------
+ excl_cols : np.ndarray
+ Array of exclusion column indices.
+ """
+ returnnp.arange(self.excl_shape[1])
+
+ @property
+ defrows_of_excl(self):
+"""List representing the supply curve points rows and which
+ exclusions rows belong to each supply curve row.
+
+ Returns
+ -------
+ _rows_of_excl : list
+ List representing the supply curve points rows. Each list entry
+ contains the exclusion row indices that are included in the sc
+ point.
+ """
+ ifself._rows_of_exclisNone:
+ self._rows_of_excl=self._chunk_excl(
+ self.excl_rows,self.resolution
+ )
+
+ returnself._rows_of_excl
+
+ @property
+ defcols_of_excl(self):
+"""List representing the supply curve points columns and which
+ exclusions columns belong to each supply curve column.
+
+ Returns
+ -------
+ _cols_of_excl : list
+ List representing the supply curve points columns. Each list entry
+ contains the exclusion column indices that are included in the sc
+ point.
+ """
+ ifself._cols_of_exclisNone:
+ self._cols_of_excl=self._chunk_excl(
+ self.excl_cols,self.resolution
+ )
+
+ returnself._cols_of_excl
+
+ @property
+ defexcl_row_slices(self):
+"""
+ List representing the supply curve points rows and which
+ exclusions rows belong to each supply curve row.
+
+ Returns
+ -------
+ _excl_row_slices : list
+ List representing the supply curve points rows. Each list entry
+ contains the exclusion row slice that are included in the sc
+ point.
+ """
+ ifself._excl_row_slicesisNone:
+ self._excl_row_slices=self._excl_slices(
+ self.excl_rows,self.resolution
+ )
+
+ returnself._excl_row_slices
+
+ @property
+ defexcl_col_slices(self):
+"""
+ List representing the supply curve points cols and which
+ exclusions cols belong to each supply curve col.
+
+ Returns
+ -------
+ _excl_col_slices : list
+ List representing the supply curve points cols. Each list entry
+ contains the exclusion col slice that are included in the sc
+ point.
+ """
+ ifself._excl_col_slicesisNone:
+ self._excl_col_slices=self._excl_slices(
+ self.excl_cols,self.resolution
+ )
+
+ returnself._excl_col_slices
+
+ @property
+ defn_rows(self):
+"""Get the number of supply curve grid rows.
+
+ Returns
+ -------
+ n_rows : int
+ Number of row entries in the full supply curve grid.
+ """
+ ifself._n_rowsisNone:
+ self._n_rows=int(np.ceil(self.excl_shape[0]/self.resolution))
+
+ returnself._n_rows
+
+ @property
+ defn_cols(self):
+"""Get the number of supply curve grid columns.
+
+ Returns
+ -------
+ n_cols : int
+ Number of column entries in the full supply curve grid.
+ """
+ ifself._n_colsisNone:
+ self._n_cols=int(np.ceil(self.excl_shape[1]/self.resolution))
+
+ returnself._n_cols
+
+ @property
+ deflatitude(self):
+"""
+ Get supply curve point latitudes
+
+ Returns
+ -------
+ ndarray
+ """
+ ifself._latitudeisNone:
+ lats=[]
+ lons=[]
+
+ sc_cols,sc_rows=np.meshgrid(
+ np.arange(self.n_cols),np.arange(self.n_rows)
+ )
+ forr,cinzip(sc_rows.flatten(),sc_cols.flatten()):
+ r=self.excl_row_slices[r]
+ c=self.excl_col_slices[c]
+ lats.append(self.exclusions[LATITUDE,r,c].mean())
+ lons.append(self.exclusions[LONGITUDE,r,c].mean())
+
+ self._latitude=np.array(lats,dtype="float32")
+ self._longitude=np.array(lons,dtype="float32")
+
+ returnself._latitude
+
+ @property
+ deflongitude(self):
+"""
+ Get supply curve point longitudes
+
+ Returns
+ -------
+ ndarray
+ """
+ ifself._longitudeisNone:
+ lats=[]
+ lons=[]
+
+ sc_cols,sc_rows=np.meshgrid(
+ np.arange(self.n_cols),np.arange(self.n_rows)
+ )
+ forr,cinzip(sc_rows.flatten(),sc_cols.flatten()):
+ r=self.excl_row_slices[r]
+ c=self.excl_col_slices[c]
+ lats.append(self.exclusions[LATITUDE,r,c].mean())
+ lons.append(self.exclusions[LONGITUDE,r,c].mean())
+
+ self._latitude=np.array(lats,dtype="float32")
+ self._longitude=np.array(lons,dtype="float32")
+
+ returnself._longitude
+
+ @property
+ deflat_lon(self):
+"""
+ 2D array of lat, lon coordinates for all sc points
+
+ Returns
+ -------
+ ndarray
+ """
+ returnnp.dstack((self.latitude,self.longitude))[0]
+
+ @property
+ defrow_indices(self):
+"""Get a 1D array of row indices for every gid. That is, this property
+ has length == len(gids) and row_indices[sc_gid] yields the row index of
+ the target supply curve gid
+
+ Returns
+ -------
+ ndarray
+ """
+ returnself._sc_row_ind
+
+ @property
+ defcol_indices(self):
+"""Get a 1D array of col indices for every gid. That is, this property
+ has length == len(gids) and col_indices[sc_gid] yields the col index of
+ the target supply curve gid
+
+ Returns
+ -------
+ ndarray
+ """
+ returnself._sc_col_ind
+
+ @property
+ defpoints(self):
+"""Get the summary dataframe of supply curve points.
+
+ Returns
+ -------
+ _points : pd.DataFrame
+ Supply curve points with columns for attributes of each sc point.
+ """
+
+ ifself._pointsisNone:
+ self._points=pd.DataFrame(
+ {
+ "row_ind":self.row_indices.copy(),
+ "col_ind":self.col_indices.copy(),
+ }
+ )
+
+ self._points.index.name="gid"# sc_point_gid
+
+ returnself._points
+
+ @staticmethod
+ def_chunk_excl(arr,resolution):
+"""Split an array into a list of arrays with len == resolution.
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ 1D array to be split into chunks.
+ resolution : int
+ Resolution of the chunks.
+
+ Returns
+ -------
+ chunks : list
+ List of arrays, each with length equal to self.resolution
+ (except for the last array in the list which is the remainder).
+ """
+
+ chunks=get_chunk_ranges(len(arr),resolution)
+ chunks=list(map(lambdai:np.arange(*i),chunks))
+
+ returnchunks
+
+ @staticmethod
+ def_excl_slices(arr,resolution):
+"""Split row or col ind into slices of excl rows or slices
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ 1D array to be split into slices
+ resolution : int
+ Resolution of the sc points
+
+ Returns
+ -------
+ slices : list
+ List of arr slices, each with length equal to self.resolution
+ (except for the last array in the list which is the remainder).
+ """
+
+ slices=get_chunk_ranges(len(arr),resolution)
+ slices=list(map(lambdai:slice(*i),slices))
+
+ returnslices
+
+
[docs]defget_sc_row_col_ind(self,gid):
+"""Get the supply curve grid row and column index values corresponding
+ to a supply curve gid.
+
+ Parameters
+ ----------
+ gid : int
+ Supply curve point gid.
+
+ Returns
+ -------
+ row_ind : int
+ Row index that the gid is located at in the sc grid.
+ col_ind : int
+ Column index that the gid is located at in the sc grid.
+ """
+ row_ind=self.points.loc[gid,"row_ind"]
+ col_ind=self.points.loc[gid,"col_ind"]
+ returnrow_ind,col_ind
+
+
[docs]defget_excl_slices(self,gid):
+"""Get the row and column slices of the exclusions grid corresponding
+ to the supply curve point gid.
+
+ Parameters
+ ----------
+ gid : int
+ Supply curve point gid.
+
+ Returns
+ -------
+ row_slice : slice
+ Exclusions grid row slice corresponding to the sc point gid.
+ col_slice : slice
+ Exclusions grid col slice corresponding to the sc point gid.
+ """
+
+ ifgid>=len(self):
+ raiseSupplyCurveError(
+ 'Requested gid "{}" is out of bounds for '
+ 'supply curve points with length "{}".'.format(gid,len(self))
+ )
+
+ row_slice=self.excl_row_slices[self.row_indices[gid]]
+ col_slice=self.excl_col_slices[self.col_indices[gid]]
+
+ returnrow_slice,col_slice
+
+
[docs]defget_flat_excl_ind(self,gid):
+"""Get the index values of the flattened exclusions grid corresponding
+ to the supply curve point gid.
+
+ Parameters
+ ----------
+ gid : int
+ Supply curve point gid.
+
+ Returns
+ -------
+ excl_ind : np.ndarray
+ Index values of the flattened exclusions grid corresponding to
+ the SC gid.
+ """
+
+ row_slice,col_slice=self.get_excl_slices(gid)
+ excl_ind=self.exclusions.iarr[row_slice,col_slice].flatten()
+
+ returnexcl_ind
+
+
[docs]defget_excl_points(self,dset,gid):
+"""Get the exclusions data corresponding to a supply curve gid.
+
+ Parameters
+ ----------
+ dset : str | int
+ Used as the first arg in the exclusions __getitem__ slice.
+ String can be "meta", integer can be layer number.
+ gid : int
+ Supply curve point gid.
+
+ Returns
+ -------
+ excl_points : pd.DataFrame
+ Exclusions data reduced to just the exclusion points associated
+ with the requested supply curve gid.
+ """
+
+ row_slice,col_slice=self.get_excl_slices(gid)
+
+ returnself.exclusions[dset,row_slice,col_slice]
+
+
[docs]defget_coord(self,gid):
+"""Get the centroid coordinate for the supply curve gid point.
+
+ Parameters
+ ----------
+ gid : int
+ Supply curve point gid.
+
+ Returns
+ -------
+ coord : tuple
+ Two entry coordinate tuple: (latitude, longitude)
+ """
+
+ lat=self.latitude[gid]
+ lon=self.longitude[gid]
+
+ return(lat,lon)
+
+
[docs]defvalid_sc_points(self,tm_dset):
+"""
+ Determine which sc_point_gids contain resource gids and are thus
+ valid supply curve points
+
+ Parameters
+ ----------
+ tm_dset : str
+ Techmap dataset name
+
+ Returns
+ -------
+ valid_gids : ndarray
+ Vector of valid sc_point_gids that contain resource gis
+ """
+
+ logger.info('Getting valid SC points from "{}"...'.format(tm_dset))
+
+ valid_bool=np.zeros(self.n_rows*self.n_cols)
+ tm=self._excls[tm_dset]
+
+ gid=0
+ forrinself.excl_row_slices:
+ forcinself.excl_col_slices:
+ ifnp.any(tm[r,c]!=-1):
+ valid_bool[gid]=1
+ gid+=1
+
+ valid_gids=np.where(valid_bool==1)[0].astype(np.uint32)
+
+ logger.info(
+ "Found {} valid SC points out of {} total possible "
+ "(valid SC points that map to valid resource gids)".format(
+ len(valid_gids),len(valid_bool)
+ )
+ )
+
+ returnvalid_gids
+
+
[docs]defget_slice_lookup(self,sc_point_gids):
+"""
+ Get exclusion slices for all requested supply curve point gids
+
+ Parameters
+ ----------
+ sc_point_gids : list | ndarray
+ List or 1D array of sc_point_gids to get exclusion slices for
+
+ Returns
+ -------
+ dict
+ lookup mapping sc_point_gid to exclusion slice
+ """
+ return{g:self.get_excl_slices(g)forginsc_point_gids}
[docs]classAbstractSupplyCurvePoint(ABC):
+"""
+ Abstract SC point based on only the point gid, SC shape, and resolution.
+ """
+
+ def__init__(self,gid,exclusion_shape,resolution=64):
+"""
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols).
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ """
+
+ self._gid=gid
+ self._resolution=resolution
+ self._rows=self._cols=self._sc_row_ind=self._sc_col_ind=None
+ self._parse_sc_row_col_ind(resolution,exclusion_shape)
+ self._parse_slices(resolution,exclusion_shape)
+
+ @staticmethod
+ def_ordered_unique(seq):
+"""Get a list of unique values in the same order as the input sequence.
+
+ Parameters
+ ----------
+ seq : list | tuple
+ Sequence of values.
+
+ Returns
+ -------
+ seq : list
+ List of unique values in seq input with original order.
+ """
+
+ seen=set()
+
+ return[xforxinseqifnot(xinseenorseen.add(x))]
+
+ def_parse_sc_row_col_ind(self,resolution,exclusion_shape):
+"""Parse SC row and column index.
+
+ Parameters
+ ----------
+ resolution : int | None
+ SC resolution, must be input in combination with gid.
+ exclusion_shape : tuple
+ Shape of the exclusions extent (rows, cols).
+ """
+ n_sc_cols=int(np.ceil(exclusion_shape[1]/resolution))
+
+ self._sc_row_ind=self._gid//n_sc_cols
+ self._sc_col_ind=self._gid%n_sc_cols
+
+ def_parse_slices(self,resolution,exclusion_shape):
+"""Parse row and column resource/generation grid slices.
+
+ Parameters
+ ----------
+ resolution : int | None
+ SC resolution, must be input in combination with gid.
+ exclusion_shape : tuple
+ Shape of the exclusions extent (rows, cols).
+ """
+ inds=self.get_agg_slices(self._gid,exclusion_shape,resolution)
+ self._rows,self._cols=inds
+
+ @property
+ defgid(self):
+"""Supply curve point gid"""
+ returnself._gid
+
+ @property
+ defsc_point_gid(self):
+"""
+ Supply curve point gid
+
+ Returns
+ -------
+ int
+ """
+ returnself._gid
+
+ @property
+ defsc_row_ind(self):
+"""int: Supply curve row index"""
+ returnself._sc_row_ind
+
+ @property
+ defsc_col_ind(self):
+"""int: Supply curve column index"""
+ returnself._sc_col_ind
+
+ @property
+ defresolution(self):
+"""Get the supply curve grid aggregation resolution"""
+ returnself._resolution
+
+ @property
+ defrows(self):
+"""Get the rows of the exclusions layer associated with this SC point.
+
+ Returns
+ -------
+ rows : slice
+ Row slice to index the high-res layer (exclusions layer) for the
+ gid in the agg layer (supply curve layer).
+ """
+ returnself._rows
+
+ @property
+ defcols(self):
+"""Get the cols of the exclusions layer associated with this SC point.
+
+ Returns
+ -------
+ cols : slice
+ Column slice to index the high-res layer (exclusions layer) for the
+ gid in the agg layer (supply curve layer).
+ """
+ returnself._cols
+
+
[docs]@staticmethod
+ defget_agg_slices(gid,shape,resolution):
+"""Get the row, col slices of an aggregation gid.
+
+ Parameters
+ ----------
+ gid : int
+ Gid of interest in the aggregated layer.
+ shape : tuple
+ (row, col) shape tuple of the underlying high-res layer.
+ resolution : int
+ Resolution of the aggregation: number of pixels in 1D being
+ aggregated.
+
+ Returns
+ -------
+ row_slice : slice
+ Row slice to index the high-res layer for the gid in the agg layer.
+ col_slice : slice
+ Col slice to index the high-res layer for the gid in the agg layer.
+ """
+
+ nrows=int(np.ceil(shape[0]/resolution))
+ ncols=int(np.ceil(shape[1]/resolution))
+ super_shape=(nrows,ncols)
+ arr=np.arange(nrows*ncols).reshape(super_shape)
+ try:
+ loc=np.where(arr==gid)
+ row=loc[0][0]
+ col=loc[1][0]
+ exceptIndexErrorasexc:
+ msg=(
+ "Gid {} out of bounds for extent shape {} and "
+ "resolution {}.".format(gid,shape,resolution)
+ )
+ raiseIndexError(msg)fromexc
+
+ ifrow+1!=nrows:
+ row_slice=slice(row*resolution,(row+1)*resolution)
+ else:
+ row_slice=slice(row*resolution,shape[0])
+
+ ifcol+1!=ncols:
+ col_slice=slice(col*resolution,(col+1)*resolution)
+ else:
+ col_slice=slice(col*resolution,shape[1])
+
+ returnrow_slice,col_slice
+
+
+
[docs]classSupplyCurvePoint(AbstractSupplyCurvePoint):
+"""Generic single SC point based on exclusions, resolution, and techmap"""
+
+ def__init__(
+ self,
+ gid,
+ excl,
+ tm_dset,
+ excl_dict=None,
+ inclusion_mask=None,
+ resolution=64,
+ excl_area=None,
+ exclusion_shape=None,
+ close=True,
+ ):
+"""
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | list | tuple | ExclusionMask
+ Filepath(s) to exclusions h5 or ExclusionMask file handler.
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ inclusion_mask : np.ndarray
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. The shape of this will be checked against the input
+ resolution.
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols). Inputing this
+ will speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit.
+ """
+
+ self._excl_dict=excl_dict
+ self._close=close
+ self._excl_fpath,self._excls=self._parse_excl_file(excl)
+
+ ifexclusion_shapeisNone:
+ exclusion_shape=self.exclusions.shape
+
+ super().__init__(gid,exclusion_shape,resolution=resolution)
+
+ self._gids=self._parse_techmap(tm_dset)
+ self._h5_gids=self._gids
+ self._h5_gid_set=None
+
+ self._incl_mask=inclusion_mask
+ self._incl_mask_flat=None
+ ifinclusion_maskisnotNone:
+ msg=(
+ "Bad inclusion mask input shape of {} with stated "
+ "resolution of {}".format(inclusion_mask.shape,resolution)
+ )
+ assertlen(inclusion_mask.shape)==2,msg
+ assertinclusion_mask.shape[0]<=resolution,msg
+ assertinclusion_mask.shape[1]<=resolution,msg
+ assertinclusion_mask.size==len(self._gids),msg
+ self._incl_mask=inclusion_mask.copy()
+
+ self._centroid=None
+ self._excl_area=excl_area
+ self._check_excl()
+
+ @staticmethod
+ def_parse_excl_file(excl):
+"""Parse excl filepath input or handler object and set to attrs.
+
+ Parameters
+ ----------
+ excl : str | ExclusionMask
+ Filepath to exclusions geotiff or ExclusionMask handler
+
+ Returns
+ -------
+ excl_fpath : str | list | tuple
+ Filepath(s) for exclusions file
+ exclusions : ExclusionMask | None
+ Exclusions mask if input is already an open handler or None if it
+ is to be lazy instantiated.
+ """
+
+ ifisinstance(excl,(str,list,tuple)):
+ excl_fpath=excl
+ exclusions=None
+ elifisinstance(excl,ExclusionMask):
+ excl_fpath=excl.excl_h5.h5_file
+ exclusions=excl
+ else:
+ raiseSupplyCurveInputError(
+ "SupplyCurvePoints needs an "
+ "exclusions file path, or "
+ "ExclusionMask handler but "
+ "received: {}".format(type(excl))
+ )
+
+ returnexcl_fpath,exclusions
+
+ def_parse_techmap(self,tm_dset):
+"""Parse data from the tech map file (exclusions to resource mapping).
+ Raise EmptySupplyCurvePointError if there are no valid resource points
+ in this SC point.
+
+ Parameters
+ ----------
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+
+ Returns
+ -------
+ res_gids : np.ndarray
+ 1D array with length == number of exclusion points. reV resource
+ gids (native resource index) from the original resource data
+ corresponding to the tech exclusions.
+ """
+ res_gids=self.exclusions.excl_h5[tm_dset,self.rows,self.cols]
+ res_gids=res_gids.astype(np.int32).flatten()
+
+ if(res_gids!=-1).sum()==0:
+ emsg=(
+ "Supply curve point gid {} has no viable exclusion points "
+ 'based on exclusions file: "{}"'.format(
+ self._gid,self._excl_fpath
+ )
+ )
+ raiseEmptySupplyCurvePointError(emsg)
+
+ returnres_gids
+
+ def__enter__(self):
+ returnself
+
+ def__exit__(self,type,value,traceback):
+ self.close()
+ iftypeisnotNone:
+ raise
+
+
+
+ @property
+ defexclusions(self):
+"""Get the exclusions object.
+
+ Returns
+ -------
+ _excls : ExclusionMask
+ ExclusionMask h5 handler object.
+ """
+ ifself._exclsisNone:
+ self._excls=ExclusionMaskFromDict(
+ self._excl_fpath,layers_dict=self._excl_dict
+ )
+
+ returnself._excls
+
+ @property
+ defcentroid(self):
+"""Get the supply curve point centroid coordinate.
+
+ Returns
+ -------
+ centroid : tuple
+ SC point centroid (lat, lon).
+ """
+
+ ifself._centroidisNone:
+ lats=self.exclusions.excl_h5[LATITUDE,self.rows,self.cols]
+ lons=self.exclusions.excl_h5[LONGITUDE,self.rows,self.cols]
+ self._centroid=(lats.mean(),lons.mean())
+
+ returnself._centroid
+
+ @property
+ defpixel_area(self):
+"""The area in km2 of a single exclusion pixel. If this value was not
+ provided on initialization, it is determined from the profile of the
+ exclusion file.
+
+ Returns
+ -------
+ float
+ """
+ ifself._excl_areaisNone:
+ withExclusionLayers(self._excl_fpath)asf:
+ self._excl_area=f.pixel_area
+ returnself._excl_area
+
+ @property
+ defarea(self):
+"""Get the non-excluded resource area of the supply curve point in the
+ current resource class.
+
+ Returns
+ -------
+ area : float
+ Non-excluded resource/generation area in square km.
+ """
+ mask=self._gids!=-1
+ area=np.sum(self.include_mask_flat[mask])*self.pixel_area
+
+ returnarea
+
+ @property
+ deflatitude(self):
+"""Get the SC point latitude"""
+ returnself.centroid[0]
+
+ @property
+ deflongitude(self):
+"""Get the SC point longitude"""
+ returnself.centroid[1]
+
+ @property
+ defn_gids(self):
+"""
+ Get the total number of not fully excluded pixels associated with the
+ available resource/generation gids at the given sc gid.
+
+ Returns
+ -------
+ n_gids : list
+ """
+ mask=self._gids!=-1
+ n_gids=np.sum(self.include_mask_flat[mask]>0)
+
+ returnn_gids
+
+ @property
+ definclude_mask(self):
+"""Get the 2D inclusion mask (normalized with expected range: [0, 1]
+ where 1 is included and 0 is excluded).
+
+ Returns
+ -------
+ np.ndarray
+ """
+
+ ifself._incl_maskisNone:
+ self._incl_mask=self.exclusions[self.rows,self.cols]
+
+ # make sure exclusion pixels outside resource extent are excluded
+ out_of_extent=self._gids.reshape(self._incl_mask.shape)==-1
+ self._incl_mask[out_of_extent]=0.0
+
+ ifself._incl_mask.max()>1:
+ w=(
+ "Exclusions data max value is > 1: {}".format(
+ self._incl_mask.max()
+ ),
+ InputWarning,
+ )
+ logger.warning(w)
+ warn(w)
+
+ returnself._incl_mask
+
+ @property
+ definclude_mask_flat(self):
+"""Get the flattened inclusion mask (normalized with expected
+ range: [0, 1] where 1 is included and 0 is excluded).
+
+ Returns
+ -------
+ np.ndarray
+ """
+
+ ifself._incl_mask_flatisNone:
+ self._incl_mask_flat=self.include_mask.flatten()
+
+ returnself._incl_mask_flat
+
+ @property
+ defbool_mask(self):
+"""Get a boolean inclusion mask (True if excl point is not excluded).
+
+ Returns
+ -------
+ mask : np.ndarray
+ Mask with length equal to the flattened exclusion shape
+ """
+ returnself._gids!=-1
+
+ @property
+ defh5(self):
+"""
+ placeholder for h5 Resource handler object
+ """
+
+ @property
+ defh5_gid_set(self):
+"""Get list of unique h5 gids corresponding to this sc point.
+
+ Returns
+ -------
+ h5_gids : list
+ List of h5 gids.
+ """
+ ifself._h5_gid_setisNone:
+ self._h5_gid_set=self._ordered_unique(self._h5_gids)
+ if-1inself._h5_gid_set:
+ self._h5_gid_set.remove(-1)
+
+ returnself._h5_gid_set
+
+ @property
+ defsummary(self):
+"""
+ Placeholder for Supply curve point's meta data summary
+ """
+
+ def_check_excl(self):
+"""
+ Check to see if supply curve point is fully excluded
+ """
+
+ ifall(self.include_mask_flat[self.bool_mask]==0):
+ msg="Supply curve point gid {} is completely excluded!".format(
+ self._gid
+ )
+ raiseEmptySupplyCurvePointError(msg)
+
+
[docs]defexclusion_weighted_mean(self,arr,drop_nan=True):
+"""
+ Calc the exclusions-weighted mean value of an array of resource data.
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ Array of resource data.
+ drop_nan : bool
+ Flag to drop nan values from the mean calculation (only works for
+ 1D arr input, profiles should not have NaN's)
+
+ Returns
+ -------
+ mean : float | np.ndarray
+ Mean of arr masked by the binary exclusions then weighted by
+ the non-zero exclusions. This will be a 1D numpy array if the
+ input data is a 2D numpy array (averaged along axis=1)
+ """
+
+ iflen(arr.shape)==2:
+ x=arr[:,self._gids[self.bool_mask]].astype("float32")
+ incl=self.include_mask_flat[self.bool_mask]
+ x*=incl
+ mean=x.sum(axis=1)/incl.sum()
+
+ else:
+ x=arr[self._gids[self.bool_mask]].astype("float32")
+ incl=self.include_mask_flat[self.bool_mask]
+
+ ifnp.isnan(x).all():
+ returnnp.nan
+ ifdrop_nanandnp.isnan(x).any():
+ nan_mask=np.isnan(x)
+ x=x[~nan_mask]
+ incl=incl[~nan_mask]
+
+ x*=incl
+ mean=x.sum()/incl.sum()
+
+ returnmean
+
+
[docs]defmean_wind_dirs(self,arr):
+"""
+ Calc the mean wind directions at every time-step
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ Array of wind direction data.
+
+ Returns
+ -------
+ mean_wind_dirs : np.ndarray | float
+ Mean wind direction of arr masked by the binary exclusions
+ """
+ incl=self.include_mask_flat[self.bool_mask]
+ gids=self._gids[self.bool_mask]
+ iflen(arr.shape)==2:
+ arr_slice=(slice(None),gids)
+ ax=1
+
+ else:
+ arr_slice=gids
+ ax=0
+
+ angle=np.radians(arr[arr_slice],dtype=np.float32)
+ sin=np.mean(np.sin(angle)*incl,axis=ax)
+ cos=np.mean(np.cos(angle)*incl,axis=ax)
+
+ mean_wind_dirs=np.degrees(np.arctan2(sin,cos))
+ mask=mean_wind_dirs<0
+ mean_wind_dirs[mask]+=360
+
+ returnmean_wind_dirs
+
+
[docs]defaggregate(self,arr):
+"""
+ Calc sum (aggregation) of the resource data.
+
+ Parameters
+ ----------
+ arr : np.ndarray
+ Array of resource data.
+
+ Returns
+ -------
+ agg : float
+ Sum of arr masked by the binary exclusions
+ """
+ iflen(arr.shape)==2:
+ x=arr[:,self._gids[self.bool_mask]].astype("float32")
+ ax=1
+ else:
+ x=arr[self._gids[self.bool_mask]].astype("float32")
+ ax=0
+
+ x*=self.include_mask_flat[self.bool_mask]
+ agg=x.sum(axis=ax)
+
+ returnagg
+
+
[docs]@classmethod
+ defsc_mean(
+ cls,
+ gid,
+ excl,
+ tm_dset,
+ data,
+ excl_dict=None,
+ resolution=64,
+ exclusion_shape=None,
+ close=True,
+ ):
+"""
+ Compute exclusions weight mean for the sc point from data
+
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | ExclusionMask
+ Filepath to exclusions h5 or ExclusionMask file handler.
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ data : ndarray | ResourceDataset
+ Array of data or open dataset handler to apply exclusions too
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols). Inputing this
+ will speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit
+
+ Returns
+ -------
+ ndarray
+ Exclusions weighted means of data for supply curve point
+ """
+ kwargs={
+ "excl_dict":excl_dict,
+ "resolution":resolution,
+ "exclusion_shape":exclusion_shape,
+ "close":close,
+ }
+ withcls(gid,excl,tm_dset,**kwargs)aspoint:
+ means=point.exclusion_weighted_mean(data)
+
+ returnmeans
+
+
[docs]@classmethod
+ defsc_sum(
+ cls,
+ gid,
+ excl,
+ tm_dset,
+ data,
+ excl_dict=None,
+ resolution=64,
+ exclusion_shape=None,
+ close=True,
+ ):
+"""
+ Compute the aggregate (sum) of data for the sc point
+
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | ExclusionMask
+ Filepath to exclusions h5 or ExclusionMask file handler.
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ data : ndarray | ResourceDataset
+ Array of data or open dataset handler to apply exclusions too
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols). Inputing this
+ will speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit.
+
+ Returns
+ -------
+ ndarray
+ Sum / aggregation of data for supply curve point
+ """
+ kwargs={
+ "excl_dict":excl_dict,
+ "resolution":resolution,
+ "exclusion_shape":exclusion_shape,
+ "close":close,
+ }
+ withcls(gid,excl,tm_dset,**kwargs)aspoint:
+ agg=point.aggregate(data)
+
+ returnagg
+
+ @staticmethod
+ def_mode(data):
+"""
+ Compute the mode of the data vector and return a single value
+
+ Parameters
+ ----------
+ data : ndarray
+ data layer vector to compute mode for
+
+ Returns
+ -------
+ float | int
+ Mode of data
+ """
+ ifnotdata.size:
+ returnNone
+ # pd series is more flexible with non-numeric than stats mode
+ returnpd.Series(data).mode().values[0]
+
+ @staticmethod
+ def_categorize(data,incl_mult):
+"""
+ Extract the sum of inclusion scalar values (where 1 is
+ included, 0 is excluded, and 0.7 is included with 70 percent of
+ available land) for each unique (categorical value) in data
+
+ Parameters
+ ----------
+ data : ndarray
+ Vector of categorical values
+ incl_mult : ndarray
+ Vector of inclusion values
+
+ Returns
+ -------
+ str
+ Jsonified string of the dictionary mapping categorical values to
+ total inclusions
+ """
+
+ data={
+ category:float(incl_mult[(data==category)].sum())
+ forcategoryinnp.unique(data)
+ }
+ data=jsonify_dict(data)
+
+ returndata
+
+ @classmethod
+ def_agg_data_layer_method(cls,data,incl_mult,method):
+"""Aggregate the data array using specified method.
+
+ Parameters
+ ----------
+ data : np.ndarray | None
+ Data array that will be flattened and operated on using method.
+ This must be the included data. Exclusions should be applied
+ before this method.
+ incl_mult : np.ndarray | None
+ Scalar exclusion data for methods with exclusion-weighted
+ aggregation methods. Shape must match input data.
+ method : str
+ Aggregation method (mode, mean, max, min, sum, category)
+
+ Returns
+ -------
+ data : float | int | str | None
+ Result of applying method to data.
+ """
+ method_func={
+ "mode":cls._mode,
+ "mean":np.mean,
+ "max":np.max,
+ "min":np.min,
+ "sum":np.sum,
+ "category":cls._categorize,
+ }
+
+ ifdataisnotNone:
+ method=method.lower()
+ ifmethodnotinmethod_func:
+ e=(
+ "Cannot recognize data layer agg method: "
+ '"{}". Can only {}'.format(method,list(method_func))
+ )
+ logger.error(e)
+ raiseValueError(e)
+
+ iflen(data.shape)>1:
+ data=data.flatten()
+
+ ifdata.shape!=incl_mult.shape:
+ e=(
+ "Cannot aggregate data with shape that doesnt "
+ "match excl mult!"
+ )
+ logger.error(e)
+ raiseDataShapeError(e)
+
+ ifmethod=="category":
+ data=method_func["category"](data,incl_mult)
+ elifmethodin["mean","sum"]:
+ data=data*incl_mult
+ data=method_func[method](data)
+ else:
+ data=method_func[method](data)
+
+ returndata
+
+
[docs]defagg_data_layers(self,summary,data_layers):
+"""Perform additional data layer aggregation. If there is no valid data
+ in the included area, the data layer will be taken from the full SC
+ point extent (ignoring exclusions). If there is still no valid data,
+ a warning will be raised and the data layer will have a NaN/None value.
+
+ Parameters
+ ----------
+ summary : dict
+ Dictionary of summary outputs for this sc point.
+ data_layers : None | dict
+ Aggregation data layers. Must be a dictionary keyed by data label
+ name. Each value must be another dictionary with "dset", "method",
+ and "fpath".
+
+ Returns
+ -------
+ summary : dict
+ Dictionary of summary outputs for this sc point. A new entry for
+ each data layer is added.
+ """
+
+ ifdata_layersisnotNone:
+ forname,attrsindata_layers.items():
+ excl_fp=attrs.get("fpath",self._excl_fpath)
+ ifexcl_fp!=self._excl_fpath:
+ fh=ExclusionLayers(attrs["fpath"])
+ else:
+ fh=self.exclusions.excl_h5
+
+ raw=fh[attrs["dset"],self.rows,self.cols]
+ nodata=fh.get_nodata_value(attrs["dset"])
+
+ data=raw.flatten()[self.bool_mask]
+ incl_mult=self.include_mask_flat[self.bool_mask].copy()
+
+ ifnodataisnotNone:
+ valid_data_mask=data!=nodata
+ data=data[valid_data_mask]
+ incl_mult=incl_mult[valid_data_mask]
+
+ ifnotdata.size:
+ m=(
+ 'Data layer "{}" has no valid data for '
+ "SC point gid {} because of exclusions "
+ "and/or nodata values in the data layer.".format(
+ name,self._gid
+ )
+ )
+ logger.debug(m)
+
+ data=self._agg_data_layer_method(
+ data,incl_mult,attrs["method"]
+ )
+ summary[name]=data
+
+ ifexcl_fp!=self._excl_fpath:
+ fh.close()
+
+ returnsummary
+
+
+
[docs]classAggregationSupplyCurvePoint(SupplyCurvePoint):
+"""Generic single SC point to aggregate data from an h5 file."""
+
+ def__init__(
+ self,
+ gid,
+ excl,
+ agg_h5,
+ tm_dset,
+ excl_dict=None,
+ inclusion_mask=None,
+ resolution=64,
+ excl_area=None,
+ exclusion_shape=None,
+ close=True,
+ gen_index=None,
+ apply_exclusions=True,
+ ):
+"""
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | ExclusionMask
+ Filepath to exclusions h5 or ExclusionMask file handler.
+ agg_h5 : str | Resource
+ Filepath to .h5 file to aggregate or Resource handler
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ inclusion_mask : np.ndarray
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. The shape of this will be checked against the input
+ resolution.
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols). Inputing this
+ will speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit.
+ gen_index : np.ndarray
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run.
+ apply_exclusions : bool
+ Flag to apply exclusions to the resource / generation gid's on
+ initialization.
+ """
+ super().__init__(
+ gid,
+ excl,
+ tm_dset,
+ excl_dict=excl_dict,
+ inclusion_mask=inclusion_mask,
+ resolution=resolution,
+ excl_area=excl_area,
+ exclusion_shape=exclusion_shape,
+ close=close,
+ )
+
+ self._h5_fpath,self._h5=self._parse_h5_file(agg_h5)
+
+ ifgen_indexisnotNone:
+ self._gids,_=self._map_gen_gids(self._gids,gen_index)
+
+ self._h5_gids=self._gids
+
+ if(self._h5_gids!=-1).sum()==0:
+ emsg=(
+ "Supply curve point gid {} has no viable exclusion "
+ 'points based on exclusions file: "{}"'.format(
+ self._gid,self._excl_fpath
+ )
+ )
+ raiseEmptySupplyCurvePointError(emsg)
+
+ ifapply_exclusions:
+ self._apply_exclusions()
+
+ @staticmethod
+ def_parse_h5_file(h5):
+"""
+ Parse .h5 filepath input or handler object and set to attrs.
+
+ Parameters
+ ----------
+ h5 : str | Resource
+ Filepath to .h5 file to aggregate or Resource handler
+
+ Returns
+ -------
+ h5_fpath : str
+ Filepath for .h5 file to aggregate
+ h5 : Resource | None
+ Resource if input is already an open handler or None if it
+ is to be lazy instantiated.
+ """
+
+ ifisinstance(h5,str):
+ h5_fpath=h5
+ h5=None
+ elifissubclass(h5.__class__,BaseResource):
+ h5_fpath=h5.h5_file
+ elifissubclass(h5.__class__,MultiTimeResource):
+ h5_fpath=h5.h5_files
+ else:
+ raiseSupplyCurveInputError(
+ "SupplyCurvePoints needs a "
+ ".h5 file path, or "
+ "Resource handler but "
+ "received: {}".format(type(h5))
+ )
+
+ returnh5_fpath,h5
+
+ def_apply_exclusions(self):
+"""Apply exclusions by masking the generation and resource gid arrays.
+ This removes all res/gen entries that are masked by the exclusions or
+ resource bin."""
+
+ # exclusions mask is False where excluded
+ exclude=self.include_mask_flat==0
+
+ self._gids[exclude]=-1
+ self._h5_gids[exclude]=-1
+
+ if(self._gids!=-1).sum()==0:
+ msg="Supply curve point gid {} is completely excluded!".format(
+ self._gid
+ )
+ raiseEmptySupplyCurvePointError(msg)
+
+
+
+ @staticmethod
+ def_map_gen_gids(res_gids,gen_index):
+"""
+ Map resource gids from techmap to gen gids in .h5 source file
+
+ Parameters
+ ----------
+ res_gids : ndarray
+ resource gids from techmap
+ gen_index : ndarray
+ Equivalent gen gids to resource gids
+
+ Returns
+ -------
+ gen_gids : ndarray
+ gen gid to excl mapping
+ res_gids : ndarray
+ updated resource gid to excl mapping
+ """
+ mask=(res_gids>=len(gen_index))|(res_gids==-1)
+ res_gids[mask]=-1
+ gen_gids=gen_index[res_gids]
+ gen_gids[mask]=-1
+ res_gids[(gen_gids==-1)]=-1
+
+ returngen_gids,res_gids
+
+ @property
+ defh5(self):
+"""
+ h5 Resource handler object
+
+ Returns
+ -------
+ _h5 : Resource
+ Resource h5 handler object.
+ """
+ ifself._h5isNoneand"*"inself._h5_fpath:
+ self._h5=MultiTimeResource(self._h5_fpath)
+ elifself._h5isNone:
+ self._h5=Resource(self._h5_fpath)
+
+ returnself._h5
+
+ @property
+ defcountry(self):
+"""Get the SC point country based on the resource meta data."""
+ country=None
+ county_not_none=self.countyisnotNone
+ ifResourceMetaField.COUNTRYinself.h5.metaandcounty_not_none:
+ # make sure country and county are coincident
+ counties=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTY
+ ].values
+ iloc=np.where(counties==self.county)[0][0]
+ country=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTRY
+ ].values
+ country=country[iloc]
+
+ elifResourceMetaField.COUNTRYinself.h5.meta:
+ country=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTRY
+ ].mode()
+ country=country.values[0]
+
+ returncountry
+
+ @property
+ defstate(self):
+"""Get the SC point state based on the resource meta data."""
+ state=None
+ ifResourceMetaField.STATEinself.h5.metaandself.countyisnotNone:
+ # make sure state and county are coincident
+ counties=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTY
+ ].values
+ iloc=np.where(counties==self.county)[0][0]
+ state=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.STATE
+ ].values
+ state=state[iloc]
+
+ elifResourceMetaField.STATEinself.h5.meta:
+ state=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.STATE
+ ].mode()
+ state=state.values[0]
+
+ returnstate
+
+ @property
+ defcounty(self):
+"""Get the SC point county based on the resource meta data."""
+ county=None
+ ifResourceMetaField.COUNTYinself.h5.meta:
+ county=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTY
+ ].mode()
+ county=county.values[0]
+
+ returncounty
+
+ @property
+ defelevation(self):
+"""Get the SC point elevation based on the resource meta data."""
+ elevation=None
+ ifResourceMetaField.ELEVATIONinself.h5.meta:
+ elevation=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.ELEVATION
+ ].mean()
+
+ returnelevation
+
+ @property
+ deftimezone(self):
+"""Get the SC point timezone based on the resource meta data."""
+ timezone=None
+ county_not_none=self.countyisnotNone
+ ifResourceMetaField.TIMEZONEinself.h5.metaandcounty_not_none:
+ # make sure timezone flag and county are coincident
+ counties=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTY
+ ].values
+ iloc=np.where(counties==self.county)[0][0]
+ timezone=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.TIMEZONE
+ ].values
+ timezone=timezone[iloc]
+
+ elifResourceMetaField.TIMEZONEinself.h5.meta:
+ timezone=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.TIMEZONE
+ ].mode()
+ timezone=timezone.values[0]
+
+ returntimezone
+
+ @property
+ defoffshore(self):
+"""Get the SC point offshore flag based on the resource meta data
+ (if offshore column is present)."""
+ offshore=None
+ county_not_none=self.countyisnotNone
+ ifResourceMetaField.OFFSHOREinself.h5.metaandcounty_not_none:
+ # make sure offshore flag and county are coincident
+ counties=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.COUNTY
+ ].values
+ iloc=np.where(counties==self.county)[0][0]
+ offshore=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.OFFSHORE
+ ].values
+ offshore=offshore[iloc]
+
+ elifResourceMetaField.OFFSHOREinself.h5.meta:
+ offshore=self.h5.meta.loc[
+ self.h5_gid_set,ResourceMetaField.OFFSHORE
+ ].mode()
+ offshore=offshore.values[0]
+
+ returnoffshore
+
+ @property
+ defgid_counts(self):
+"""Get the sum of the inclusion values in each resource/generation gid
+ corresponding to this sc point. The sum of the gid counts can be less
+ than the value provided by n_gids if fractional exclusion/inclusions
+ are provided.
+
+ Returns
+ -------
+ gid_counts : list
+ """
+ gid_counts=[
+ self.include_mask_flat[(self._h5_gids==gid)].sum()
+ forgidinself.h5_gid_set
+ ]
+
+ returngid_counts
+
+ @property
+ defsummary(self):
+"""
+ Supply curve point's meta data summary
+
+ Returns
+ -------
+ pandas.Series
+ List of supply curve point's meta data
+ """
+ meta={
+ SupplyCurveField.SC_POINT_GID:self.sc_point_gid,
+ SupplyCurveField.SOURCE_GIDS:self.h5_gid_set,
+ SupplyCurveField.GID_COUNTS:self.gid_counts,
+ SupplyCurveField.N_GIDS:self.n_gids,
+ SupplyCurveField.AREA_SQ_KM:self.area,
+ SupplyCurveField.LATITUDE:self.latitude,
+ SupplyCurveField.LONGITUDE:self.longitude,
+ SupplyCurveField.COUNTRY:self.country,
+ SupplyCurveField.STATE:self.state,
+ SupplyCurveField.COUNTY:self.county,
+ SupplyCurveField.ELEVATION:self.elevation,
+ SupplyCurveField.TIMEZONE:self.timezone,
+ }
+ meta=pd.Series(meta)
+
+ returnmeta
+
+
[docs]@classmethod
+ defrun(
+ cls,
+ gid,
+ excl,
+ agg_h5,
+ tm_dset,
+ *agg_dset,
+ agg_method="mean",
+ excl_dict=None,
+ inclusion_mask=None,
+ resolution=64,
+ excl_area=None,
+ exclusion_shape=None,
+ close=True,
+ gen_index=None,
+ ):
+"""
+ Compute exclusions weight mean for the sc point from data
+
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | ExclusionMask
+ Filepath to exclusions h5 or ExclusionMask file handler.
+ agg_h5 : str | Resource
+ Filepath to .h5 file to aggregate or Resource handler
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ agg_dset : str
+ Dataset to aggreate, can supply multiple datasets or no datasets.
+ The datasets should be scalar values for each site. This method
+ cannot aggregate timeseries data.
+ agg_method : str
+ Aggregation method, either mean or sum/aggregate
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ inclusion_mask : np.ndarray
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. The shape of this will be checked against the input
+ resolution.
+ resolution : int
+ Number of exclusion points per SC point along an axis.
+ This number**2 is the total number of exclusion points per
+ SC point.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ exclusion_shape : tuple
+ Shape of the full exclusions extent (rows, cols). Inputing this
+ will speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit.
+ gen_index : np.ndarray
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run.
+
+ Returns
+ -------
+ out : dict
+ Given datasets and meta data aggregated to supply curve points
+ """
+ ifisinstance(agg_dset,str):
+ agg_dset=(agg_dset,)
+
+ kwargs={
+ "excl_dict":excl_dict,
+ "inclusion_mask":inclusion_mask,
+ "resolution":resolution,
+ "excl_area":excl_area,
+ "exclusion_shape":exclusion_shape,
+ "close":close,
+ "gen_index":gen_index,
+ }
+
+ withcls(gid,excl,agg_h5,tm_dset,**kwargs)aspoint:
+ ifagg_method.lower().startswith("mean"):
+ agg_method=point.exclusion_weighted_mean
+ elifagg_method.lower().startswith(("sum","agg")):
+ agg_method=point.aggregate
+ elif"wind_dir"inagg_method.lower():
+ agg_method=point.mean_wind_dirs
+ else:
+ msg=(
+ "Aggregation method must be either mean, "
+ "sum/aggregate, or wind_dir"
+ )
+ logger.error(msg)
+ raiseValueError(msg)
+
+ out={"meta":point.summary}
+
+ fordsetinagg_dset:
+ ds=point.h5.open_dataset(dset)
+ out[dset]=agg_method(ds)
+
+ returnout
+
+
+
[docs]classGenerationSupplyCurvePoint(AggregationSupplyCurvePoint):
+"""Supply curve point summary framework that ties a reV SC point to its
+ respective generation and resource data."""
+
+ # technology-dependent power density estimates in MW/km2
+ POWER_DENSITY={"pv":36,"wind":3}
+
+ def__init__(
+ self,
+ gid,
+ excl,
+ gen,
+ tm_dset,
+ gen_index,
+ excl_dict=None,
+ inclusion_mask=None,
+ res_class_dset=None,
+ res_class_bin=None,
+ excl_area=None,
+ power_density=None,
+ cf_dset="cf_mean-means",
+ lcoe_dset="lcoe_fcr-means",
+ h5_dsets=None,
+ resolution=64,
+ exclusion_shape=None,
+ close=False,
+ friction_layer=None,
+ recalc_lcoe=True,
+ apply_exclusions=True,
+ ):
+"""
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl : str | ExclusionMask
+ Filepath to exclusions h5 or ExclusionMask file handler.
+ gen : str | reV.handlers.Outputs
+ Filepath to .h5 reV generation output results or reV Outputs file
+ handler.
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ gen_index : np.ndarray
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ inclusion_mask : np.ndarray
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. The shape of this will be checked against the input
+ resolution.
+ res_class_dset : str | np.ndarray | None
+ Dataset in the generation file dictating resource classes.
+ Can be pre-extracted resource data in np.ndarray.
+ None if no resource classes.
+ res_class_bin : list | None
+ Two-entry lists dictating the single resource class bin.
+ None if no resource classes.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ power_density : float | None | pd.DataFrame
+ Constant power density float, None, or opened dataframe with
+ (resource) "gid" and "power_density columns".
+ cf_dset : str | np.ndarray
+ Dataset name from gen containing capacity factor mean values.
+ This name is used to infer AC capacity factor dataset for
+ solar runs (i.e. the AC vsersion of "cf_mean-means" would
+ be inferred to be "cf_mean_ac-means"). This input can also
+ be pre-extracted generation output data in np.ndarray, in
+ which case all DC solar outputs are set to `None`.
+ lcoe_dset : str | np.ndarray
+ Dataset name from gen containing LCOE mean values.
+ Can be pre-extracted generation output data in np.ndarray.
+ h5_dsets : None | list | dict
+ Optional list of dataset names to summarize from the gen/econ h5
+ files. Can also be pre-extracted data dictionary where keys are
+ the dataset names and values are the arrays of data from the
+ h5 files.
+ resolution : int | None
+ SC resolution, must be input in combination with gid.
+ exclusion_shape : tuple
+ Shape of the exclusions extent (rows, cols). Inputing this will
+ speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit.
+ friction_layer : None | FrictionMask
+ Friction layer with scalar friction values if valid friction inputs
+ were entered. Otherwise, None to not apply friction layer.
+ recalc_lcoe : bool
+ Flag to re-calculate the LCOE from the multi-year mean capacity
+ factor and annual energy production data. This requires several
+ datasets to be aggregated in the gen input: system_capacity,
+ fixed_charge_rate, capital_cost, fixed_operating_cost,
+ and variable_operating_cost.
+ apply_exclusions : bool
+ Flag to apply exclusions to the resource / generation gid's on
+ initialization.
+ """
+
+ self._res_class_dset=res_class_dset
+ self._res_class_bin=res_class_bin
+ self._cf_dset=cf_dset
+ self._lcoe_dset=lcoe_dset
+ self._h5_dsets=h5_dsets
+ self._mean_res=None
+ self._res_data=None
+ self._gen_data=None
+ self._lcoe_data=None
+ self._pd_obj=None
+ self._power_density=self._power_density_ac=power_density
+ self._friction_layer=friction_layer
+ self._recalc_lcoe=recalc_lcoe
+ self._ssc=None
+ self._slk={}
+
+ super().__init__(
+ gid,
+ excl,
+ gen,
+ tm_dset,
+ excl_dict=excl_dict,
+ inclusion_mask=inclusion_mask,
+ resolution=resolution,
+ excl_area=excl_area,
+ exclusion_shape=exclusion_shape,
+ close=close,
+ apply_exclusions=False,
+ )
+
+ self._res_gid_set=None
+ self._gen_gid_set=None
+
+ self._gen_fpath,self._gen=self._h5_fpath,self._h5
+
+ self._gen_gids,self._res_gids=self._map_gen_gids(
+ self._gids,gen_index
+ )
+ self._gids=self._gen_gids
+ if(self._gen_gids!=-1).sum()==0:
+ emsg=(
+ "Supply curve point gid {} has no viable exclusion "
+ 'points based on exclusions file: "{}"'.format(
+ self._gid,self._excl_fpath
+ )
+ )
+ raiseEmptySupplyCurvePointError(emsg)
+
+ ifapply_exclusions:
+ self._apply_exclusions()
+
+
[docs]defexclusion_weighted_mean(self,flat_arr):
+"""Calc the exclusions-weighted mean value of a flat array of gen data.
+
+ Parameters
+ ----------
+ flat_arr : np.ndarray
+ Flattened array of resource/generation/econ data. Must be
+ index-able with the self._gen_gids array (must be a 1D array with
+ an entry for every site in the generation extent).
+
+ Returns
+ -------
+ mean : float
+ Mean of flat_arr masked by the binary exclusions then weighted by
+ the non-zero exclusions.
+ """
+ x=flat_arr[self._gen_gids[self.bool_mask]].astype("float32")
+ incl=self.include_mask_flat[self.bool_mask]
+ x*=incl
+ mean=x.sum()/incl.sum()
+
+ returnmean
+
+ @property
+ defgen(self):
+"""Get the generation output object.
+
+ Returns
+ -------
+ _gen : Resource
+ reV generation Resource object
+ """
+ ifself._genisNone:
+ self._gen=Resource(self._gen_fpath,str_decode=False)
+
+ returnself._gen
+
+ @property
+ defres_gid_set(self):
+"""Get list of unique resource gids corresponding to this sc point.
+
+ Returns
+ -------
+ res_gids : list
+ List of resource gids.
+ """
+ ifself._res_gid_setisNone:
+ self._res_gid_set=self._ordered_unique(self._res_gids)
+ if-1inself._res_gid_set:
+ self._res_gid_set.remove(-1)
+
+ returnself._res_gid_set
+
+ @property
+ defgen_gid_set(self):
+"""Get list of unique generation gids corresponding to this sc point.
+
+ Returns
+ -------
+ gen_gids : list
+ List of generation gids.
+ """
+ ifself._gen_gid_setisNone:
+ self._gen_gid_set=self._ordered_unique(self._gen_gids)
+ if-1inself._gen_gid_set:
+ self._gen_gid_set.remove(-1)
+
+ returnself._gen_gid_set
+
+ @property
+ defh5_gid_set(self):
+"""Get list of unique h5 gids corresponding to this sc point.
+ Same as gen_gid_set
+
+ Returns
+ -------
+ h5_gids : list
+ List of h5 gids.
+ """
+ returnself.gen_gid_set
+
+ @property
+ defgid_counts(self):
+"""Get the number of exclusion pixels in each resource/generation gid
+ corresponding to this sc point.
+
+ Returns
+ -------
+ gid_counts : list
+ List of exclusion pixels in each resource/generation gid.
+ """
+ gid_counts=[
+ self.include_mask_flat[(self._res_gids==gid)].sum()
+ forgidinself.res_gid_set
+ ]
+
+ returngid_counts
+
+ @property
+ defres_data(self):
+"""Get the resource data array.
+
+ Returns
+ -------
+ _res_data : np.ndarray
+ Multi-year-mean resource data array for all sites in the
+ generation data output file.
+ """
+
+ ifisinstance(self._res_class_dset,np.ndarray):
+ returnself._res_class_dset
+
+ ifself._res_dataisNone:
+ ifself._res_class_dsetinself.gen.datasets:
+ self._res_data=self.gen[self._res_class_dset]
+
+ returnself._res_data
+
+ @property
+ defgen_data(self):
+"""Get the generation capacity factor data array.
+
+ Returns
+ -------
+ _gen_data : np.ndarray
+ Multi-year-mean capacity factor data array for all sites in the
+ generation data output file.
+ """
+
+ ifisinstance(self._cf_dset,np.ndarray):
+ returnself._cf_dset
+
+ ifself._gen_dataisNone:
+ ifself._cf_dsetinself.gen.datasets:
+ self._gen_data=self.gen[self._cf_dset]
+
+ returnself._gen_data
+
+ @property
+ defgen_ac_data(self):
+"""Get the generation ac capacity factor data array.
+
+ This output is only not `None` for solar runs where `cf_dset`
+ was specified as a string.
+
+ Returns
+ -------
+ gen_ac_data : np.ndarray | None
+ Multi-year-mean ac capacity factor data array for all sites
+ in the generation data output file or `None` if none
+ detected.
+ """
+
+ ifisinstance(self._cf_dset,np.ndarray):
+ returnNone
+
+ ac_cf_dset=_infer_cf_dset_ac(self._cf_dset)
+ ifac_cf_dsetinself.gen.datasets:
+ returnself.gen[ac_cf_dset]
+
+ returnNone
+
+ @property
+ deflcoe_data(self):
+"""Get the LCOE data array.
+
+ Returns
+ -------
+ _lcoe_data : np.ndarray
+ Multi-year-mean LCOE data array for all sites in the
+ generation data output file.
+ """
+
+ ifisinstance(self._lcoe_dset,np.ndarray):
+ returnself._lcoe_dset
+
+ ifself._lcoe_dataisNone:
+ ifself._lcoe_dsetinself.gen.datasets:
+ self._lcoe_data=self.gen[self._lcoe_dset]
+
+ returnself._lcoe_data
+
+ @property
+ defmean_cf(self):
+"""Get the mean capacity factor for the non-excluded data. Capacity
+ factor is weighted by the exclusions (usually 0 or 1, but 0.5
+ exclusions will weight appropriately).
+
+ This value represents DC capacity factor for solar and AC
+ capacity factor for all other technologies. This is the capacity
+ factor that should be used for all cost calculations for ALL
+ technologies (to align with SAM).
+
+ Returns
+ -------
+ mean_cf : float | None
+ Mean capacity factor value for the non-excluded data.
+ """
+ mean_cf=None
+ ifself.gen_dataisnotNone:
+ mean_cf=self.exclusion_weighted_mean(self.gen_data)
+
+ returnmean_cf
+
+ @property
+ defmean_cf_ac(self):
+"""Get the mean AC capacity factor for the non-excluded data.
+
+ This output is only not `None` for solar runs.
+
+ Capacity factor is weighted by the exclusions (usually 0 or 1,
+ but 0.5 exclusions will weight appropriately).
+
+ Returns
+ -------
+ mean_cf_ac : float | None
+ Mean capacity factor value for the non-excluded data.
+ """
+ mean_cf_ac=None
+ ifself.gen_ac_dataisnotNone:
+ mean_cf_ac=self.exclusion_weighted_mean(self.gen_ac_data)
+
+ returnmean_cf_ac
+
+ @property
+ defmean_cf_dc(self):
+"""Get the mean DC capacity factor for the non-excluded data.
+
+ This output is only not `None` for solar runs.
+
+ Capacity factor is weighted by the exclusions (usually 0 or 1,
+ but 0.5 exclusions will weight appropriately).
+
+ Returns
+ -------
+ mean_cf_dc : float | None
+ Mean capacity factor value for the non-excluded data.
+ """
+ ifself.mean_cf_acisnotNone:
+ returnself.mean_cf
+
+ returnNone
+
+ @property
+ defmean_lcoe(self):
+"""Get the mean LCOE for the non-excluded data.
+
+ Returns
+ -------
+ mean_lcoe : float | None
+ Mean LCOE value for the non-excluded data.
+ """
+
+ mean_lcoe=None
+
+ # prioritize the calculation of lcoe explicitly from the multi year
+ # mean CF (the lcoe re-calc will still happen if mean_cf is a single
+ # year CF, but the output should be identical to the original LCOE and
+ # so is not consequential).
+ ifself._recalc_lcoe:
+ required=("fixed_charge_rate","capital_cost",
+ "fixed_operating_cost","variable_operating_cost",
+ "system_capacity")
+ ifall(self._sam_lcoe_kwargs.get(k)isnotNoneforkinrequired):
+ aep=(
+ self._sam_lcoe_kwargs["system_capacity"]
+ *self.mean_cf
+ *8760
+ )
+ # Note the AEP computation uses the SAM config
+ # `system_capacity`, so no need to scale `capital_cost`
+ # or `fixed_operating_cost` by anything
+ mean_lcoe=lcoe_fcr(
+ self._sam_lcoe_kwargs["fixed_charge_rate"],
+ self._sam_lcoe_kwargs["capital_cost"],
+ self._sam_lcoe_kwargs["fixed_operating_cost"],
+ aep,
+ self._sam_lcoe_kwargs["variable_operating_cost"],
+ )
+
+ # alternative if lcoe was not able to be re-calculated from
+ # multi year mean CF
+ ifmean_lcoeisNoneandself.lcoe_dataisnotNone:
+ mean_lcoe=self.exclusion_weighted_mean(self.lcoe_data)
+
+ returnmean_lcoe
+
+ @property
+ defmean_res(self):
+"""Get the mean resource for the non-excluded data.
+
+ Returns
+ -------
+ mean_res : float | None
+ Mean resource for the non-excluded data.
+ """
+ mean_res=None
+ ifself._res_class_dsetisnotNone:
+ mean_res=self.exclusion_weighted_mean(self.res_data)
+
+ returnmean_res
+
+ @property
+ defmean_lcoe_friction(self):
+"""Get the mean LCOE for the non-excluded data, multiplied by the
+ mean_friction scalar value.
+
+ Returns
+ -------
+ mean_lcoe_friction : float | None
+ Mean LCOE value for the non-excluded data multiplied by the
+ mean friction scalar value.
+ """
+ mean_lcoe_friction=None
+ ifself.mean_lcoeisnotNoneandself.mean_frictionisnotNone:
+ mean_lcoe_friction=self.mean_lcoe*self.mean_friction
+
+ returnmean_lcoe_friction
+
+ @property
+ defmean_friction(self):
+"""Get the mean friction scalar for the non-excluded data.
+
+ Returns
+ -------
+ friction : None | float
+ Mean value of the friction data layer for the non-excluded data.
+ If friction layer is not input to this class, None is returned.
+ """
+ friction=None
+ ifself._friction_layerisnotNone:
+ friction=self.friction_data.flatten()[self.bool_mask].mean()
+
+ returnfriction
+
+ @property
+ deffriction_data(self):
+"""Get the friction data for the full SC point (no exclusions)
+
+ Returns
+ -------
+ friction_data : None | np.ndarray
+ 2D friction data layer corresponding to the exclusions grid in
+ the SC domain. If friction layer is not input to this class,
+ None is returned.
+ """
+ friction_data=None
+ ifself._friction_layerisnotNone:
+ friction_data=self._friction_layer[self.rows,self.cols]
+
+ returnfriction_data
+
+ @property
+ defpower_density(self):
+"""Get the estimated power density either from input or infered from
+ generation output meta.
+
+ Returns
+ -------
+ _power_density : float
+ Estimated power density in MW/km2
+ """
+
+ ifself._power_densityisNone:
+ tech=self.gen.meta["reV_tech"][0]
+ iftechinself.POWER_DENSITY:
+ self._power_density=self.POWER_DENSITY[tech]
+ else:
+ warn(
+ "Could not recognize reV technology in generation meta "
+ 'data: "{}". Cannot lookup an appropriate power density '
+ "to calculate SC point capacity.".format(tech)
+ )
+
+ elifisinstance(self._power_density,pd.DataFrame):
+ self._pd_obj=self._power_density
+
+ missing=set(self.res_gid_set)-set(self._pd_obj.index.values)
+ ifany(missing):
+ msg=(
+ "Variable power density input is missing the "
+ "following resource GIDs: {}".format(missing)
+ )
+ logger.error(msg)
+ raiseFileInputError(msg)
+
+ pds=self._pd_obj.loc[
+ self._res_gids[self.bool_mask],"power_density"
+ ].values
+ pds=pds.astype(np.float32)
+ pds*=self.include_mask_flat[self.bool_mask]
+ denom=self.include_mask_flat[self.bool_mask].sum()
+ self._power_density=pds.sum()/denom
+
+ returnself._power_density
+
+ @property
+ defpower_density_ac(self):
+"""Get the estimated AC power density either from input or
+ inferred from generation output meta.
+
+ This value is only available for solar runs with a "dc_ac_ratio"
+ dataset in the generation file. If these conditions are not met,
+ this value is `None`.
+
+ Returns
+ -------
+ _power_density_ac : float | None
+ Estimated AC power density in MW/km2
+ """
+ if"dc_ac_ratio"notinself.gen.datasets:
+ returnNone
+
+ ilr=self.gen["dc_ac_ratio",self._gen_gids[self.bool_mask]]
+ ilr=ilr.astype("float32")
+ weights=self.include_mask_flat[self.bool_mask]
+ ifself._power_density_acisNone:
+ tech=self.gen.meta["reV_tech"][0]
+ iftechinself.POWER_DENSITY:
+ power_density_ac=self.POWER_DENSITY[tech]/ilr
+ power_density_ac*=weights
+ power_density_ac=power_density_ac.sum()/weights.sum()
+ else:
+ warn(
+ "Could not recognize reV technology in generation meta "
+ 'data: "{}". Cannot lookup an appropriate power density '
+ "to calculate SC point capacity.".format(tech)
+ )
+ power_density_ac=None
+
+ elifisinstance(self._power_density_ac,pd.DataFrame):
+ self._pd_obj=self._power_density_ac
+
+ missing=set(self.res_gid_set)-set(self._pd_obj.index.values)
+ ifany(missing):
+ msg=(
+ "Variable power density input is missing the "
+ "following resource GIDs: {}".format(missing)
+ )
+ logger.error(msg)
+ raiseFileInputError(msg)
+
+ pds=self._pd_obj.loc[
+ self._res_gids[self.bool_mask],"power_density"
+ ].values
+ power_density_ac=pds.astype(np.float32)/ilr
+ power_density_ac*=weights
+ power_density_ac=power_density_ac.sum()/weights.sum()
+ else:
+ power_density_ac=self._power_density_ac*weights/ilr
+ power_density_ac=power_density_ac.sum()/weights.sum()
+
+ returnpower_density_ac
+
+ @property
+ defcapacity(self):
+"""Get the estimated capacity in MW of the supply curve point in the
+ current resource class with the applied exclusions.
+
+ This value represents DC capacity for solar and AC capacity for
+ all other technologies. This is the capacity that should be used
+ for all cost calculations for ALL technologies (to align with
+ SAM).
+
+ Returns
+ -------
+ capacity : float
+ Estimated capacity in MW of the supply curve point in the
+ current resource class with the applied exclusions.
+ """
+
+ capacity=None
+ ifself.power_densityisnotNone:
+ capacity=self.area*self.power_density
+
+ returncapacity
+
+ @property
+ defcapacity_ac(self):
+"""Get the AC estimated capacity in MW of the supply curve point in the
+ current resource class with the applied exclusions.
+
+ This value is provided only for solar inputs that have
+ the "dc_ac_ratio" dataset in the generation file. If these
+ conditions are not met, this value is `None`.
+
+ Returns
+ -------
+ capacity : float | None
+ Estimated AC capacity in MW of the supply curve point in the
+ current resource class with the applied exclusions. Only not
+ `None` for solar runs with "dc_ac_ratio" dataset in the
+ generation file
+ """
+ ifself.power_density_acisNone:
+ returnNone
+
+ returnself.area*self.power_density_ac
+
+ @property
+ defcapacity_dc(self):
+"""Get the DC estimated capacity in MW of the supply curve point
+ in the current resource class with the applied exclusions.
+
+ This value is provided only for solar inputs that have
+ the "dc_ac_ratio" dataset in the generation file. If these
+ conditions are not met, this value is `None`.
+
+ Returns
+ -------
+ capacity : float | None
+ Estimated AC capacity in MW of the supply curve point in the
+ current resource class with the applied exclusions. Only not
+ `None` for solar runs with "dc_ac_ratio" dataset in the
+ generation file
+ """
+ ifself.power_density_acisNone:
+ returnNone
+
+ returnself.area*self.power_density
+
+ @property
+ defsc_point_annual_energy(self):
+"""Get the total annual energy (MWh) for the entire SC point.
+
+ This value is computed using the capacity of the supply curve
+ point as well as the mean capacity factor. If the mean capacity
+ factor is `None`, this value will also be `None`.
+
+ Returns
+ -------
+ sc_point_annual_energy : float | None
+ Total annual energy (MWh) for the entire SC point.
+ """
+ ifself.mean_cfisNone:
+ returnNone
+
+ returnself.mean_cf*self.capacity*8760
+
+ @property
+ defh5_dsets_data(self):
+"""Get any additional/supplemental h5 dataset data to summarize.
+
+ Returns
+ -------
+ h5_dsets_data : dict | None
+
+ """
+
+ _h5_dsets_data=None
+
+ ifisinstance(self._h5_dsets,(list,tuple)):
+ _h5_dsets_data={}
+ fordsetinself._h5_dsets:
+ ifdsetinself.gen.datasets:
+ _h5_dsets_data[dset]=self.gen[dset]
+
+ elifisinstance(self._h5_dsets,dict):
+ _h5_dsets_data=self._h5_dsets
+
+ elifself._h5_dsetsisnotNone:
+ e=(
+ "Cannot recognize h5_dsets input type, should be None, "
+ "a list of dataset names, or a dictionary or "
+ "pre-extracted data. Received: {}{}".format(
+ type(self._h5_dsets),self._h5_dsets
+ )
+ )
+ logger.error(e)
+ raiseTypeError(e)
+
+ return_h5_dsets_data
+
+ @property
+ defregional_multiplier(self):
+"""float: Mean regional capital cost multiplier, defaults to 1."""
+ if"capital_cost_multiplier"notinself.gen.datasets:
+ return1
+
+ multipliers=self.gen["capital_cost_multiplier"]
+ returnself.exclusion_weighted_mean(multipliers)
+
+ @property
+ deffixed_charge_rate(self):
+"""float: Mean fixed_charge_rate, defaults to 0."""
+ if"fixed_charge_rate"notinself.gen.datasets:
+ return0
+
+ returnself.exclusion_weighted_mean(self.gen["fixed_charge_rate"])
+
+ @property
+ def_sam_system_capacity(self):
+"""float: Mean SAM generation system capacity input, defaults to 0. """
+ ifself._sscisnotNone:
+ returnself._ssc
+
+ self._ssc=0
+ if"system_capacity"inself.gen.datasets:
+ self._ssc=self.exclusion_weighted_mean(
+ self.gen["system_capacity"]
+ )
+
+ returnself._ssc
+
+ @property
+ def_sam_lcoe_kwargs(self):
+"""dict: Mean LCOE inputs, as passed to SAM during generation."""
+ ifself._slk:
+ returnself._slk
+
+ self._slk={"capital_cost":None,"fixed_operating_cost":None,
+ "variable_operating_cost":None,
+ "fixed_charge_rate":None,"system_capacity":None}
+
+ fordsetinself._slk:
+ ifdsetinself.gen.datasets:
+ self._slk[dset]=self.exclusion_weighted_mean(
+ self.gen[dset]
+ )
+
+ returnself._slk
+
+ def_compute_cost_per_ac_mw(self,dset):
+"""Compute a cost per AC MW for a given input. """
+ ifself._sam_system_capacity<=0:
+ returnNone
+
+ ifdsetnotinself.gen.datasets:
+ returnNone
+
+ sam_cost=self.exclusion_weighted_mean(self.gen[dset])
+ sam_cost_per_mw=sam_cost/self._sam_system_capacity
+ sc_point_cost=sam_cost_per_mw*self.capacity
+
+ ac_cap=(self.capacity
+ ifself.capacity_acisNone
+ elseself.capacity_ac)
+ returnsc_point_cost/ac_cap
+
+ @property
+ defmean_h5_dsets_data(self):
+"""Get the mean supplemental h5 datasets data (optional)
+
+ Returns
+ -------
+ mean_h5_dsets_data : dict | None
+ Mean dataset values for the non-excluded data for the optional
+ h5_dsets input.
+ """
+ _mean_h5_dsets_data=None
+ ifself.h5_dsets_dataisnotNone:
+ _mean_h5_dsets_data={}
+ fordset,arrinself.h5_dsets_data.items():
+ _mean_h5_dsets_data[dset]=self.exclusion_weighted_mean(arr)
+
+ return_mean_h5_dsets_data
+
+ def_apply_exclusions(self):
+"""Apply exclusions by masking the generation and resource gid arrays.
+ This removes all res/gen entries that are masked by the exclusions or
+ resource bin."""
+
+ # exclusions mask is False where excluded
+ exclude=self.include_mask_flat==0
+ exclude=self._resource_exclusion(exclude)
+
+ self._gen_gids[exclude]=-1
+ self._res_gids[exclude]=-1
+
+ # ensure that excluded pixels (including resource exclusions!)
+ # has an exclusions multiplier of 0
+ exclude=exclude.reshape(self.include_mask.shape)
+ self._incl_mask[exclude]=0.0
+ self._incl_mask=self._incl_mask.flatten()
+
+ if(self._gen_gids!=-1).sum()==0:
+ msg=(
+ "Supply curve point gid {} is completely excluded for res "
+ "bin: {}".format(self._gid,self._res_class_bin)
+ )
+ raiseEmptySupplyCurvePointError(msg)
+
+ def_resource_exclusion(self,boolean_exclude):
+"""Include the resource exclusion into a pre-existing bool exclusion.
+
+ Parameters
+ ----------
+ boolean_exclude : np.ndarray
+ Boolean exclusion array (True is exclude).
+
+ Returns
+ -------
+ boolean_exclude : np.ndarray
+ Same as input but includes additional exclusions for resource
+ outside of current resource class bin.
+ """
+
+ if(
+ self._res_class_dsetisnotNone
+ andself._res_class_binisnotNone
+ ):
+ rex=self.res_data[self._gen_gids]
+ rex=(rex<np.min(self._res_class_bin))|(
+ rex>=np.max(self._res_class_bin)
+ )
+
+ boolean_exclude=boolean_exclude|rex
+
+ returnboolean_exclude
+
+
[docs]defpoint_summary(self,args=None):
+"""
+ Get a summary dictionary of a single supply curve point.
+
+ Parameters
+ ----------
+ args : tuple | list | None
+ List of summary arguments to include. None defaults to all
+ available args defined in the class attr.
+
+ Returns
+ -------
+ summary : dict
+ Dictionary of summary outputs for this sc point.
+ """
+
+ ARGS={
+ SupplyCurveField.LATITUDE:self.latitude,
+ SupplyCurveField.LONGITUDE:self.longitude,
+ SupplyCurveField.COUNTRY:self.country,
+ SupplyCurveField.STATE:self.state,
+ SupplyCurveField.COUNTY:self.county,
+ SupplyCurveField.ELEVATION:self.elevation,
+ SupplyCurveField.TIMEZONE:self.timezone,
+ SupplyCurveField.SC_POINT_GID:self.sc_point_gid,
+ SupplyCurveField.SC_ROW_IND:self.sc_row_ind,
+ SupplyCurveField.SC_COL_IND:self.sc_col_ind,
+ SupplyCurveField.RES_GIDS:self.res_gid_set,
+ SupplyCurveField.GEN_GIDS:self.gen_gid_set,
+ SupplyCurveField.GID_COUNTS:self.gid_counts,
+ SupplyCurveField.N_GIDS:self.n_gids,
+ SupplyCurveField.OFFSHORE:self.offshore,
+ SupplyCurveField.MEAN_CF_AC:(
+ self.mean_cfifself.mean_cf_acisNoneelseself.mean_cf_ac
+ ),
+ SupplyCurveField.MEAN_CF_DC:self.mean_cf_dc,
+ SupplyCurveField.MEAN_LCOE:self.mean_lcoe,
+ SupplyCurveField.MEAN_RES:self.mean_res,
+ SupplyCurveField.AREA_SQ_KM:self.area,
+ SupplyCurveField.CAPACITY_AC_MW:(
+ self.capacityifself.capacity_acisNoneelseself.capacity_ac
+ ),
+ SupplyCurveField.CAPACITY_DC_MW:self.capacity_dc,
+ SupplyCurveField.EOS_MULT:1,# added later
+ SupplyCurveField.REG_MULT:self.regional_multiplier,
+ SupplyCurveField.SC_POINT_ANNUAL_ENERGY_MW:(
+ self.sc_point_annual_energy
+ ),
+ SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW:(
+ self._compute_cost_per_ac_mw("capital_cost")
+ ),
+ SupplyCurveField.COST_BASE_OCC_USD_PER_AC_MW:(
+ self._compute_cost_per_ac_mw("base_capital_cost")
+ ),
+ SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW:(
+ self._compute_cost_per_ac_mw("fixed_operating_cost")
+ ),
+ SupplyCurveField.COST_BASE_FOC_USD_PER_AC_MW:(
+ self._compute_cost_per_ac_mw("base_fixed_operating_cost")
+ ),
+ SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW:(
+ self._compute_cost_per_ac_mw("variable_operating_cost")
+ ),
+ SupplyCurveField.COST_BASE_VOC_USD_PER_AC_MW:(
+ self._compute_cost_per_ac_mw("base_variable_operating_cost")
+ ),
+ SupplyCurveField.FIXED_CHARGE_RATE:self.fixed_charge_rate,
+ }
+
+ ifself._friction_layerisnotNone:
+ ARGS[SupplyCurveField.MEAN_FRICTION]=self.mean_friction
+ ARGS[SupplyCurveField.MEAN_LCOE_FRICTION]=self.mean_lcoe_friction
+
+ ifself._h5_dsetsisnotNone:
+ fordset,datainself.mean_h5_dsets_data.items():
+ ARGS["mean_{}".format(dset)]=data
+
+ ifargsisNone:
+ args=list(ARGS.keys())
+
+ summary={}
+ forarginargs:
+ ifarginARGS:
+ summary[arg]=ARGS[arg]
+ else:
+ warn(
+ 'Cannot find "{}" as an available SC self summary '
+ "output",
+ OutputWarning,
+ )
+
+ returnsummary
+
+
[docs]@staticmethod
+ defeconomies_of_scale(cap_cost_scale,summary):
+"""Apply economies of scale to this point summary
+
+ Parameters
+ ----------
+ cap_cost_scale : str
+ LCOE scaling equation to implement "economies of scale".
+ Equation must be in python string format and return a scalar
+ value to multiply the capital cost by. Independent variables in
+ the equation should match the names of the columns in the reV
+ supply curve aggregation table.
+ summary : dict
+ Dictionary of summary outputs for this sc point.
+
+ Returns
+ -------
+ summary : dict
+ Dictionary of summary outputs for this sc point.
+ """
+
+ eos=EconomiesOfScale(cap_cost_scale,summary)
+ summary[SupplyCurveField.RAW_LCOE]=eos.raw_lcoe
+ summary[SupplyCurveField.MEAN_LCOE]=eos.scaled_lcoe
+ summary[SupplyCurveField.EOS_MULT]=eos.capital_cost_scalar
+ cost=summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW]
+ ifcostisnotNone:
+ summary[SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW]=(
+ cost*summary[SupplyCurveField.EOS_MULT]
+ )
+ returnsummary
+
+
[docs]@classmethod
+ defsummarize(
+ cls,
+ gid,
+ excl_fpath,
+ gen_fpath,
+ tm_dset,
+ gen_index,
+ excl_dict=None,
+ inclusion_mask=None,
+ res_class_dset=None,
+ res_class_bin=None,
+ excl_area=None,
+ power_density=None,
+ cf_dset="cf_mean-means",
+ lcoe_dset="lcoe_fcr-means",
+ h5_dsets=None,
+ resolution=64,
+ exclusion_shape=None,
+ close=False,
+ friction_layer=None,
+ args=None,
+ data_layers=None,
+ cap_cost_scale=None,
+ recalc_lcoe=True,
+ ):
+"""Get a summary dictionary of a single supply curve point.
+
+ Parameters
+ ----------
+ gid : int
+ gid for supply curve point to analyze.
+ excl_fpath : str
+ Filepath to exclusions h5.
+ gen_fpath : str
+ Filepath to .h5 reV generation output results.
+ tm_dset : str
+ Dataset name in the techmap file containing the
+ exclusions-to-resource mapping data.
+ gen_index : np.ndarray
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ None if excl input is pre-initialized.
+ inclusion_mask : np.ndarray
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. The shape of this will be checked against the input
+ resolution.
+ res_class_dset : str | np.ndarray | None
+ Dataset in the generation file dictating resource classes.
+ Can be pre-extracted resource data in np.ndarray.
+ None if no resource classes.
+ res_class_bin : list | None
+ Two-entry lists dictating the single resource class bin.
+ None if no resource classes.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ power_density : float | None | pd.DataFrame
+ Constant power density float, None, or opened dataframe with
+ (resource) "gid" and "power_density columns".
+ cf_dset : str | np.ndarray
+ Dataset name from gen containing capacity factor mean values.
+ Can be pre-extracted generation output data in np.ndarray.
+ lcoe_dset : str | np.ndarray
+ Dataset name from gen containing LCOE mean values.
+ Can be pre-extracted generation output data in np.ndarray.
+ h5_dsets : None | list | dict
+ Optional list of dataset names to summarize from the gen/econ h5
+ files. Can also be pre-extracted data dictionary where keys are
+ the dataset names and values are the arrays of data from the
+ h5 files.
+ resolution : int | None
+ SC resolution, must be input in combination with gid.
+ exclusion_shape : tuple
+ Shape of the exclusions extent (rows, cols). Inputing this will
+ speed things up considerably.
+ close : bool
+ Flag to close object file handlers on exit.
+ friction_layer : None | FrictionMask
+ Friction layer with scalar friction values if valid friction inputs
+ were entered. Otherwise, None to not apply friction layer.
+ args : tuple | list, optional
+ List of summary arguments to include. None defaults to all
+ available args defined in the class attr, by default None
+ data_layers : dict, optional
+ Aggregation data layers. Must be a dictionary keyed by data label
+ name. Each value must be another dictionary with "dset", "method",
+ and "fpath", by default None
+ cap_cost_scale : str | None
+ Optional LCOE scaling equation to implement "economies of scale".
+ Equations must be in python string format and return a scalar
+ value to multiply the capital cost by. Independent variables in
+ the equation should match the names of the columns in the reV
+ supply curve aggregation table.
+ recalc_lcoe : bool
+ Flag to re-calculate the LCOE from the multi-year mean capacity
+ factor and annual energy production data. This requires several
+ datasets to be aggregated in the gen input: system_capacity,
+ fixed_charge_rate, capital_cost, fixed_operating_cost,
+ and variable_operating_cost.
+
+ Returns
+ -------
+ summary : dict
+ Dictionary of summary outputs for this sc point.
+ """
+ kwargs={
+ "excl_dict":excl_dict,
+ "inclusion_mask":inclusion_mask,
+ "res_class_dset":res_class_dset,
+ "res_class_bin":res_class_bin,
+ "excl_area":excl_area,
+ "power_density":power_density,
+ "cf_dset":cf_dset,
+ "lcoe_dset":lcoe_dset,
+ "h5_dsets":h5_dsets,
+ "resolution":resolution,
+ "exclusion_shape":exclusion_shape,
+ "close":close,
+ "friction_layer":friction_layer,
+ "recalc_lcoe":recalc_lcoe,
+ }
+
+ withcls(
+ gid,excl_fpath,gen_fpath,tm_dset,gen_index,**kwargs
+ )aspoint:
+ summary=point.point_summary(args=args)
+
+ ifdata_layersisnotNone:
+ summary=point.agg_data_layers(summary,data_layers)
+
+ ifcap_cost_scaleisnotNone:
+ summary=point.economies_of_scale(cap_cost_scale,summary)
+
+ forarg,valinsummary.items():
+ ifvalisNone:
+ summary[arg]=np.nan
+
+ returnsummary
+
+
+def_infer_cf_dset_ac(cf_dset):
+"""Infer AC dataset name from input. """
+ parts=cf_dset.split("-")
+ iflen(parts)==1:
+ returnf"{cf_dset}_ac"
+
+ cf_name="-".join(parts[:-1])
+ returnf"{cf_name}_ac-{parts[-1]}"
+
[docs]classSupplyCurveAggFileHandler(AbstractAggFileHandler):
+"""
+ Framework to handle aggregation summary context managers:
+ - exclusions .h5 file
+ - generation .h5 file
+ - econ .h5 file (optional)
+ - friction surface .h5 file (optional)
+ - variable power density .csv (optional)
+ """
+
+ def__init__(
+ self,
+ excl_fpath,
+ gen_fpath,
+ econ_fpath=None,
+ data_layers=None,
+ power_density=None,
+ excl_dict=None,
+ friction_fpath=None,
+ friction_dset=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ ):
+"""
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ gen_fpath : str
+ Filepath to .h5 reV generation output results.
+ econ_fpath : str | None
+ Filepath to .h5 reV econ output results. This is optional and only
+ used if the lcoe_dset is not present in the gen_fpath file.
+ data_layers : None | dict
+ Aggregation data layers. Must be a dictionary keyed by data label
+ name. Each value must be another dictionary with "dset", "method",
+ and "fpath".
+ power_density : float | str | None
+ Power density in MW/km2 or filepath to variable power
+ density file. None will attempt to infer a constant
+ power density from the generation meta data technology.
+ Variable power density csvs must have "gid" and "power_density"
+ columns where gid is the resource gid (typically wtk or nsrdb gid)
+ and the power_density column is in MW/km2.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ friction_fpath : str | None
+ Filepath to friction surface data (cost based exclusions).
+ Must be paired with friction_dset. The friction data must be the
+ same shape as the exclusions. Friction input creates a new output
+ "mean_lcoe_friction" which is the nominal LCOE multiplied by the
+ friction data.
+ friction_dset : str | None
+ Dataset name in friction_fpath for the friction surface data.
+ Must be paired with friction_fpath. Must be same shape as
+ exclusions.
+ area_filter_kernel : str
+ Contiguous area filter method to use on final exclusions mask
+ min_area : float | None
+ Minimum required contiguous area filter in sq-km
+ """
+ super().__init__(
+ excl_fpath,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ )
+
+ self._gen=self._open_gen_econ_resource(gen_fpath,econ_fpath)
+ # pre-initialize the resource meta data
+ _=self._gen.meta
+
+ self._data_layers=data_layers
+ self._power_density=power_density
+ self._parse_power_density()
+
+ self._friction_layer=None
+ iffriction_fpathisnotNoneandfriction_dsetisnotNone:
+ self._friction_layer=FrictionMask(friction_fpath,friction_dset)
+
+ ifnotnp.all(self._friction_layer.shape==self._excl.shape):
+ e=("Friction layer shape {} must match exclusions shape {}!"
+ .format(self._friction_layer.shape,self._excl.shape))
+ logger.error(e)
+ raiseFileInputError(e)
+
+ @staticmethod
+ def_open_gen_econ_resource(gen_fpath,econ_fpath):
+"""Open a rex resource file handler for the reV generation and
+ (optionally) the reV econ output(s).
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to .h5 reV generation output results.
+ econ_fpath : str | None
+ Filepath to .h5 reV econ output results. This is optional and only
+ used if the lcoe_dset is not present in the gen_fpath file.
+
+ Returns
+ -------
+ handler : Resource | MultiFileResource
+ Open resource handler initialized with gen_fpath and (optionally)
+ econ_fpath.
+ """
+
+ handler=None
+ is_gen_h5=isinstance(gen_fpath,str)andgen_fpath.endswith(".h5")
+ is_econ_h5=isinstance(econ_fpath,str)andecon_fpath.endswith(".h5")
+
+ ifis_gen_h5andnotis_econ_h5:
+ handler=Resource(gen_fpath)
+ elifis_gen_h5andis_econ_h5:
+ handler=MultiFileResource(
+ [gen_fpath,econ_fpath],check_files=True
+ )
+
+ returnhandler
+
+ def_parse_power_density(self):
+"""Parse the power density input. If file, open file handler."""
+
+ ifisinstance(self._power_density,str):
+ self._pdf=self._power_density
+
+ ifself._pdf.endswith(".csv"):
+ self._power_density=pd.read_csv(self._pdf)
+ if(ResourceMetaField.GIDinself._power_density
+ and'power_density'inself._power_density):
+ self._power_density= \
+ self._power_density.set_index(ResourceMetaField.GID)
+ else:
+ msg=('Variable power density file must include "{}" '
+ 'and "power_density" columns, but received: {}'
+ .format(ResourceMetaField.GID,
+ self._power_density.columns.values))
+ logger.error(msg)
+ raiseFileInputError(msg)
+ else:
+ msg=(
+ "Variable power density file must be csv but received: "
+ "{}".format(self._pdf)
+ )
+ logger.error(msg)
+ raiseFileInputError(msg)
+
+
+
+ @property
+ defgen(self):
+"""Get the gen file handler object.
+
+ Returns
+ -------
+ _gen : Outputs
+ reV gen outputs handler object.
+ """
+ returnself._gen
+
+ @property
+ defdata_layers(self):
+"""Get the data layers object.
+
+ Returns
+ -------
+ _data_layers : dict
+ Data layers namespace.
+ """
+ returnself._data_layers
+
+ @property
+ defpower_density(self):
+"""Get the power density object.
+
+ Returns
+ -------
+ _power_density : float | None | pd.DataFrame
+ Constant power density float, None, or opened dataframe with
+ (resource) "gid" and "power_density columns".
+ """
+ returnself._power_density
+
+ @property
+ deffriction_layer(self):
+"""Get the friction layer (cost based exclusions).
+
+ Returns
+ -------
+ friction_layer : None | FrictionMask
+ Friction layer with scalar friction values if valid friction inputs
+ were entered. Otherwise, None to not apply friction layer.
+ """
+ returnself._friction_layer
+
+
+
[docs]classSupplyCurveAggregation(BaseAggregation):
+"""SupplyCurveAggregation"""
+
+ def__init__(self,excl_fpath,tm_dset,econ_fpath=None,
+ excl_dict=None,area_filter_kernel='queen',min_area=None,
+ resolution=64,excl_area=None,res_fpath=None,gids=None,
+ pre_extract_inclusions=False,res_class_dset=None,
+ res_class_bins=None,cf_dset='cf_mean-means',
+ lcoe_dset='lcoe_fcr-means',h5_dsets=None,data_layers=None,
+ power_density=None,friction_fpath=None,friction_dset=None,
+ cap_cost_scale=None,recalc_lcoe=True):
+r"""ReV supply curve points aggregation framework.
+
+ ``reV`` supply curve aggregation combines a high-resolution
+ (e.g. 90m) exclusion dataset with a (typically) lower resolution
+ (e.g. 2km) generation dataset by mapping all data onto the high-
+ resolution grid and aggregating it by a large factor (e.g. 64 or
+ 128). The result is coarsely-gridded data that summarizes
+ capacity and generation potential as well as associated
+ economics under a particular land access scenario. This module
+ can also summarize extra data layers during the aggregation
+ process, allowing for complementary land characterization
+ analysis.
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions data HDF5 file. The exclusions HDF5
+ file should contain the layers specified in `excl_dict`
+ and `data_layers`. These layers may also be spread out
+ across multiple HDF5 files, in which case this input should
+ be a list or tuple of filepaths pointing to the files
+ containing the layers. Note that each data layer must be
+ uniquely defined (i.e.only appear once and in a single
+ input file).
+ tm_dset : str
+ Dataset name in the `excl_fpath` file containing the
+ techmap (exclusions-to-resource mapping data). This data
+ layer links the supply curve GID's to the generation GID's
+ that are used to evaluate performance metrics such as
+ ``mean_cf``.
+
+ .. Important:: This dataset uniquely couples the (typically
+ high-resolution) exclusion layers to the (typically
+ lower-resolution) resource data. Therefore, a separate
+ techmap must be used for every unique combination of
+ resource and exclusion coordinates.
+
+ .. Note:: If executing ``reV`` from the command line, you
+ can specify a name that is not in the exclusions HDF5
+ file, and ``reV`` will calculate the techmap for you. Note
+ however that computing the techmap and writing it to the
+ exclusion HDF5 file is a blocking operation, so you may
+ only run a single ``reV`` aggregation step at a time this
+ way.
+
+ econ_fpath : str, optional
+ Filepath to HDF5 file with ``reV`` econ output results
+ containing an `lcoe_dset` dataset. If ``None``, `lcoe_dset`
+ should be a dataset in the `gen_fpath` HDF5 file that
+ aggregation is executed on.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ input can be set to ``"PIPELINE"`` to parse the output
+ from one of these preceding pipeline steps:
+ ``multi-year``, ``collect``, or ``generation``. However,
+ note that duplicate executions of any of these commands
+ within the pipeline may invalidate this parsing, meaning
+ the `econ_fpath` input will have to be specified manually.
+
+ By default, ``None``.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arguments of the format
+ ``{layer_dset_name: {kwarg: value}}``, where
+ ``layer_dset_name`` is a dataset in the exclusion h5 file
+ and the ``kwarg: value`` pair is a keyword argument to
+ the :class:`reV.supply_curve.exclusions.LayerMask` class.
+ For example::
+
+ excl_dict = {
+ "typical_exclusion": {
+ "exclude_values": 255,
+ },
+ "another_exclusion": {
+ "exclude_values": [2, 3],
+ "weight": 0.5
+ },
+ "exclusion_with_nodata": {
+ "exclude_range": [10, 100],
+ "exclude_nodata": True,
+ "nodata_value": -1
+ },
+ "partial_setback": {
+ "use_as_weights": True
+ },
+ "height_limit": {
+ "exclude_range": [0, 200]
+ },
+ "slope": {
+ "include_range": [0, 20]
+ },
+ "developable_land": {
+ "force_include_values": 42
+ },
+ "more_developable_land": {
+ "force_include_range": [5, 10]
+ },
+ "viewsheds": {
+ "exclude_values": 1,
+ "extent": {
+ "layer": "federal_parks",
+ "include_range": [1, 5]
+ }
+ }
+ ...
+ }
+
+ Note that all the keys given in this dictionary should be
+ datasets of the `excl_fpath` file. If ``None`` or empty
+ dictionary, no exclusions are applied. By default, ``None``.
+ area_filter_kernel : {"queen", "rook"}, optional
+ Contiguous area filter method to use on final exclusions
+ mask. The filters are defined as::
+
+ # Queen: # Rook:
+ [[1,1,1], [[0,1,0],
+ [1,1,1], [1,1,1],
+ [1,1,1]] [0,1,0]]
+
+ These filters define how neighboring pixels are "connected".
+ Once pixels in the final exclusion layer are connected, the
+ area of each resulting cluster is computed and compared
+ against the `min_area` input. Any cluster with an area
+ less than `min_area` is excluded from the final mask.
+ This argument has no effect if `min_area` is ``None``.
+ By default, ``"queen"``.
+ min_area : float, optional
+ Minimum area (in km\ :sup:`2`) required to keep an isolated
+ cluster of (included) land within the resulting exclusions
+ mask. Any clusters of land with areas less than this value
+ will be marked as exclusions. See the documentation for
+ `area_filter_kernel` for an explanation of how the area of
+ each land cluster is computed. If ``None``, no area
+ filtering is performed. By default, ``None``.
+ resolution : int, optional
+ Supply Curve resolution. This value defines how many pixels
+ are in a single side of a supply curve cell. For example,
+ a value of ``64`` would generate a supply curve where the
+ side of each supply curve cell is ``64x64`` exclusion
+ pixels. By default, ``64``.
+ excl_area : float, optional
+ Area of a single exclusion mask pixel (in km\ :sup:`2`).
+ If ``None``, this value will be inferred from the profile
+ transform attribute in `excl_fpath`. By default, ``None``.
+ res_fpath : str, optional
+ Filepath to HDF5 resource file (e.g. WTK or NSRDB). This
+ input is required if techmap dset is to be created or if the
+ ``gen_fpath`` input to the ``summarize`` or ``run`` methods
+ is ``None``. By default, ``None``.
+ gids : list, optional
+ List of supply curve point gids to get summary for. If you
+ would like to obtain all available ``reV`` supply curve
+ points to run, you can use the
+ :class:`reV.supply_curve.extent.SupplyCurveExtent` class
+ like so::
+
+ import pandas as pd
+ from reV.supply_curve.extent import SupplyCurveExtent
+
+ excl_fpath = "..."
+ resolution = ...
+ tm_dset = "..."
+ with SupplyCurveExtent(excl_fpath, resolution) as sc:
+ gids = sc.valid_sc_points(tm_dset).tolist()
+ ...
+
+ If ``None``, supply curve aggregation is computed for all
+ gids in the supply curve extent. By default, ``None``.
+ pre_extract_inclusions : bool, optional
+ Optional flag to pre-extract/compute the inclusion mask from
+ the `excl_dict` input. It is typically faster to compute
+ the inclusion mask on the fly with parallel workers.
+ By default, ``False``.
+ res_class_dset : str, optional
+ Name of dataset in the ``reV`` generation HDF5 output file
+ containing resource data. If ``None``, no aggregated
+ resource classification is performed (i.e. no ``mean_res``
+ output), and the `res_class_bins` is ignored.
+ By default, ``None``.
+ res_class_bins : list, optional
+ Optional input to perform separate aggregations for various
+ resource data ranges. If ``None``, only a single aggregation
+ per supply curve point is performed. Otherwise, this input
+ should be a list of floats or ints representing the resource
+ bin boundaries. One aggregation per resource value range is
+ computed, and only pixels within the given resource range
+ are aggregated. By default, ``None``.
+ cf_dset : str, optional
+ Dataset name from the ``reV`` generation HDF5 output file
+ containing a 1D dataset of mean capacity factor values. This
+ dataset will be mapped onto the high-resolution grid and
+ used to compute the mean capacity factor for non-excluded
+ area. By default, ``"cf_mean-means"``.
+ lcoe_dset : str, optional
+ Dataset name from the ``reV`` generation HDF5 output file
+ containing a 1D dataset of mean LCOE values. This
+ dataset will be mapped onto the high-resolution grid and
+ used to compute the mean LCOE for non-excluded area, but
+ only if the LCOE is not re-computed during processing (see
+ the `recalc_lcoe` input for more info).
+ By default, ``"lcoe_fcr-means"``.
+ h5_dsets : list, optional
+ Optional list of additional datasets from the ``reV``
+ generation/econ HDF5 output file to aggregate. If ``None``,
+ no extra datasets are aggregated.
+
+ .. WARNING:: This input is meant for passing through 1D
+ datasets. If you specify a 2D or higher-dimensional
+ dataset, you may run into memory errors. If you wish to
+ aggregate 2D datasets, see the rep-profiles module.
+
+ By default, ``None``.
+ data_layers : dict, optional
+ Dictionary of aggregation data layers of the format::
+
+ data_layers = {
+ "output_layer_name": {
+ "dset": "layer_name",
+ "method": "mean",
+ "fpath": "/path/to/data.h5"
+ },
+ "another_output_layer_name": {
+ "dset": "input_layer_name",
+ "method": "mode",
+ # optional "fpath" key omitted
+ },
+ ...
+ }
+
+ The ``"output_layer_name"`` is the column name under which
+ the aggregated data will appear in the output CSV file. The
+ ``"output_layer_name"`` does not have to match the ``dset``
+ input value. The latter should match the layer name in the
+ HDF5 from which the data to aggregate should be pulled. The
+ ``method`` should be one of
+ ``{"mode", "mean", "min", "max", "sum", "category"}``,
+ describing how the high-resolution data should be aggregated
+ for each supply curve point. ``fpath`` is an optional key
+ that can point to an HDF5 file containing the layer data. If
+ left out, the data is assumed to exist in the file(s)
+ specified by the `excl_fpath` input. If ``None``, no data
+ layer aggregation is performed. By default, ``None``
+ power_density : float | str, optional
+ Power density value (in MW/km\ :sup:`2`) or filepath to
+ variable power density CSV file containing the following
+ columns:
+
+ - ``gid`` : resource gid (typically wtk or nsrdb gid)
+ - ``power_density`` : power density value (in
+ MW/km\ :sup:`2`)
+
+ If ``None``, a constant power density is inferred from the
+ generation meta data technology. By default, ``None``.
+ friction_fpath : str, optional
+ Filepath to friction surface data (cost based exclusions).
+ Must be paired with the `friction_dset` input below. The
+ friction data must be the same shape as the exclusions.
+ Friction input creates a new output column
+ ``"mean_lcoe_friction"`` which is the nominal LCOE
+ multiplied by the friction data. If ``None``, no friction
+ data is aggregated. By default, ``None``.
+ friction_dset : str, optional
+ Dataset name in friction_fpath for the friction surface
+ data. Must be paired with the `friction_fpath` above. If
+ ``None``, no friction data is aggregated.
+ By default, ``None``.
+ cap_cost_scale : str, optional
+ Optional LCOE scaling equation to implement "economies of
+ scale". Equations must be in python string format and must
+ return a scalar value to multiply the capital cost by.
+ Independent variables in the equation should match the names
+ of the columns in the ``reV`` supply curve aggregation
+ output table (see the documentation of
+ :class:`~reV.supply_curve.sc_aggregation.SupplyCurveAggregation`
+ for details on available outputs). If ``None``, no economies
+ of scale are applied. By default, ``None``.
+ recalc_lcoe : bool, optional
+ Flag to re-calculate the LCOE from the multi-year mean
+ capacity factor and annual energy production data. This
+ requires several datasets to be aggregated in the h5_dsets
+ input:
+
+ - ``system_capacity``
+ - ``fixed_charge_rate``
+ - ``capital_cost``
+ - ``fixed_operating_cost``
+ - ``variable_operating_cost``
+
+ If any of these datasets are missing from the ``reV``
+ generation HDF5 output, or if `recalc_lcoe` is set to
+ ``False``, the mean LCOE will be computed from the data
+ stored under the `lcoe_dset` instead. By default, ``True``.
+
+ Examples
+ --------
+ Standard outputs:
+
+ sc_gid : int
+ Unique supply curve gid. This is the enumerated supply curve
+ points, which can have overlapping geographic locations due
+ to different resource bins at the same geographic SC point.
+ res_gids : list
+ Stringified list of resource gids (e.g. original WTK or
+ NSRDB resource GIDs) corresponding to each SC point.
+ gen_gids : list
+ Stringified list of generation gids (e.g. GID in the reV
+ generation output, which corresponds to the reV project
+ points and not necessarily the resource GIDs).
+ gid_counts : list
+ Stringified list of the sum of inclusion scalar values
+ corresponding to each `gen_gid` and `res_gid`, where 1 is
+ included, 0 is excluded, and 0.7 is included with 70 percent
+ of available land. Each entry in this list is associated
+ with the corresponding entry in the `gen_gids` and
+ `res_gids` lists.
+ n_gids : int
+ Total number of included pixels. This is a boolean sum and
+ considers partial inclusions to be included (e.g. 1).
+ mean_cf : float
+ Mean capacity factor of each supply curve point (the
+ arithmetic mean is weighted by the inclusion layer)
+ (unitless).
+ mean_lcoe : float
+ Mean LCOE of each supply curve point (the arithmetic mean is
+ weighted by the inclusion layer). Units match the reV econ
+ output ($/MWh). By default, the LCOE is re-calculated using
+ the multi-year mean capacity factor and annual energy
+ production. This requires several datasets to be aggregated
+ in the h5_dsets input: ``fixed_charge_rate``,
+ ``capital_cost``,
+ ``fixed_operating_cost``, ``annual_energy_production``, and
+ ``variable_operating_cost``. This recalc behavior can be
+ disabled by setting ``recalc_lcoe=False``.
+ mean_res : float
+ Mean resource, the resource dataset to average is provided
+ by the user in `res_class_dset`. The arithmetic mean is
+ weighted by the inclusion layer.
+ capacity : float
+ Total capacity of each supply curve point (MW). Units are
+ contingent on the `power_density` input units of MW/km2.
+ area_sq_km : float
+ Total included area for each supply curve point in km2. This
+ is based on the nominal area of each exclusion pixel which
+ by default is calculated from the exclusion profile
+ attributes. The NREL reV default is 0.0081 km2 pixels
+ (90m x 90m). The area sum considers partial inclusions.
+ latitude : float
+ Supply curve point centroid latitude coordinate, in degrees
+ (does not consider exclusions).
+ longitude : float
+ Supply curve point centroid longitude coordinate, in degrees
+ (does not consider exclusions).
+ country : str
+ Country of the supply curve point based on the most common
+ country of the associated resource meta data. Does not
+ consider exclusions.
+ state : str
+ State of the supply curve point based on the most common
+ state of the associated resource meta data. Does not
+ consider exclusions.
+ county : str
+ County of the supply curve point based on the most common
+ county of the associated resource meta data. Does not
+ consider exclusions.
+ elevation : float
+ Mean elevation of the supply curve point based on the mean
+ elevation of the associated resource meta data. Does not
+ consider exclusions.
+ timezone : int
+ UTC offset of local timezone based on the most common
+ timezone of the associated resource meta data. Does not
+ consider exclusions.
+ sc_point_gid : int
+ Spatially deterministic supply curve point gid. Duplicate
+ `sc_point_gid` values can exist due to resource binning.
+ sc_row_ind : int
+ Row index of the supply curve point in the aggregated
+ exclusion grid.
+ sc_col_ind : int
+ Column index of the supply curve point in the aggregated
+ exclusion grid
+ res_class : int
+ Resource class for the supply curve gid. Each geographic
+ supply curve point (`sc_point_gid`) can have multiple
+ resource classes associated with it, resulting in multiple
+ supply curve gids (`sc_gid`) associated with the same
+ spatially deterministic supply curve point.
+
+
+ Optional outputs:
+
+ mean_friction : float
+ Mean of the friction data provided in 'friction_fpath' and
+ 'friction_dset'. The arithmetic mean is weighted by boolean
+ inclusions and considers partial inclusions to be included.
+ mean_lcoe_friction : float
+ Mean of the nominal LCOE multiplied by mean_friction value.
+ mean_{dset} : float
+ Mean input h5 dataset(s) provided by the user in 'h5_dsets'.
+ These mean calculations are weighted by the partial
+ inclusion layer.
+ data_layers : float | int | str | dict
+ Requested data layer aggregations, each data layer must be
+ the same shape as the exclusion layers.
+
+ - mode: int | str
+ Most common value of a given data layer after
+ applying the boolean inclusion mask.
+ - mean : float
+ Arithmetic mean value of a given data layer weighted
+ by the scalar inclusion mask (considers partial
+ inclusions).
+ - min : float | int
+ Minimum value of a given data layer after applying
+ the boolean inclusion mask.
+ - max : float | int
+ Maximum value of a given data layer after applying
+ the boolean inclusion mask.
+ - sum : float
+ Sum of a given data layer weighted by the scalar
+ inclusion mask (considers partial inclusions).
+ - category : dict
+ Dictionary mapping the unique values in the
+ `data_layer` to the sum of inclusion scalar values
+ associated with all pixels with that unique value.
+ """
+ log_versions(logger)
+ logger.info("Initializing SupplyCurveAggregation...")
+ logger.debug("Exclusion filepath: {}".format(excl_fpath))
+ logger.debug("Exclusion dict: {}".format(excl_dict))
+
+ super().__init__(
+ excl_fpath,
+ tm_dset,
+ excl_dict=excl_dict,
+ area_filter_kernel=area_filter_kernel,
+ min_area=min_area,
+ resolution=resolution,
+ excl_area=excl_area,
+ res_fpath=res_fpath,
+ gids=gids,
+ pre_extract_inclusions=pre_extract_inclusions,
+ )
+
+ self._econ_fpath=econ_fpath
+ self._res_class_dset=res_class_dset
+ self._res_class_bins=self._convert_bins(res_class_bins)
+ self._cf_dset=cf_dset
+ self._lcoe_dset=lcoe_dset
+ self._h5_dsets=h5_dsets
+ self._cap_cost_scale=cap_cost_scale
+ self._power_density=power_density
+ self._friction_fpath=friction_fpath
+ self._friction_dset=friction_dset
+ self._data_layers=data_layers
+ self._recalc_lcoe=recalc_lcoe
+
+ logger.debug("Resource class bins: {}".format(self._res_class_bins))
+
+ ifself._power_densityisNone:
+ msg=(
+ "Supply curve aggregation power density not specified. "
+ "Will try to infer based on lookup table: {}".format(
+ GenerationSupplyCurvePoint.POWER_DENSITY
+ )
+ )
+ logger.warning(msg)
+ warn(msg,InputWarning)
+
+ self._check_data_layers()
+
+ def_check_data_layers(
+ self,methods=("mean","max","min","mode","sum","category")
+ ):
+"""Run pre-flight checks on requested aggregation data layers.
+
+ Parameters
+ ----------
+ methods : list | tuple
+ Data layer aggregation methods that are available to the user.
+ """
+
+ ifself._data_layersisnotNone:
+ logger.debug("Checking data layers...")
+
+ withExclusionLayers(self._excl_fpath)asf:
+ shape_base=f.shape
+
+ fork,vinself._data_layers.items():
+ if"dset"notinv:
+ raiseKeyError(
+ 'Data aggregation "dset" data layer "{}" '
+ "must be specified.".format(k)
+ )
+ if"method"notinv:
+ raiseKeyError(
+ 'Data aggregation "method" data layer "{}" '
+ "must be specified.".format(k)
+ )
+ ifv["method"].lower()notinmethods:
+ raiseValueError(
+ "Cannot recognize data layer agg method: "
+ '"{}". Can only do: {}.'.format(v["method"],methods)
+ )
+ if"fpath"inv:
+ withExclusionLayers(v["fpath"])asf:
+ try:
+ mismatched_shapes=any(f.shape!=shape_base)
+ exceptTypeError:
+ mismatched_shapes=f.shape!=shape_base
+ ifmismatched_shapes:
+ msg=(
+ 'Data shape of data layer "{}" is {}, '
+ "which does not match the baseline "
+ "exclusions shape {}.".format(
+ k,f.shape,shape_base
+ )
+ )
+ raiseFileInputError(msg)
+
+ logger.debug("Finished checking data layers.")
+
+ @staticmethod
+ def_get_res_gen_lcoe_data(
+ gen,res_class_dset,res_class_bins,lcoe_dset
+ ):
+"""Extract the basic resource / generation / lcoe data to be used in
+ the aggregation process.
+
+ Parameters
+ ----------
+ gen : Resource | MultiFileResource
+ Open rex resource handler initialized from gen_fpath and
+ (optionally) econ_fpath.
+ res_class_dset : str | None
+ Dataset in the generation file dictating resource classes.
+ None if no resource classes.
+ res_class_bins : list | None
+ List of two-entry lists dictating the resource class bins.
+ None if no resource classes.
+ lcoe_dset : str
+ Dataset name from f_gen containing LCOE mean values.
+
+ Returns
+ -------
+ res_data : np.ndarray | None
+ Extracted resource data from res_class_dset
+ res_class_bins : list
+ List of resouce class bin ranges.
+ lcoe_data : np.ndarray | None
+ LCOE data extracted from lcoe_dset in gen
+ """
+
+ dset_list=(res_class_dset,lcoe_dset)
+ gen_dsets=[]ifgenisNoneelsegen.datasets
+ labels=("res_class_dset","lcoe_dset")
+ temp=[None,None]
+
+ ifisinstance(gen,Resource):
+ source_fps=[gen.h5_file]
+ elifisinstance(gen,MultiFileResource):
+ source_fps=gen._h5_files
+ else:
+ msg='Did not recognize gen object input of type "{}": {}'.format(
+ type(gen),gen
+ )
+ logger.error(msg)
+ raiseTypeError(msg)
+
+ fori,dsetinenumerate(dset_list):
+ ifdsetingen_dsets:
+ _warn_about_large_datasets(gen,dset)
+ temp[i]=gen[dset]
+ elifdsetnotingen_dsetsanddsetisnotNone:
+ w=(
+ 'Could not find "{}" input as "{}" in source files: {}. '
+ "Available datasets: {}".format(
+ labels[i],dset,source_fps,gen_dsets
+ )
+ )
+ logger.warning(w)
+ warn(w,OutputWarning)
+
+ res_data,lcoe_data=temp
+
+ ifres_class_dsetisNoneorres_class_binsisNone:
+ res_class_bins=[None]
+
+ returnres_data,res_class_bins,lcoe_data
+
+ @staticmethod
+ def_get_extra_dsets(gen,h5_dsets):
+"""Extract extra ancillary datasets to be used in the aggregation
+ process
+
+ Parameters
+ ----------
+ gen : Resource | MultiFileResource
+ Open rex resource handler initialized from gen_fpath and
+ (optionally) econ_fpath.
+ h5_dsets : list | None
+ Optional list of additional datasets from the source h5 gen/econ
+ files to aggregate.
+
+ Returns
+ -------
+ h5_dsets_data : dict | None
+ If additional h5_dsets are requested, this will be a dictionary
+ keyed by the h5 dataset names. The corresponding values will be
+ the extracted arrays from the h5 files.
+ """
+
+ # look for the datasets required by the LCOE re-calculation and make
+ # lists of the missing datasets
+ gen_dsets=[]ifgenisNoneelsegen.datasets
+ lcoe_recalc_req=('fixed_charge_rate',
+ 'capital_cost',
+ 'fixed_operating_cost',
+ 'variable_operating_cost',
+ 'system_capacity')
+ missing_lcoe_source=[kforkinlcoe_recalc_req
+ ifknotingen_dsets]
+
+ ifisinstance(gen,Resource):
+ source_fps=[gen.h5_file]
+ elifisinstance(gen,MultiFileResource):
+ source_fps=gen._h5_files
+ else:
+ msg='Did not recognize gen object input of type "{}": {}'.format(
+ type(gen),gen
+ )
+ logger.error(msg)
+ raiseTypeError(msg)
+
+ h5_dsets_data=None
+ ifh5_dsetsisnotNone:
+
+ ifnotisinstance(h5_dsets,(list,tuple)):
+ e=(
+ "Additional h5_dsets argument must be a list or tuple "
+ "but received: {}{}".format(type(h5_dsets),h5_dsets)
+ )
+ logger.error(e)
+ raiseTypeError(e)
+
+ missing_h5_dsets=[kforkinh5_dsetsifknotingen_dsets]
+ ifany(missing_h5_dsets):
+ msg=(
+ 'Could not find requested h5_dsets "{}" in '
+ "source files: {}. Available datasets: {}".format(
+ missing_h5_dsets,source_fps,gen_dsets
+ )
+ )
+ logger.error(msg)
+ raiseFileInputError(msg)
+
+ h5_dsets_data={dset:gen[dset]fordsetinh5_dsets}
+
+ ifany(missing_lcoe_source):
+ msg=(
+ "Could not find the datasets in the gen source file that "
+ "are required to re-calculate the multi-year LCOE. If you "
+ "are running a multi-year job, it is strongly suggested "
+ "you pass through these datasets to re-calculate the LCOE "
+ "from the multi-year mean CF: {}".format(missing_lcoe_source)
+ )
+ logger.warning(msg)
+ warn(msg,InputWarning)
+
+ returnh5_dsets_data
+
+
[docs]@classmethod
+ defrun_serial(
+ cls,
+ excl_fpath,
+ gen_fpath,
+ tm_dset,
+ gen_index,
+ econ_fpath=None,
+ excl_dict=None,
+ inclusion_mask=None,
+ area_filter_kernel="queen",
+ min_area=None,
+ resolution=64,
+ gids=None,
+ args=None,
+ res_class_dset=None,
+ res_class_bins=None,
+ cf_dset="cf_mean-means",
+ lcoe_dset="lcoe_fcr-means",
+ h5_dsets=None,
+ data_layers=None,
+ power_density=None,
+ friction_fpath=None,
+ friction_dset=None,
+ excl_area=None,
+ cap_cost_scale=None,
+ recalc_lcoe=True,
+ ):
+"""Standalone method to create agg summary - can be parallelized.
+
+ Parameters
+ ----------
+ excl_fpath : str | list | tuple
+ Filepath to exclusions h5 with techmap dataset
+ (can be one or more filepaths).
+ gen_fpath : str
+ Filepath to .h5 reV generation output results.
+ tm_dset : str
+ Dataset name in the exclusions file containing the
+ exclusions-to-resource mapping data.
+ gen_index : np.ndarray
+ Array of generation gids with array index equal to resource gid.
+ Array value is -1 if the resource index was not used in the
+ generation run.
+ econ_fpath : str | None
+ Filepath to .h5 reV econ output results. This is optional and only
+ used if the lcoe_dset is not present in the gen_fpath file.
+ excl_dict : dict | None
+ Dictionary of exclusion keyword arugments of the format
+ {layer_dset_name: {kwarg: value}} where layer_dset_name is a
+ dataset in the exclusion h5 file and kwarg is a keyword argument to
+ the reV.supply_curve.exclusions.LayerMask class.
+ inclusion_mask : np.ndarray | dict | optional
+ 2D array pre-extracted inclusion mask where 1 is included and 0 is
+ excluded. This must be either match the full exclusion shape or
+ be a dict lookup of single-sc-point exclusion masks corresponding
+ to the gids input and keyed by gids, by default None which will
+ calculate exclusions on the fly for each sc point.
+ area_filter_kernel : str
+ Contiguous area filter method to use on final exclusions mask
+ min_area : float | None
+ Minimum required contiguous area filter in sq-km
+ resolution : int | None
+ SC resolution, must be input in combination with gid. Prefered
+ option is to use the row/col slices to define the SC point instead.
+ gids : list | None
+ List of supply curve point gids to get summary for (can use to
+ subset if running in parallel), or None for all gids in the SC
+ extent, by default None
+ args : list | None
+ List of positional args for sc_point_method
+ res_class_dset : str | None
+ Dataset in the generation file dictating resource classes.
+ None if no resource classes.
+ res_class_bins : list | None
+ List of two-entry lists dictating the resource class bins.
+ None if no resource classes.
+ cf_dset : str
+ Dataset name from f_gen containing capacity factor mean values.
+ lcoe_dset : str
+ Dataset name from f_gen containing LCOE mean values.
+ h5_dsets : list | None
+ Optional list of additional datasets from the source h5 gen/econ
+ files to aggregate.
+ data_layers : None | dict
+ Aggregation data layers. Must be a dictionary keyed by data label
+ name. Each value must be another dictionary with "dset", "method",
+ and "fpath".
+ power_density : float | str | None
+ Power density in MW/km2 or filepath to variable power
+ density file. None will attempt to infer a constant
+ power density from the generation meta data technology.
+ Variable power density csvs must have "gid" and "power_density"
+ columns where gid is the resource gid (typically wtk or nsrdb gid)
+ and the power_density column is in MW/km2.
+ friction_fpath : str | None
+ Filepath to friction surface data (cost based exclusions).
+ Must be paired with friction_dset. The friction data must be the
+ same shape as the exclusions. Friction input creates a new output
+ "mean_lcoe_friction" which is the nominal LCOE multiplied by the
+ friction data.
+ friction_dset : str | None
+ Dataset name in friction_fpath for the friction surface data.
+ Must be paired with friction_fpath. Must be same shape as
+ exclusions.
+ excl_area : float | None, optional
+ Area of an exclusion pixel in km2. None will try to infer the area
+ from the profile transform attribute in excl_fpath, by default None
+ cap_cost_scale : str | None
+ Optional LCOE scaling equation to implement "economies of scale".
+ Equations must be in python string format and return a scalar
+ value to multiply the capital cost by. Independent variables in
+ the equation should match the names of the columns in the reV
+ supply curve aggregation table.
+ recalc_lcoe : bool
+ Flag to re-calculate the LCOE from the multi-year mean capacity
+ factor and annual energy production data. This requires several
+ datasets to be aggregated in the h5_dsets input: system_capacity,
+ fixed_charge_rate, capital_cost, fixed_operating_cost,
+ and variable_operating_cost.
+
+ Returns
+ -------
+ summary : list
+ List of dictionaries, each being an SC point summary.
+ """
+ summary=[]
+
+ withSupplyCurveExtent(excl_fpath,resolution=resolution)assc:
+ exclusion_shape=sc.exclusions.shape
+ ifgidsisNone:
+ gids=sc.valid_sc_points(tm_dset)
+ elifnp.issubdtype(type(gids),np.number):
+ gids=[gids]
+
+ slice_lookup=sc.get_slice_lookup(gids)
+
+ logger.debug(
+ "Starting SupplyCurveAggregation serial with "
+ "supply curve {} gids".format(len(gids))
+ )
+
+ cls._check_inclusion_mask(inclusion_mask,gids,exclusion_shape)
+
+ # pre-extract handlers so they are not repeatedly initialized
+ file_kwargs={
+ "econ_fpath":econ_fpath,
+ "data_layers":data_layers,
+ "power_density":power_density,
+ "excl_dict":excl_dict,
+ "area_filter_kernel":area_filter_kernel,
+ "min_area":min_area,
+ "friction_fpath":friction_fpath,
+ "friction_dset":friction_dset,
+ }
+ withSupplyCurveAggFileHandler(
+ excl_fpath,gen_fpath,**file_kwargs
+ )asfh:
+ temp=cls._get_res_gen_lcoe_data(
+ fh.gen,res_class_dset,res_class_bins,lcoe_dset
+ )
+ res_data,res_class_bins,lcoe_data=temp
+ h5_dsets_data=cls._get_extra_dsets(fh.gen,h5_dsets)
+
+ n_finished=0
+ forgidingids:
+ gid_inclusions=cls._get_gid_inclusion_mask(
+ inclusion_mask,gid,slice_lookup,resolution=resolution
+ )
+
+ forri,res_bininenumerate(res_class_bins):
+ try:
+ pointsum=GenerationSupplyCurvePoint.summarize(
+ gid,
+ fh.exclusions,
+ fh.gen,
+ tm_dset,
+ gen_index,
+ res_class_dset=res_data,
+ res_class_bin=res_bin,
+ cf_dset=cf_dset,
+ lcoe_dset=lcoe_data,
+ h5_dsets=h5_dsets_data,
+ data_layers=fh.data_layers,
+ resolution=resolution,
+ exclusion_shape=exclusion_shape,
+ power_density=fh.power_density,
+ args=args,
+ excl_dict=excl_dict,
+ inclusion_mask=gid_inclusions,
+ excl_area=excl_area,
+ close=False,
+ friction_layer=fh.friction_layer,
+ cap_cost_scale=cap_cost_scale,
+ recalc_lcoe=recalc_lcoe,
+ )
+
+ exceptEmptySupplyCurvePointError:
+ logger.debug("SC point {} is empty".format(gid))
+ else:
+ pointsum['res_class']=ri
+
+ summary.append(pointsum)
+ logger.debug(
+ "Serial aggregation completed gid {}: "
+ "{} out of {} points complete".format(
+ gid,n_finished,len(gids)
+ )
+ )
+
+ n_finished+=1
+
+ returnsummary
+
+
[docs]defrun_parallel(
+ self,gen_fpath,args=None,max_workers=None,sites_per_worker=100
+ ):
+"""Get the supply curve points aggregation summary using futures.
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to .h5 reV generation output results.
+ args : tuple | list | None
+ List of summary arguments to include. None defaults to all
+ available args defined in the class attr.
+ max_workers : int | None, optional
+ Number of cores to run summary on. None is all
+ available cpus, by default None
+ sites_per_worker : int
+ Number of sc_points to summarize on each worker, by default 100
+
+ Returns
+ -------
+ summary : list
+ List of dictionaries, each being an SC point summary.
+ """
+
+ gen_index=self._parse_gen_index(gen_fpath)
+ chunks=int(np.ceil(len(self.gids)/sites_per_worker))
+ chunks=np.array_split(self.gids,chunks)
+
+ logger.info(
+ "Running supply curve point aggregation for "
+ "points {} through {} at a resolution of {} "
+ "on {} cores in {} chunks.".format(
+ self.gids[0],
+ self.gids[-1],
+ self._resolution,
+ max_workers,
+ len(chunks),
+ )
+ )
+
+ slice_lookup=None
+ ifself._inclusion_maskisnotNone:
+ withSupplyCurveExtent(
+ self._excl_fpath,resolution=self._resolution
+ )assc:
+ assertsc.exclusions.shape==self._inclusion_mask.shape
+ slice_lookup=sc.get_slice_lookup(self.gids)
+
+ futures=[]
+ summary=[]
+ n_finished=0
+ loggers=[__name__,"reV.supply_curve.point_summary","reV"]
+ withSpawnProcessPool(max_workers=max_workers,loggers=loggers)asexe:
+ # iterate through split executions, submitting each to worker
+ forgid_setinchunks:
+ # submit executions and append to futures list
+ chunk_incl_masks=None
+ ifself._inclusion_maskisnotNone:
+ chunk_incl_masks={}
+ forgidingid_set:
+ rs,cs=slice_lookup[gid]
+ chunk_incl_masks[gid]=self._inclusion_mask[rs,cs]
+
+ futures.append(
+ exe.submit(
+ self.run_serial,
+ self._excl_fpath,
+ gen_fpath,
+ self._tm_dset,
+ gen_index,
+ econ_fpath=self._econ_fpath,
+ excl_dict=self._excl_dict,
+ inclusion_mask=chunk_incl_masks,
+ res_class_dset=self._res_class_dset,
+ res_class_bins=self._res_class_bins,
+ cf_dset=self._cf_dset,
+ lcoe_dset=self._lcoe_dset,
+ h5_dsets=self._h5_dsets,
+ data_layers=self._data_layers,
+ resolution=self._resolution,
+ power_density=self._power_density,
+ friction_fpath=self._friction_fpath,
+ friction_dset=self._friction_dset,
+ area_filter_kernel=self._area_filter_kernel,
+ min_area=self._min_area,
+ gids=gid_set,
+ args=args,
+ excl_area=self._excl_area,
+ cap_cost_scale=self._cap_cost_scale,
+ recalc_lcoe=self._recalc_lcoe,
+ )
+ )
+
+ # gather results
+ forfutureinas_completed(futures):
+ n_finished+=1
+ summary+=future.result()
+ ifn_finished%10==0:
+ mem=psutil.virtual_memory()
+ logger.info(
+ "Parallel aggregation futures collected: "
+ "{} out of {}. Memory usage is {:.3f} GB out "
+ "of {:.3f} GB ({:.2f}% utilized).".format(
+ n_finished,
+ len(chunks),
+ mem.used/1e9,
+ mem.total/1e9,
+ 100*mem.used/mem.total,
+ )
+ )
+
+ returnsummary
+
+ @staticmethod
+ def_convert_bins(bins):
+"""Convert a list of floats or ints to a list of two-entry bin bounds.
+
+ Parameters
+ ----------
+ bins : list | None
+ List of floats or ints (bin edges) to convert to list of two-entry
+ bin boundaries or list of two-entry bind boundaries in final format
+
+ Returns
+ -------
+ bins : list
+ List of two-entry bin boundaries
+ """
+
+ ifbinsisNone:
+ returnNone
+
+ type_check=[isinstance(x,(list,tuple))forxinbins]
+
+ ifall(type_check):
+ returnbins
+
+ ifany(type_check):
+ raiseTypeError(
+ "Resource class bins has inconsistent "
+ "entry type: {}".format(bins)
+ )
+
+ bbins=[]
+ fori,binenumerate(sorted(bins)):
+ ifi<len(bins)-1:
+ bbins.append([b,bins[i+1]])
+
+ returnbbins
+
+ @staticmethod
+ def_summary_to_df(summary):
+"""Convert the agg summary list to a DataFrame.
+
+ Parameters
+ ----------
+ summary : list
+ List of dictionaries, each being an SC point summary.
+
+ Returns
+ -------
+ summary : DataFrame
+ Summary of the SC points.
+ """
+ summary=pd.DataFrame(summary)
+ sort_by=[xforxin(SupplyCurveField.SC_POINT_GID,'res_class')
+ ifxinsummary]
+ summary=summary.sort_values(sort_by)
+ summary=summary.reset_index(drop=True)
+ summary.index.name=SupplyCurveField.SC_GID
+
+ returnsummary
+
+
[docs]defsummarize(
+ self,gen_fpath,args=None,max_workers=None,sites_per_worker=100
+ ):
+"""
+ Get the supply curve points aggregation summary
+
+ Parameters
+ ----------
+ gen_fpath : str
+ Filepath to .h5 reV generation output results.
+ args : tuple | list | None
+ List of summary arguments to include. None defaults to all
+ available args defined in the class attr.
+ max_workers : int | None, optional
+ Number of cores to run summary on. None is all
+ available cpus, by default None
+ sites_per_worker : int
+ Number of sc_points to summarize on each worker, by default 100
+
+ Returns
+ -------
+ summary : list
+ List of dictionaries, each being an SC point summary.
+ """
+ ifmax_workersisNone:
+ max_workers=os.cpu_count()
+
+ ifmax_workers==1:
+ gen_index=self._parse_gen_index(gen_fpath)
+ afk=self._area_filter_kernel
+ summary=self.run_serial(
+ self._excl_fpath,
+ gen_fpath,
+ self._tm_dset,
+ gen_index,
+ econ_fpath=self._econ_fpath,
+ excl_dict=self._excl_dict,
+ inclusion_mask=self._inclusion_mask,
+ res_class_dset=self._res_class_dset,
+ res_class_bins=self._res_class_bins,
+ cf_dset=self._cf_dset,
+ lcoe_dset=self._lcoe_dset,
+ h5_dsets=self._h5_dsets,
+ data_layers=self._data_layers,
+ resolution=self._resolution,
+ power_density=self._power_density,
+ friction_fpath=self._friction_fpath,
+ friction_dset=self._friction_dset,
+ area_filter_kernel=afk,
+ min_area=self._min_area,
+ gids=self.gids,
+ args=args,
+ excl_area=self._excl_area,
+ cap_cost_scale=self._cap_cost_scale,
+ recalc_lcoe=self._recalc_lcoe,
+ )
+ else:
+ summary=self.run_parallel(
+ gen_fpath=gen_fpath,
+ args=args,
+ max_workers=max_workers,
+ sites_per_worker=sites_per_worker,
+ )
+
+ ifnotany(summary):
+ e=(
+ "Supply curve aggregation found no non-excluded SC points. "
+ "Please check your exclusions or subset SC GID selection."
+ )
+ logger.error(e)
+ raiseEmptySupplyCurvePointError(e)
+
+ summary=self._summary_to_df(summary)
+
+ returnsummary
+
+
[docs]defrun(
+ self,
+ out_fpath,
+ gen_fpath=None,
+ args=None,
+ max_workers=None,
+ sites_per_worker=100,
+ ):
+"""Run a supply curve aggregation.
+
+ Parameters
+ ----------
+ gen_fpath : str, optional
+ Filepath to HDF5 file with ``reV`` generation output
+ results. If ``None``, a simple aggregation without any
+ generation, resource, or cost data is performed.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ input can be set to ``"PIPELINE"`` to parse the output
+ from one of these preceding pipeline steps:
+ ``multi-year``, ``collect``, or ``econ``. However, note
+ that duplicate executions of any of these commands within
+ the pipeline may invalidate this parsing, meaning the
+ `gen_fpath` input will have to be specified manually.
+
+ By default, ``None``.
+ args : tuple | list, optional
+ List of columns to include in summary output table. ``None``
+ defaults to all available args defined in the
+ :class:`~reV.supply_curve.sc_aggregation.SupplyCurveAggregation`
+ documentation. By default, ``None``.
+ max_workers : int, optional
+ Number of cores to run summary on. ``None`` is all available
+ CPUs. By default, ``None``.
+ sites_per_worker : int, optional
+ Number of sc_points to summarize on each worker.
+ By default, ``100``.
+
+ Returns
+ -------
+ str
+ Path to output CSV file containing supply curve aggregation.
+ """
+
+ ifgen_fpathisNone:
+ out=Aggregation.run(
+ self._excl_fpath,
+ self._res_fpath,
+ self._tm_dset,
+ excl_dict=self._excl_dict,
+ resolution=self._resolution,
+ excl_area=self._excl_area,
+ area_filter_kernel=self._area_filter_kernel,
+ min_area=self._min_area,
+ pre_extract_inclusions=self._pre_extract_inclusions,
+ max_workers=max_workers,
+ sites_per_worker=sites_per_worker,
+ )
+ summary=out["meta"]
+ else:
+ summary=self.summarize(
+ gen_fpath=gen_fpath,
+ args=args,
+ max_workers=max_workers,
+ sites_per_worker=sites_per_worker,
+ )
+
+ out_fpath=_format_sc_agg_out_fpath(out_fpath)
+ summary.to_csv(out_fpath)
+
+ returnout_fpath
+
+
+def_format_sc_agg_out_fpath(out_fpath):
+"""Add CSV file ending and replace underscore, if necessary."""
+ ifnotout_fpath.endswith(".csv"):
+ out_fpath="{}.csv".format(out_fpath)
+
+ project_dir,out_fn=os.path.split(out_fpath)
+ out_fn=out_fn.replace(
+ "supply_curve_aggregation","supply-curve-aggregation"
+ )
+ returnos.path.join(project_dir,out_fn)
+
+
+def_warn_about_large_datasets(gen,dset):
+"""Warn user about multi-dimensional datasets in passthrough datasets"""
+ dset_shape=gen.shapes.get(dset,(1,))
+ iflen(dset_shape)>1:
+ msg=(
+ "Generation dataset {!r} is not 1-dimensional (shape: {})."
+ "You may run into memory errors during aggregation - use "
+ "rep-profiles for aggregating higher-order datasets instead!"
+ .format(dset,dset_shape)
+ )
+ logger.warning(msg)
+ warn(msg,UserWarning)
+
+# -*- coding: utf-8 -*-
+"""
+reV supply curve module
+- Calculation of LCOT
+- Supply Curve creation
+"""
+importjson
+importlogging
+importos
+fromitertoolsimportchain
+fromcopyimportdeepcopy
+fromwarningsimportwarn
+
+importnumpyasnp
+importpandasaspd
+fromreximportResource
+fromrex.utilitiesimportSpawnProcessPool,parse_table
+
+fromreV.handlers.transmissionimportTransmissionCostsasTC
+fromreV.handlers.transmissionimportTransmissionFeaturesasTF
+fromreV.supply_curve.competitive_wind_farmsimportCompetitiveWindFarms
+fromreV.utilitiesimportSupplyCurveField,log_versions
+fromreV.utilities.exceptionsimportSupplyCurveError,SupplyCurveInputError
+
+logger=logging.getLogger(__name__)
+
+
+# map is column name to relative order in which it should appear in output file
+_REQUIRED_COMPUTE_AND_OUTPUT_COLS={
+ SupplyCurveField.TRANS_GID:0,
+ SupplyCurveField.TRANS_TYPE:1,
+ SupplyCurveField.N_PARALLEL_TRANS:2,
+ SupplyCurveField.DIST_SPUR_KM:3,
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW:10,
+ SupplyCurveField.LCOT:11,
+ SupplyCurveField.TOTAL_LCOE:12,
+}
+_REQUIRED_OUTPUT_COLS={SupplyCurveField.DIST_EXPORT_KM:4,
+ SupplyCurveField.REINFORCEMENT_DIST_KM:5,
+ SupplyCurveField.TIE_LINE_COST_PER_MW:6,
+ SupplyCurveField.CONNECTION_COST_PER_MW:7,
+ SupplyCurveField.EXPORT_COST_PER_MW:8,
+ SupplyCurveField.REINFORCEMENT_COST_PER_MW:9,
+ SupplyCurveField.POI_LAT:13,
+ SupplyCurveField.POI_LON:14,
+ SupplyCurveField.REINFORCEMENT_POI_LAT:15,
+ SupplyCurveField.REINFORCEMENT_POI_LON:16}
+DEFAULT_COLUMNS=tuple(str(field)
+ forfieldinchain(_REQUIRED_COMPUTE_AND_OUTPUT_COLS,
+ _REQUIRED_OUTPUT_COLS))
+"""Default output columns from supply chain computation (not ordered)"""
+
+
+
[docs]classSupplyCurve:
+"""SupplyCurve"""
+
+ def__init__(self,sc_points,trans_table,sc_features=None,
+ # str() to fix docs
+ sc_capacity_col=str(SupplyCurveField.CAPACITY_AC_MW)):
+"""ReV LCOT calculation and SupplyCurve sorting class.
+
+ ``reV`` supply curve computes the transmission costs associated
+ with each supply curve point output by ``reV`` supply curve
+ aggregation. Transmission costs can either be computed
+ competitively (where total capacity remaining on the
+ transmission grid is tracked and updated after each new
+ connection) or non-competitively (where the cheapest connections
+ for each supply curve point are allowed regardless of the
+ remaining transmission grid capacity). In both cases, the
+ permutation of transmission costs between supply curve points
+ and transmission grid features should be computed using the
+ `reVX Least Cost Transmission Paths
+ <https://github.com/NREL/reVX/tree/main/reVX/least_cost_xmission>`_
+ utility.
+
+ Parameters
+ ----------
+ sc_points : str | pandas.DataFrame
+ Path to CSV or JSON or DataFrame containing supply curve
+ point summary. Can also be a filepath to a ``reV`` bespoke
+ HDF5 output file where the ``meta`` dataset has the same
+ format as the supply curve aggregation output.
+
+ .. Note:: If executing ``reV`` from the command line, this
+ input can also be ``"PIPELINE"`` to parse the output of
+ the previous pipeline step and use it as input to this
+ call. However, note that duplicate executions of any
+ preceding commands within the pipeline may invalidate this
+ parsing, meaning the `sc_points` input will have to be
+ specified manually.
+
+ trans_table : str | pandas.DataFrame | list
+ Path to CSV or JSON or DataFrame containing supply curve
+ transmission mapping. This can also be a list of
+ transmission tables with different line voltage (capacity)
+ ratings. See the `reVX Least Cost Transmission Paths
+ <https://github.com/NREL/reVX/tree/main/reVX/least_cost_xmission>`_
+ utility to generate these input tables.
+ sc_features : str | pandas.DataFrame, optional
+ Path to CSV or JSON or DataFrame containing additional
+ supply curve features (e.g. transmission multipliers,
+ regions, etc.). These features will be merged to the
+ `sc_points` input table on ALL columns that both have in
+ common. If ``None``, no extra supply curve features are
+ added. By default, ``None``.
+ sc_capacity_col : str, optional
+ Name of capacity column in `trans_sc_table`. The values in
+ this column determine the size of transmission lines built.
+ The transmission capital costs per MW and the reinforcement
+ costs per MW will be returned in terms of these capacity
+ values. Note that if this column != "capacity", then
+ "capacity" must also be included in `trans_sc_table` since
+ those values match the "mean_cf" data (which is used to
+ calculate LCOT and Total LCOE). This input can be used to,
+ e.g., size transmission lines based on solar AC capacity (
+ ``sc_capacity_col="capacity_ac"``). By default,
+ ``"capacity"``.
+
+ Examples
+ --------
+ Standard outputs in addition to the values provided in
+ `sc_points`, produced by
+ :class:`reV.supply_curve.sc_aggregation.SupplyCurveAggregation`:
+
+ - transmission_multiplier : int | float
+ Transmission cost multiplier that scales the line cost
+ but not the tie-in cost in the calculation of LCOT.
+ - trans_gid : int
+ Unique transmission feature identifier that each supply
+ curve point was connected to.
+ - trans_capacity : float
+ Total capacity (not available capacity) of the
+ transmission feature that each supply curve point was
+ connected to. Default units are MW.
+ - trans_type : str
+ Tranmission feature type that each supply curve point
+ was connected to (e.g. Transline, Substation).
+ - trans_cap_cost_per_mw : float
+ Capital cost of connecting each supply curve point to
+ their respective transmission feature. This value
+ includes line cost with transmission_multiplier and the
+ tie-in cost. Default units are $/MW.
+ - dist_km : float
+ Distance in km from supply curve point to transmission
+ connection.
+ - lcot : float
+ Levelized cost of connecting to transmission ($/MWh).
+ - total_lcoe : float
+ Total LCOE of each supply curve point (mean_lcoe + lcot)
+ ($/MWh).
+ - total_lcoe_friction : float
+ Total LCOE of each supply curve point considering the
+ LCOE friction scalar from the aggregation step
+ (mean_lcoe_friction + lcot) ($/MWh).
+ """
+ log_versions(logger)
+ logger.info("Supply curve points input: {}".format(sc_points))
+ logger.info("Transmission table input: {}".format(trans_table))
+ logger.info("Supply curve capacity column: {}".format(sc_capacity_col))
+
+ self._sc_capacity_col=sc_capacity_col
+ self._sc_points=self._parse_sc_points(
+ sc_points,sc_features=sc_features
+ )
+ self._trans_table=self._map_tables(
+ self._sc_points,trans_table,sc_capacity_col=sc_capacity_col
+ )
+ self._sc_gids,self._mask=self._parse_sc_gids(self._trans_table)
+
+ def__repr__(self):
+ msg="{} with {} points".format(self.__class__.__name__,len(self))
+
+ returnmsg
+
+ def__len__(self):
+ returnlen(self._sc_gids)
+
+ def__getitem__(self,gid):
+ ifgidnotinself._sc_gids:
+ msg="Invalid supply curve gid {}".format(gid)
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ i=self._sc_gids.index(gid)
+
+ returnself._sc_points.iloc[i]
+
+ @staticmethod
+ def_parse_sc_points(sc_points,sc_features=None):
+"""
+ Import supply curve point summary and add any additional features
+
+ Parameters
+ ----------
+ sc_points : str | pandas.DataFrame
+ Path to .csv or .json or DataFrame containing supply curve point
+ summary. Can also now be a filepath to a bespoke h5 where the
+ "meta" dataset has the same format as the sc aggregation output.
+ sc_features : str | pandas.DataFrame
+ Path to .csv or .json or DataFrame containing additional supply
+ curve features, e.g. transmission multipliers, regions
+
+ Returns
+ -------
+ sc_points : pandas.DataFrame
+ DataFrame of supply curve point summary with additional features
+ added if supplied
+ """
+ ifisinstance(sc_points,str)andsc_points.endswith(".h5"):
+ withResource(sc_points)asres:
+ sc_points=res.meta
+ sc_points.index.name=SupplyCurveField.SC_GID
+ sc_points=sc_points.reset_index()
+ else:
+ sc_points=parse_table(sc_points)
+ sc_points=sc_points.rename(
+ columns=SupplyCurveField.map_from_legacy())
+
+ logger.debug(
+ "Supply curve points table imported with columns: {}".format(
+ sc_points.columns.values.tolist()
+ )
+ )
+
+ ifsc_featuresisnotNone:
+ sc_features=parse_table(sc_features)
+ sc_features=sc_features.rename(
+ columns=SupplyCurveField.map_from_legacy())
+ merge_cols=[cforcinsc_featuresifcinsc_points]
+ sc_points=sc_points.merge(sc_features,on=merge_cols,how="left")
+ logger.debug(
+ "Adding Supply Curve Features table with columns: {}".format(
+ sc_features.columns.values.tolist()
+ )
+ )
+
+ if"transmission_multiplier"insc_points:
+ col="transmission_multiplier"
+ sc_points.loc[:,col]=sc_points.loc[:,col].fillna(1)
+
+ logger.debug(
+ "Final supply curve points table has columns: {}".format(
+ sc_points.columns.values.tolist()
+ )
+ )
+
+ returnsc_points
+
+ @staticmethod
+ def_get_merge_cols(sc_columns,trans_columns):
+"""
+ Get columns with 'row' or 'col' in them to use for merging
+
+ Parameters
+ ----------
+ sc_columns : list
+ Columns to search
+ trans_cols
+
+ Returns
+ -------
+ merge_cols : dict
+ Columns to merge on which maps the sc columns (keys) to the
+ corresponding trans table columns (values)
+ """
+ sc_columns=[cforcinsc_columnsifc.startswith("sc_")]
+ trans_columns=[cforcintrans_columnsifc.startswith("sc_")]
+ merge_cols={}
+ forc_valin["row","col"]:
+ trans_col=[cforcintrans_columnsifc_valinc]
+ sc_col=[cforcinsc_columnsifc_valinc]
+ iftrans_colandsc_col:
+ merge_cols[sc_col[0]]=trans_col[0]
+
+ iflen(merge_cols)!=2:
+ msg=(
+ "Did not find a unique set of sc row and column ids to "
+ "merge on: {}".format(merge_cols)
+ )
+ logger.error(msg)
+ raiseRuntimeError(msg)
+
+ returnmerge_cols
+
+ @staticmethod
+ def_parse_trans_table(trans_table):
+"""
+ Import transmission features table
+
+ Parameters
+ ----------
+ trans_table : pd.DataFrame | str
+ Table mapping supply curve points to transmission features
+ (either str filepath to table file or pre-loaded dataframe).
+
+ Returns
+ -------
+ trans_table : pd.DataFrame
+ Loaded transmission feature table.
+ """
+
+ trans_table=parse_table(trans_table)
+
+ # Update legacy transmission table columns to match new less ambiguous
+ # column names:
+ # trans_gid -> the transmission feature id, legacy name: trans_line_gid
+ # trans_line_gids -> gids of transmission lines connected to the given
+ # transmission feature (only used for Substations),
+ # legacy name: trans_gids
+ # also xformer_cost_p_mw -> xformer_cost_per_mw (not sure why there
+ # would be a *_p_mw but here we are...)
+ rename_map={
+ "trans_line_gid":SupplyCurveField.TRANS_GID,
+ "trans_gids":"trans_line_gids",
+ "xformer_cost_p_mw":"xformer_cost_per_mw",
+ }
+ trans_table=trans_table.rename(columns=rename_map)
+
+ contains_dist_in_miles="dist_mi"intrans_table
+ missing_km_dist=SupplyCurveField.DIST_SPUR_KMnotintrans_table
+ ifcontains_dist_in_milesandmissing_km_dist:
+ trans_table=trans_table.rename(
+ columns={"dist_mi":SupplyCurveField.DIST_SPUR_KM}
+ )
+ trans_table[SupplyCurveField.DIST_SPUR_KM]*=1.60934
+
+ drop_cols=[SupplyCurveField.SC_GID,'cap_left',
+ SupplyCurveField.SC_POINT_GID]
+ drop_cols=[cforcindrop_colsifcintrans_table]
+ ifdrop_cols:
+ trans_table=trans_table.drop(columns=drop_cols)
+
+ returntrans_table.rename(columns=SupplyCurveField.map_from_legacy())
+
+ @staticmethod
+ def_map_trans_capacity(trans_sc_table,
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
+"""
+ Map SC gids to transmission features based on capacity. For any SC
+ gids with capacity > the maximum transmission feature capacity, map
+ SC gids to the feature with the largest capacity
+
+ Parameters
+ ----------
+ trans_sc_table : pandas.DataFrame
+ Table mapping supply curve points to transmission features.
+ sc_capacity_col : str, optional
+ Name of capacity column in `trans_sc_table`. The values in
+ this column determine the size of transmission lines built.
+ The transmission capital costs per MW and the reinforcement
+ costs per MW will be returned in terms of these capacity
+ values. Note that if this column != "capacity", then
+ "capacity" must also be included in `trans_sc_table` since
+ those values match the "mean_cf" data (which is used to
+ calculate LCOT and Total LCOE). By default, ``"capacity"``.
+
+ Returns
+ -------
+ trans_sc_table : pandas.DataFrame
+ Updated table mapping supply curve points to transmission features
+ based on maximum capacity
+ """
+
+ nx=trans_sc_table[sc_capacity_col]/trans_sc_table["max_cap"]
+ nx=np.ceil(nx).astype(int)
+ trans_sc_table[SupplyCurveField.N_PARALLEL_TRANS]=nx
+
+ if(nx>1).any():
+ mask=nx>1
+ tie_line_cost=(
+ trans_sc_table.loc[mask,"tie_line_cost"]*nx[mask]
+ )
+
+ xformer_cost=(
+ trans_sc_table.loc[mask,"xformer_cost_per_mw"]
+ *trans_sc_table.loc[mask,"max_cap"]
+ *nx[mask]
+ )
+
+ conn_cost=(
+ xformer_cost
+ +trans_sc_table.loc[mask,"sub_upgrade_cost"]
+ +trans_sc_table.loc[mask,"new_sub_cost"]
+ )
+
+ trans_cap_cost=tie_line_cost+conn_cost
+
+ trans_sc_table.loc[mask,"tie_line_cost"]=tie_line_cost
+ trans_sc_table.loc[mask,"xformer_cost"]=xformer_cost
+ trans_sc_table.loc[mask,"connection_cost"]=conn_cost
+ trans_sc_table.loc[mask,"trans_cap_cost"]=trans_cap_cost
+
+ msg=(
+ "{} SC points have a capacity that exceeds the maximum "
+ "transmission feature capacity and will be connected with "
+ "multiple parallel transmission features.".format(
+ (nx>1).sum()
+ )
+ )
+ logger.info(msg)
+
+ returntrans_sc_table
+
+ @staticmethod
+ def_parse_trans_line_gids(trans_line_gids):
+"""
+ Parse json string of trans_line_gids if needed
+
+ Parameters
+ ----------
+ trans_line_gids : str | list
+ list of transmission line 'trans_gid's, if a json string, convert
+ to list
+
+ Returns
+ -------
+ trans_line_gids : list
+ list of transmission line 'trans_gid's
+ """
+ ifisinstance(trans_line_gids,str):
+ trans_line_gids=json.loads(trans_line_gids)
+
+ returntrans_line_gids
+
+ @classmethod
+ def_check_sub_trans_lines(cls,features):
+"""
+ Check to make sure all trans-lines are available for all sub-stations
+
+ Parameters
+ ----------
+ features : pandas.DataFrame
+ Table of transmission feature to check substation to transmission
+ line gid connections
+
+ Returns
+ -------
+ line_gids : list
+ List of missing transmission line 'trans_gid's for all substations
+ in features table
+ """
+ features=features.rename(
+ columns={
+ "trans_line_gid":SupplyCurveField.TRANS_GID,
+ "trans_gids":"trans_line_gids",
+ }
+ )
+ mask=(features[SupplyCurveField.TRANS_TYPE].str.casefold()
+ =="substation")
+
+ ifnotany(mask):
+ return[]
+
+ line_gids=features.loc[mask,"trans_line_gids"].apply(
+ cls._parse_trans_line_gids
+ )
+
+ line_gids=np.unique(np.concatenate(line_gids.values))
+
+ test=np.isin(line_gids,features[SupplyCurveField.TRANS_GID].values)
+
+ returnline_gids[~test].tolist()
+
+ @classmethod
+ def_check_substation_conns(cls,trans_table,
+ sc_cols=SupplyCurveField.SC_GID):
+"""
+ Run checks on substation transmission features to make sure that
+ every sc point connecting to a substation can also connect to its
+ respective transmission lines
+
+ Parameters
+ ----------
+ trans_table : pd.DataFrame
+ Table mapping supply curve points to transmission features
+ (should already be merged with SC points).
+ sc_cols : str | list, optional
+ Column(s) in trans_table with unique supply curve id,
+ by default SupplyCurveField.SC_GID
+ """
+ missing={}
+ forsc_point,sc_tableintrans_table.groupby(sc_cols):
+ tl_gids=cls._check_sub_trans_lines(sc_table)
+ iftl_gids:
+ missing[sc_point]=tl_gids
+
+ ifany(missing):
+ msg=(
+ "The following sc_gid (keys) were connected to substations "
+ "but were not connected to the respective transmission line"
+ " gids (values) which is required for full SC sort: {}".format(
+ missing
+ )
+ )
+ logger.error(msg)
+ raiseSupplyCurveInputError(msg)
+
+ @classmethod
+ def_check_sc_trans_table(cls,sc_points,trans_table):
+"""Run self checks on sc_points table and the merged trans_table
+
+ Parameters
+ ----------
+ sc_points : pd.DataFrame
+ Table of supply curve point summary
+ trans_table : pd.DataFrame
+ Table mapping supply curve points to transmission features
+ (should already be merged with SC points).
+ """
+ sc_gids=set(sc_points[SupplyCurveField.SC_GID].unique())
+ trans_sc_gids=set(trans_table[SupplyCurveField.SC_GID].unique())
+ missing=sorted(list(sc_gids-trans_sc_gids))
+ ifany(missing):
+ msg=(
+ "There are {} Supply Curve points with missing "
+ "transmission mappings. Supply curve points with no "
+ "transmission features will not be connected! "
+ "Missing sc_gid's: {}".format(len(missing),missing)
+ )
+ logger.warning(msg)
+ warn(msg)
+
+ ifnotany(trans_sc_gids)ornotany(sc_gids):
+ msg=(
+ "Merging of sc points table and transmission features "
+ "table failed with {} original sc gids and {} transmission "
+ "sc gids after table merge.".format(
+ len(sc_gids),len(trans_sc_gids)
+ )
+ )
+ logger.error(msg)
+ raiseSupplyCurveError(msg)
+
+ logger.debug(
+ "There are {} original SC gids and {} sc gids in the "
+ "merged transmission table.".format(
+ len(sc_gids),len(trans_sc_gids)
+ )
+ )
+ logger.debug(
+ "Transmission Table created with columns: {}".format(
+ trans_table.columns.values.tolist()
+ )
+ )
+
+ @classmethod
+ def_merge_sc_trans_tables(cls,sc_points,trans_table,
+ sc_cols=(SupplyCurveField.SC_GID,
+ SupplyCurveField.CAPACITY_AC_MW,
+ SupplyCurveField.MEAN_CF_AC,
+ SupplyCurveField.MEAN_LCOE),
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW
+ ):
+"""
+ Merge the supply curve table with the transmission features table.
+
+ Parameters
+ ----------
+ sc_points : pd.DataFrame
+ Table of supply curve point summary
+ trans_table : pd.DataFrame | str
+ Table mapping supply curve points to transmission features
+ (either str filepath to table file, list of filepaths to tables by
+ line voltage (capacity) or pre-loaded dataframe).
+ sc_cols : tuple | list, optional
+ List of column from sc_points to transfer into the trans table,
+ If the `sc_capacity_col` is not included, it will get added.
+ by default (SupplyCurveField.SC_GID, 'capacity', 'mean_cf',
+ 'mean_lcoe')
+ sc_capacity_col : str, optional
+ Name of capacity column in `trans_sc_table`. The values in
+ this column determine the size of transmission lines built.
+ The transmission capital costs per MW and the reinforcement
+ costs per MW will be returned in terms of these capacity
+ values. Note that if this column != "capacity", then
+ "capacity" must also be included in `trans_sc_table` since
+ those values match the "mean_cf" data (which is used to
+ calculate LCOT and Total LCOE). By default, ``"capacity"``.
+
+ Returns
+ -------
+ trans_sc_table : pd.DataFrame
+ Updated table mapping supply curve points to transmission features.
+ This is performed by an inner merging with trans_table
+ """
+ ifsc_capacity_colnotinsc_cols:
+ sc_cols=tuple([sc_capacity_col]+list(sc_cols))
+
+ ifisinstance(trans_table,(list,tuple)):
+ trans_sc_table=[]
+ fortableintrans_table:
+ trans_sc_table.append(
+ cls._merge_sc_trans_tables(
+ sc_points,
+ table,
+ sc_cols=sc_cols,
+ sc_capacity_col=sc_capacity_col,
+ )
+ )
+
+ trans_sc_table=pd.concat(trans_sc_table)
+ else:
+ trans_table=cls._parse_trans_table(trans_table)
+
+ merge_cols=cls._get_merge_cols(
+ sc_points.columns,trans_table.columns
+ )
+ logger.info(
+ "Merging SC table and Trans Table with "
+ "{} mapping: {}".format(
+ "sc_table_col: trans_table_col",merge_cols
+ )
+ )
+ sc_points=sc_points.rename(columns=merge_cols)
+ merge_cols=list(merge_cols.values())
+
+ ifisinstance(sc_cols,tuple):
+ sc_cols=list(sc_cols)
+
+ extra_cols=[SupplyCurveField.CAPACITY_DC_MW,
+ SupplyCurveField.MEAN_CF_DC,
+ SupplyCurveField.MEAN_LCOE_FRICTION,
+ "transmission_multiplier"]
+ forcolinextra_cols:
+ ifcolinsc_points:
+ sc_cols.append(col)
+
+ sc_cols+=merge_cols
+ sc_points=sc_points[sc_cols].copy()
+ trans_sc_table=trans_table.merge(
+ sc_points,on=merge_cols,how="inner"
+ )
+
+ returntrans_sc_table
+
+ @classmethod
+ def_map_tables(cls,sc_points,trans_table,
+ sc_cols=(SupplyCurveField.SC_GID,
+ SupplyCurveField.CAPACITY_AC_MW,
+ SupplyCurveField.MEAN_CF_AC,
+ SupplyCurveField.MEAN_LCOE),
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
+"""
+ Map supply curve points to transmission features
+
+ Parameters
+ ----------
+ sc_points : pd.DataFrame
+ Table of supply curve point summary
+ trans_table : pd.DataFrame | str
+ Table mapping supply curve points to transmission features
+ (either str filepath to table file, list of filepaths to tables by
+ line voltage (capacity) or pre-loaded DataFrame).
+ sc_cols : tuple | list, optional
+ List of column from sc_points to transfer into the trans table,
+ If the `sc_capacity_col` is not included, it will get added.
+ by default (SupplyCurveField.SC_GID,
+ SupplyCurveField.CAPACITY_AC_MW, SupplyCurveField.MEAN_CF_AC,
+ SupplyCurveField.MEAN_LCOE)
+ sc_capacity_col : str, optional
+ Name of capacity column in `trans_sc_table`. The values in
+ this column determine the size of transmission lines built.
+ The transmission capital costs per MW and the reinforcement
+ costs per MW will be returned in terms of these capacity
+ values. Note that if this column != "capacity", then
+ "capacity" must also be included in `trans_sc_table` since
+ those values match the "mean_cf" data (which is used to
+ calculate LCOT and Total LCOE). By default, ``"capacity"``.
+
+ Returns
+ -------
+ trans_sc_table : pd.DataFrame
+ Updated table mapping supply curve points to transmission features.
+ This is performed by an inner merging with trans_table
+ """
+ scc=sc_capacity_col
+ trans_sc_table=cls._merge_sc_trans_tables(
+ sc_points,trans_table,sc_cols=sc_cols,sc_capacity_col=scc
+ )
+
+ if"max_cap"intrans_sc_table:
+ trans_sc_table=cls._map_trans_capacity(
+ trans_sc_table,sc_capacity_col=scc
+ )
+
+ sort_cols=[SupplyCurveField.SC_GID,SupplyCurveField.TRANS_GID]
+ trans_sc_table=trans_sc_table.sort_values(sort_cols)
+ trans_sc_table=trans_sc_table.reset_index(drop=True)
+
+ cls._check_sc_trans_table(sc_points,trans_sc_table)
+
+ returntrans_sc_table
+
+ @staticmethod
+ def_create_handler(trans_table,trans_costs=None,avail_cap_frac=1):
+"""
+ Create TransmissionFeatures handler from supply curve transmission
+ mapping table. Update connection costs if given.
+
+ Parameters
+ ----------
+ trans_table : str | pandas.DataFrame
+ Path to .csv or .json or DataFrame containing supply curve
+ transmission mapping
+ trans_costs : str | dict
+ Transmission feature costs to use with TransmissionFeatures
+ handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+ center_tie_in_cost, sink_tie_in_cost
+ avail_cap_frac: int, optional
+ Fraction of transmissions features capacity 'ac_cap' to make
+ available for connection to supply curve points, by default 1
+
+ Returns
+ -------
+ trans_features : TransmissionFeatures
+ TransmissionFeatures or TransmissionCosts instance initilized
+ with specified transmission costs
+ """
+ iftrans_costsisnotNone:
+ kwargs=TF._parse_dictionary(trans_costs)
+ else:
+ kwargs={}
+
+ trans_features=TF(
+ trans_table,avail_cap_frac=avail_cap_frac,**kwargs
+ )
+
+ returntrans_features
+
+ @staticmethod
+ def_parse_sc_gids(trans_table,gid_key=SupplyCurveField.SC_GID):
+"""Extract unique sc gids, make bool mask from tranmission table
+
+ Parameters
+ ----------
+ trans_table : pd.DataFrame
+ reV Supply Curve table joined with transmission features table.
+ gid_key : str
+ Column label in trans_table containing the supply curve points
+ primary key.
+
+ Returns
+ -------
+ sc_gids : list
+ List of unique integer supply curve gids (non-nan)
+ mask : np.ndarray
+ Boolean array initialized as true. Length is equal to the maximum
+ SC gid so that the SC gids can be used to index the mask directly.
+ """
+ sc_gids=list(np.sort(trans_table[gid_key].unique()))
+ sc_gids=[int(gid)forgidinsc_gids]
+ mask=np.ones(int(1+max(sc_gids)),dtype=bool)
+
+ returnsc_gids,mask
+
+ @staticmethod
+ def_get_capacity(sc_gid,sc_table,connectable=True,
+ sc_capacity_col=SupplyCurveField.CAPACITY_AC_MW):
+"""
+ Get capacity of supply curve point
+
+ Parameters
+ ----------
+ sc_gid : int
+ Supply curve gid
+ sc_table : pandas.DataFrame
+ DataFrame of sc point to transmission features mapping for given
+ sc_gid
+ connectable : bool, optional
+ Flag to ensure SC point can connect to transmission features,
+ by default True
+ sc_capacity_col : str, optional
+ Name of capacity column in `trans_sc_table`. The values in
+ this column determine the size of transmission lines built.
+ The transmission capital costs per MW and the reinforcement
+ costs per MW will be returned in terms of these capacity
+ values. Note that if this column != "capacity", then
+ "capacity" must also be included in `trans_sc_table` since
+ those values match the "mean_cf" data (which is used to
+ calculate LCOT and Total LCOE). By default, ``"capacity"``.
+
+ Returns
+ -------
+ capacity : float
+ Capacity of supply curve point
+ """
+ ifconnectable:
+ capacity=sc_table[sc_capacity_col].unique()
+ iflen(capacity)==1:
+ capacity=capacity[0]
+ else:
+ msg=(
+ "Each supply curve point should only have "
+ "a single capacity, but {} has {}".format(sc_gid,capacity)
+ )
+ logger.error(msg)
+ raiseRuntimeError(msg)
+ else:
+ capacity=None
+
+ returncapacity
+
+ @classmethod
+ def_compute_trans_cap_cost(cls,trans_table,trans_costs=None,
+ avail_cap_frac=1,max_workers=None,
+ connectable=True,line_limited=False,
+ sc_capacity_col=(
+ SupplyCurveField.CAPACITY_AC_MW)):
+"""
+ Compute levelized cost of transmission for all combinations of
+ supply curve points and tranmission features in trans_table
+
+ Parameters
+ ----------
+ trans_table : pd.DataFrame
+ Table mapping supply curve points to transmission features
+ MUST contain `sc_capacity_col` column.
+ fcr : float
+ Fixed charge rate needed to compute LCOT
+ trans_costs : str | dict
+ Transmission feature costs to use with TransmissionFeatures
+ handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+ center_tie_in_cost, sink_tie_in_cost
+ avail_cap_frac: int, optional
+ Fraction of transmissions features capacity 'ac_cap' to make
+ available for connection to supply curve points, by default 1
+ max_workers : int | NoneType
+ Number of workers to use to compute lcot, if > 1 run in parallel.
+ None uses all available cpu's.
+ connectable : bool, optional
+ Flag to only compute tranmission capital cost if transmission
+ feature has enough available capacity, by default True
+ line_limited : bool
+ Substation connection is limited by maximum capacity of the
+ attached lines, legacy method
+ sc_capacity_col : str, optional
+ Name of capacity column in `trans_sc_table`. The values in
+ this column determine the size of transmission lines built.
+ The transmission capital costs per MW and the reinforcement
+ costs per MW will be returned in terms of these capacity
+ values. Note that if this column != "capacity", then
+ "capacity" must also be included in `trans_sc_table` since
+ those values match the "mean_cf" data (which is used to
+ calculate LCOT and Total LCOE). By default, ``"capacity"``.
+
+ Returns
+ -------
+ lcot : list
+ Levelized cost of transmission for all supply curve -
+ tranmission feature connections
+ cost : list
+ Capital cost of tramsmission for all supply curve - transmission
+ feature connections
+ """
+ scc=sc_capacity_col
+ ifsccnotintrans_table:
+ raiseSupplyCurveInputError(
+ "Supply curve table must have "
+ "supply curve point capacity column"
+ "({}) to compute lcot".format(scc)
+ )
+
+ iftrans_costsisnotNone:
+ trans_costs=TF._parse_dictionary(trans_costs)
+ else:
+ trans_costs={}
+
+ ifmax_workersisNone:
+ max_workers=os.cpu_count()
+
+ logger.info('Computing LCOT costs for all possible connections...')
+ groups=trans_table.groupby(SupplyCurveField.SC_GID)
+ ifmax_workers>1:
+ loggers=[__name__,"reV.handlers.transmission","reV"]
+ withSpawnProcessPool(
+ max_workers=max_workers,loggers=loggers
+ )asexe:
+ futures=[]
+ forsc_gid,sc_tableingroups:
+ capacity=cls._get_capacity(
+ sc_gid,
+ sc_table,
+ connectable=connectable,
+ sc_capacity_col=scc,
+ )
+ futures.append(
+ exe.submit(
+ TC.feature_costs,
+ sc_table,
+ capacity=capacity,
+ avail_cap_frac=avail_cap_frac,
+ line_limited=line_limited,
+ **trans_costs,
+ )
+ )
+
+ cost=[future.result()forfutureinfutures]
+ else:
+ cost=[]
+ forsc_gid,sc_tableingroups:
+ capacity=cls._get_capacity(
+ sc_gid,
+ sc_table,
+ connectable=connectable,
+ sc_capacity_col=scc,
+ )
+ cost.append(
+ TC.feature_costs(
+ sc_table,
+ capacity=capacity,
+ avail_cap_frac=avail_cap_frac,
+ line_limited=line_limited,
+ **trans_costs,
+ )
+ )
+
+ cost=np.hstack(cost).astype("float32")
+ logger.info("LCOT cost calculation is complete.")
+
+ returncost
+
+
[docs]defcompute_total_lcoe(
+ self,
+ fcr,
+ transmission_costs=None,
+ avail_cap_frac=1,
+ line_limited=False,
+ connectable=True,
+ max_workers=None,
+ consider_friction=True,
+ ):
+"""
+ Compute LCOT and total LCOE for all sc point to transmission feature
+ connections
+
+ Parameters
+ ----------
+ fcr : float
+ Fixed charge rate, used to compute LCOT
+ transmission_costs : str | dict, optional
+ Transmission feature costs to use with TransmissionFeatures
+ handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+ center_tie_in_cost, sink_tie_in_cost, by default None
+ avail_cap_frac : int, optional
+ Fraction of transmissions features capacity 'ac_cap' to make
+ available for connection to supply curve points, by default 1
+ line_limited : bool, optional
+ Flag to have substation connection is limited by maximum capacity
+ of the attached lines, legacy method, by default False
+ connectable : bool, optional
+ Flag to only compute tranmission capital cost if transmission
+ feature has enough available capacity, by default True
+ max_workers : int | NoneType, optional
+ Number of workers to use to compute lcot, if > 1 run in parallel.
+ None uses all available cpu's. by default None
+ consider_friction : bool, optional
+ Flag to consider friction layer on LCOE when "mean_lcoe_friction"
+ is in the sc points input, by default True
+ """
+ tcc_per_mw_col=SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW
+ iftcc_per_mw_colinself._trans_table:
+ cost=self._trans_table[tcc_per_mw_col].values.copy()
+ elif"trans_cap_cost"notinself._trans_table:
+ scc=self._sc_capacity_col
+ cost=self._compute_trans_cap_cost(
+ self._trans_table,
+ trans_costs=transmission_costs,
+ avail_cap_frac=avail_cap_frac,
+ line_limited=line_limited,
+ connectable=connectable,
+ max_workers=max_workers,
+ sc_capacity_col=scc,
+ )
+ self._trans_table[tcc_per_mw_col]=cost# $/MW
+ else:
+ cost=self._trans_table["trans_cap_cost"].values.copy()# $
+ cost/=self._trans_table[SupplyCurveField.CAPACITY_AC_MW]# $/MW
+ self._trans_table[tcc_per_mw_col]=cost
+
+ self._trans_table[tcc_per_mw_col]=(
+ self._trans_table[tcc_per_mw_col].astype("float32")
+ )
+ cost=cost.astype("float32")
+ cf_mean_arr=self._trans_table[SupplyCurveField.MEAN_CF_AC]
+ cf_mean_arr=cf_mean_arr.values.astype("float32")
+ resource_lcoe=self._trans_table[SupplyCurveField.MEAN_LCOE]
+ resource_lcoe=resource_lcoe.values.astype("float32")
+
+ if'reinforcement_cost_floored_per_mw'inself._trans_table:
+ logger.info("'reinforcement_cost_floored_per_mw' column found in "
+ "transmission table. Adding floored reinforcement "
+ "cost LCOE as sorting option.")
+ fr_cost=(self._trans_table['reinforcement_cost_floored_per_mw']
+ .values.copy())
+
+ lcot_fr=((cost+fr_cost)*fcr)/(cf_mean_arr*8760)
+ lcoe_fr=lcot_fr+resource_lcoe
+ self._trans_table['lcot_floored_reinforcement']=lcot_fr
+ self._trans_table['lcoe_floored_reinforcement']=lcoe_fr
+
+ ifSupplyCurveField.REINFORCEMENT_COST_PER_MWinself._trans_table:
+ logger.info("%s column found in transmission table. Adding "
+ "reinforcement costs to total LCOE.",
+ SupplyCurveField.REINFORCEMENT_COST_PER_MW)
+ lcot_nr=(cost*fcr)/(cf_mean_arr*8760)
+ lcoe_nr=lcot_nr+resource_lcoe
+ self._trans_table['lcot_no_reinforcement']=lcot_nr
+ self._trans_table['lcoe_no_reinforcement']=lcoe_nr
+
+ col_name=SupplyCurveField.REINFORCEMENT_COST_PER_MW
+ r_cost=self._trans_table[col_name].astype("float32")
+ r_cost=r_cost.values.copy()
+ self._trans_table[tcc_per_mw_col]+=r_cost
+ cost+=r_cost# $/MW
+
+ lcot=(cost*fcr)/(cf_mean_arr*8760)
+ self._trans_table[SupplyCurveField.LCOT]=lcot
+ self._trans_table[SupplyCurveField.TOTAL_LCOE]=lcot+resource_lcoe
+
+ ifconsider_friction:
+ self._calculate_total_lcoe_friction()
+
+ def_calculate_total_lcoe_friction(self):
+"""Look for site mean LCOE with friction in the trans table and if
+ found make a total LCOE column with friction."""
+
+ ifSupplyCurveField.MEAN_LCOE_FRICTIONinself._trans_table:
+ lcoe_friction=(
+ self._trans_table[SupplyCurveField.LCOT]
+ +self._trans_table[SupplyCurveField.MEAN_LCOE_FRICTION])
+ self._trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION]=(
+ lcoe_friction
+ )
+ logger.info('Found mean LCOE with friction. Adding key '
+ '"total_lcoe_friction" to trans table.')
+
+ def_exclude_noncompetitive_wind_farms(
+ self,comp_wind_dirs,sc_gid,downwind=False
+ ):
+"""
+ Exclude non-competitive wind farms for given sc_gid
+
+ Parameters
+ ----------
+ comp_wind_dirs : CompetitiveWindFarms
+ Pre-initilized CompetitiveWindFarms instance
+ sc_gid : int
+ Supply curve gid to exclude non-competitive wind farms around
+ downwind : bool, optional
+ Flag to remove downwind neighbors as well as upwind neighbors,
+ by default False
+
+ Returns
+ -------
+ comp_wind_dirs : CompetitiveWindFarms
+ updated CompetitiveWindFarms instance
+ """
+ gid=comp_wind_dirs.check_sc_gid(sc_gid)
+ ifgidisnotNone:
+ ifcomp_wind_dirs.mask[gid]:
+ exclude_gids=comp_wind_dirs["upwind",gid]
+ ifdownwind:
+ exclude_gids=np.append(
+ exclude_gids,comp_wind_dirs["downwind",gid]
+ )
+ forninexclude_gids:
+ check=comp_wind_dirs.exclude_sc_point_gid(n)
+ ifcheck:
+ sc_gids=comp_wind_dirs[SupplyCurveField.SC_GID,n]
+ forsc_idinsc_gids:
+ ifself._mask[sc_id]:
+ logger.debug(
+ "Excluding sc_gid {}".format(sc_id)
+ )
+ self._mask[sc_id]=False
+
+ returncomp_wind_dirs
+
+
[docs]@staticmethod
+ defadd_sum_cols(table,sum_cols):
+"""Add a summation column to table.
+
+ Parameters
+ ----------
+ table : pd.DataFrame
+ Supply curve table.
+ sum_cols : dict
+ Mapping of new column label(s) to multiple column labels to sum.
+ Example: sum_col={'total_cap_cost': ['cap_cost1', 'cap_cost2']}
+ Which would add a new 'total_cap_cost' column which would be the
+ sum of 'cap_cost1' and 'cap_cost2' if they are present in table.
+
+ Returns
+ -------
+ table : pd.DataFrame
+ Supply curve table with additional summation columns.
+ """
+
+ fornew_label,sum_labelsinsum_cols.items():
+ missing=[sforsinsum_labelsifsnotintable]
+
+ ifany(missing):
+ logger.info(
+ 'Could not make sum column "{}", missing: {}'.format(
+ new_label,missing
+ )
+ )
+ else:
+ sum_arr=np.zeros(len(table))
+ forsinsum_labels:
+ temp=table[s].values
+ temp[np.isnan(temp)]=0
+ sum_arr+=temp
+
+ table[new_label]=sum_arr
+
+ returntable
+
+ def_full_sort(# noqa: C901
+ self,
+ trans_table,
+ trans_costs=None,
+ avail_cap_frac=1,
+ comp_wind_dirs=None,
+ total_lcoe_fric=None,
+ sort_on=SupplyCurveField.TOTAL_LCOE,
+ columns=(
+ SupplyCurveField.TRANS_GID,
+ SupplyCurveField.TRANS_CAPACITY,
+ SupplyCurveField.TRANS_TYPE,
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
+ SupplyCurveField.DIST_SPUR_KM,
+ SupplyCurveField.LCOT,
+ SupplyCurveField.TOTAL_LCOE,
+ ),
+ downwind=False,
+ ):
+"""
+ Internal method to handle full supply curve sorting
+
+ Parameters
+ ----------
+ trans_table : pandas.DataFrame
+ Supply Curve Tranmission table to sort on
+ trans_costs : str | dict, optional
+ Transmission feature costs to use with TransmissionFeatures
+ handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+ center_tie_in_cost, sink_tie_in_cost, by default None
+ avail_cap_frac : int, optional
+ Fraction of transmissions features capacity 'ac_cap' to make
+ available for connection to supply curve points, by default 1
+ comp_wind_dirs : CompetitiveWindFarms, optional
+ Pre-initilized CompetitiveWindFarms instance, by default None
+ total_lcoe_fric : ndarray, optional
+ Vector of lcoe friction values, by default None
+ sort_on : str, optional
+ Column label to sort the Supply Curve table on. This affects the
+ build priority - connections with the lowest value in this column
+ will be built first, by default 'total_lcoe'
+ columns : tuple, optional
+ Columns to preserve in output connections dataframe,
+ by default ('trans_gid', 'trans_capacity', 'trans_type',
+ 'trans_cap_cost_per_mw', 'dist_km', 'lcot',
+ 'total_lcoe')
+ downwind : bool, optional
+ Flag to remove downwind neighbors as well as upwind neighbors,
+ by default False
+
+ Returns
+ -------
+ supply_curve : pandas.DataFrame
+ Updated sc_points table with transmission connections, LCOT
+ and LCOE+LCOT based on full supply curve connections
+ """
+ trans_features=self._create_handler(
+ self._trans_table,
+ trans_costs=trans_costs,
+ avail_cap_frac=avail_cap_frac,
+ )
+ init_list=[np.nan]*int(1+np.max(self._sc_gids))
+ columns=list(columns)
+ ifsort_onnotincolumns:
+ columns.append(sort_on)
+
+ conn_lists={k:deepcopy(init_list)forkincolumns}
+
+ trans_sc_gids=trans_table[SupplyCurveField.SC_GID].values.astype(int)
+
+ # syntax is final_key: source_key (source from trans_table)
+ all_cols=list(columns)
+ essentials=[SupplyCurveField.TRANS_GID,
+ SupplyCurveField.TRANS_CAPACITY,
+ SupplyCurveField.TRANS_TYPE,
+ SupplyCurveField.DIST_SPUR_KM,
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
+ SupplyCurveField.LCOT,
+ SupplyCurveField.TOTAL_LCOE]
+
+ forcolinessentials:
+ ifcolnotinall_cols:
+ all_cols.append(col)
+
+ arrays={col:trans_table[col].valuesforcolinall_cols}
+
+ sc_capacities=trans_table[self._sc_capacity_col].values
+
+ connected=0
+ progress=0
+ foriinrange(len(trans_table)):
+ sc_gid=trans_sc_gids[i]
+ ifself._mask[sc_gid]:
+ connect=trans_features.connect(
+ arrays[SupplyCurveField.TRANS_GID][i],sc_capacities[i]
+ )
+ ifconnect:
+ connected+=1
+ logger.debug("Connecting sc gid {}".format(sc_gid))
+ self._mask[sc_gid]=False
+
+ forcol_name,data_arrinarrays.items():
+ conn_lists[col_name][sc_gid]=data_arr[i]
+
+ iftotal_lcoe_fricisnotNone:
+ col_name=SupplyCurveField.TOTAL_LCOE_FRICTION
+ conn_lists[col_name][sc_gid]=total_lcoe_fric[i]
+
+ current_prog=connected//(len(self)/100)
+ ifcurrent_prog>progress:
+ progress=current_prog
+ logger.info(
+ "{}% of supply curve points connected".format(
+ progress
+ )
+ )
+
+ ifcomp_wind_dirsisnotNone:
+ comp_wind_dirs=(
+ self._exclude_noncompetitive_wind_farms(
+ comp_wind_dirs,sc_gid,downwind=downwind
+ )
+ )
+
+ index=range(0,int(1+np.max(self._sc_gids)))
+ connections=pd.DataFrame(conn_lists,index=index)
+ connections.index.name=SupplyCurveField.SC_GID
+ connections=connections.dropna(subset=[sort_on])
+ connections=connections[columns].reset_index()
+
+ sc_gids=self._sc_points[SupplyCurveField.SC_GID].values
+ connected=connections[SupplyCurveField.SC_GID].values
+ logger.debug('Connected gids {} out of total supply curve gids {}'
+ .format(len(connected),len(sc_gids)))
+ unconnected=~np.isin(sc_gids,connected)
+ unconnected=sc_gids[unconnected].tolist()
+
+ ifunconnected:
+ msg=(
+ "{} supply curve points were not connected to tranmission! "
+ "Unconnected sc_gid's: {}".format(
+ len(unconnected),unconnected
+ )
+ )
+ logger.warning(msg)
+ warn(msg)
+
+ supply_curve=self._sc_points.merge(
+ connections,on=SupplyCurveField.SC_GID)
+
+ returnsupply_curve.reset_index(drop=True)
+
+ def_check_feature_capacity(self,avail_cap_frac=1):
+"""
+ Add the transmission connection feature capacity to the trans table if
+ needed
+ """
+ ifSupplyCurveField.TRANS_CAPACITYnotinself._trans_table:
+ kwargs={"avail_cap_frac":avail_cap_frac}
+ fc=TF.feature_capacity(self._trans_table,**kwargs)
+ self._trans_table=self._trans_table.merge(
+ fc,on=SupplyCurveField.TRANS_GID)
+
+ def_adjust_output_columns(self,columns,consider_friction):
+"""Add extra output columns, if needed."""
+
+ forcolin_REQUIRED_COMPUTE_AND_OUTPUT_COLS:
+ ifcolnotincolumns:
+ columns.append(col)
+
+ forcolin_REQUIRED_OUTPUT_COLS:
+ ifcolnotinself._trans_table:
+ self._trans_table[col]=np.nan
+ ifcolnotincolumns:
+ columns.append(col)
+
+ missing_cols=[colforcolincolumnsifcolnotinself._trans_table]
+ ifmissing_cols:
+ msg=(f"The following requested columns are not found in "
+ f"transmission table: {missing_cols}.\nSkipping...")
+ logger.warning(msg)
+ warn(msg)
+
+ columns=[colforcolincolumnsifcolinself._trans_table]
+
+ fric_col=SupplyCurveField.TOTAL_LCOE_FRICTION
+ ifconsider_frictionandfric_colinself._trans_table:
+ columns.append(fric_col)
+
+ returnsorted(columns,key=_column_sort_key)
+
+ def_determine_sort_on(self,sort_on):
+"""Determine the `sort_on` column from user input and trans table"""
+ r_cost_col=SupplyCurveField.REINFORCEMENT_COST_PER_MW
+ found_reinforcement_costs=(
+ r_cost_colinself._trans_table
+ andnotself._trans_table[r_cost_col].isna().all()
+ )
+ iffound_reinforcement_costs:
+ sort_on=sort_onor"lcoe_no_reinforcement"
+ returnsort_onorSupplyCurveField.TOTAL_LCOE
+
+
[docs]deffull_sort(
+ self,
+ fcr,
+ transmission_costs=None,
+ avail_cap_frac=1,
+ line_limited=False,
+ connectable=True,
+ max_workers=None,
+ consider_friction=True,
+ sort_on=None,
+ columns=(
+ SupplyCurveField.TRANS_GID,
+ SupplyCurveField.TRANS_CAPACITY,
+ SupplyCurveField.TRANS_TYPE,
+ SupplyCurveField.TOTAL_TRANS_CAP_COST_PER_MW,
+ SupplyCurveField.DIST_SPUR_KM,
+ SupplyCurveField.LCOT,
+ SupplyCurveField.TOTAL_LCOE,
+ ),
+ wind_dirs=None,
+ n_dirs=2,
+ downwind=False,
+ offshore_compete=False,
+ ):
+"""
+ run full supply curve sorting
+
+ Parameters
+ ----------
+ fcr : float
+ Fixed charge rate, used to compute LCOT
+ transmission_costs : str | dict, optional
+ Transmission feature costs to use with TransmissionFeatures
+ handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+ center_tie_in_cost, sink_tie_in_cost, by default None
+ avail_cap_frac : int, optional
+ Fraction of transmissions features capacity 'ac_cap' to make
+ available for connection to supply curve points, by default 1
+ line_limited : bool, optional
+ Flag to have substation connection is limited by maximum capacity
+ of the attached lines, legacy method, by default False
+ connectable : bool, optional
+ Flag to only compute tranmission capital cost if transmission
+ feature has enough available capacity, by default True
+ max_workers : int | NoneType, optional
+ Number of workers to use to compute lcot, if > 1 run in parallel.
+ None uses all available cpu's. by default None
+ consider_friction : bool, optional
+ Flag to consider friction layer on LCOE when "mean_lcoe_friction"
+ is in the sc points input, by default True
+ sort_on : str, optional
+ Column label to sort the Supply Curve table on. This affects the
+ build priority - connections with the lowest value in this column
+ will be built first, by default `None`, which will use
+ total LCOE without any reinforcement costs as the sort value.
+ columns : list | tuple, optional
+ Columns to preserve in output connections dataframe,
+ by default ('trans_gid', 'trans_capacity', 'trans_type',
+ 'trans_cap_cost_per_mw', 'dist_km', 'lcot', 'total_lcoe')
+ wind_dirs : pandas.DataFrame | str, optional
+ path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+ the neighboring supply curve point gids and power-rose value at
+ each cardinal direction, by default None
+ n_dirs : int, optional
+ Number of prominent directions to use, by default 2
+ downwind : bool, optional
+ Flag to remove downwind neighbors as well as upwind neighbors,
+ by default False
+ offshore_compete : bool, default
+ Flag as to whether offshore farms should be included during
+ CompetitiveWindFarms, by default False
+
+ Returns
+ -------
+ supply_curve : pandas.DataFrame
+ Updated sc_points table with transmission connections, LCOT
+ and LCOE+LCOT based on full supply curve connections
+ """
+ logger.info("Starting full competitive supply curve sort.")
+ self._check_substation_conns(self._trans_table)
+ self.compute_total_lcoe(
+ fcr,
+ transmission_costs=transmission_costs,
+ avail_cap_frac=avail_cap_frac,
+ line_limited=line_limited,
+ connectable=connectable,
+ max_workers=max_workers,
+ consider_friction=consider_friction,
+ )
+ self._check_feature_capacity(avail_cap_frac=avail_cap_frac)
+
+ ifisinstance(columns,tuple):
+ columns=list(columns)
+
+ columns=self._adjust_output_columns(columns,consider_friction)
+ sort_on=self._determine_sort_on(sort_on)
+
+ trans_table=self._trans_table.copy()
+ pos=trans_table[SupplyCurveField.LCOT].isnull()
+ trans_table=trans_table.loc[~pos].sort_values(
+ [sort_on,SupplyCurveField.TRANS_GID]
+ )
+
+ total_lcoe_fric=None
+ col_in_table=SupplyCurveField.MEAN_LCOE_FRICTIONintrans_table
+ ifconsider_frictionandcol_in_table:
+ total_lcoe_fric= \
+ trans_table[SupplyCurveField.TOTAL_LCOE_FRICTION].values
+
+ comp_wind_dirs=None
+ ifwind_dirsisnotNone:
+ msg="Excluding {} upwind".format(n_dirs)
+ ifdownwind:
+ msg+=" and downwind"
+
+ msg+=" onshore"
+ ifoffshore_compete:
+ msg+=" and offshore"
+
+ msg+=" windfarms"
+ logger.info(msg)
+ comp_wind_dirs=CompetitiveWindFarms(
+ wind_dirs,
+ self._sc_points,
+ n_dirs=n_dirs,
+ offshore=offshore_compete,
+ )
+
+ supply_curve=self._full_sort(
+ trans_table,
+ trans_costs=transmission_costs,
+ avail_cap_frac=avail_cap_frac,
+ comp_wind_dirs=comp_wind_dirs,
+ total_lcoe_fric=total_lcoe_fric,
+ sort_on=sort_on,
+ columns=columns,
+ downwind=downwind,
+ )
+
+ returnsupply_curve
+
+
[docs]defsimple_sort(
+ self,
+ fcr,
+ transmission_costs=None,
+ avail_cap_frac=1,
+ max_workers=None,
+ consider_friction=True,
+ sort_on=None,
+ columns=DEFAULT_COLUMNS,
+ wind_dirs=None,
+ n_dirs=2,
+ downwind=False,
+ offshore_compete=False,
+ ):
+"""
+ Run simple supply curve sorting that does not take into account
+ available capacity
+
+ Parameters
+ ----------
+ fcr : float
+ Fixed charge rate, used to compute LCOT
+ transmission_costs : str | dict, optional
+ Transmission feature costs to use with TransmissionFeatures
+ handler: line_tie_in_cost, line_cost, station_tie_in_cost,
+ center_tie_in_cost, sink_tie_in_cost, by default None
+ avail_cap_frac : int, optional
+ Fraction of transmissions features capacity 'ac_cap' to make
+ available for connection to supply curve points, by default 1
+ line_limited : bool, optional
+ Flag to have substation connection is limited by maximum capacity
+ of the attached lines, legacy method, by default False
+ connectable : bool, optional
+ Flag to only compute tranmission capital cost if transmission
+ feature has enough available capacity, by default True
+ max_workers : int | NoneType, optional
+ Number of workers to use to compute lcot, if > 1 run in parallel.
+ None uses all available cpu's. by default None
+ consider_friction : bool, optional
+ Flag to consider friction layer on LCOE when "mean_lcoe_friction"
+ is in the sc points input, by default True
+ sort_on : str, optional
+ Column label to sort the Supply Curve table on. This affects the
+ build priority - connections with the lowest value in this column
+ will be built first, by default `None`, which will use
+ total LCOE without any reinforcement costs as the sort value.
+ columns : list | tuple, optional
+ Columns to preserve in output connections dataframe.
+ By default, :obj:`DEFAULT_COLUMNS`.
+ wind_dirs : pandas.DataFrame | str, optional
+ path to .csv or reVX.wind_dirs.wind_dirs.WindDirs output with
+ the neighboring supply curve point gids and power-rose value at
+ each cardinal direction, by default None
+ n_dirs : int, optional
+ Number of prominent directions to use, by default 2
+ downwind : bool, optional
+ Flag to remove downwind neighbors as well as upwind neighbors
+ offshore_compete : bool, default
+ Flag as to whether offshore farms should be included during
+ CompetitiveWindFarms, by default False
+
+ Returns
+ -------
+ supply_curve : pandas.DataFrame
+ Updated sc_points table with transmission connections, LCOT
+ and LCOE+LCOT based on simple supply curve connections
+ """
+ logger.info("Starting simple supply curve sort (no capacity limits).")
+ self.compute_total_lcoe(
+ fcr,
+ transmission_costs=transmission_costs,
+ avail_cap_frac=avail_cap_frac,
+ connectable=False,
+ max_workers=max_workers,
+ consider_friction=consider_friction,
+ )
+ sort_on=self._determine_sort_on(sort_on)
+
+ ifisinstance(columns,tuple):
+ columns=list(columns)
+ columns=self._adjust_output_columns(columns,consider_friction)
+
+ trans_table=self._trans_table.copy()
+ connections=trans_table.sort_values(
+ [sort_on,SupplyCurveField.TRANS_GID])
+ connections=connections.groupby(SupplyCurveField.SC_GID).first()
+ connections=connections[columns].reset_index()
+
+ supply_curve=self._sc_points.merge(connections,
+ on=SupplyCurveField.SC_GID)
+ ifwind_dirsisnotNone:
+ supply_curve=CompetitiveWindFarms.run(
+ wind_dirs,
+ supply_curve,
+ n_dirs=n_dirs,
+ offshore=offshore_compete,
+ sort_on=sort_on,
+ downwind=downwind,
+ )
+
+ supply_curve=supply_curve.reset_index(drop=True)
+
+ returnsupply_curve
+
+
[docs]defrun(
+ self,
+ out_fpath,
+ fixed_charge_rate,
+ simple=True,
+ avail_cap_frac=1,
+ line_limited=False,
+ transmission_costs=None,
+ consider_friction=True,
+ sort_on=None,
+ columns=DEFAULT_COLUMNS,
+ max_workers=None,
+ competition=None,
+ ):
+"""Run Supply Curve Transmission calculations.
+
+ Run full supply curve taking into account available capacity of
+ tranmission features when making connections.
+
+ Parameters
+ ----------
+ out_fpath : str
+ Full path to output CSV file. Does not need to include file
+ ending - it will be added automatically if missing.
+ fixed_charge_rate : float
+ Fixed charge rate, (in decimal form: 5% = 0.05). This value
+ is used to compute LCOT.
+ simple : bool, optional
+ Option to run the simple sort (does not keep track of
+ capacity available on the existing transmission grid). If
+ ``False``, a full transmission sort (where connections are
+ limited based on available transmission capacity) is run.
+ Note that the full transmission sort requires the
+ `avail_cap_frac` and `line_limited` inputs.
+ By default, ``True``.
+ avail_cap_frac : int, optional
+ This input has no effect if ``simple=True``. Fraction of
+ transmissions features capacity ``ac_cap`` to make available
+ for connection to supply curve points. By default, ``1``.
+ line_limited : bool, optional
+ This input has no effect if ``simple=True``. Flag to have
+ substation connection limited by maximum capacity
+ of the attached lines. This is a legacy method.
+ By default, ``False``.
+ transmission_costs : str | dict, optional
+ Dictionary of transmission feature costs or path to JSON
+ file containing a dictionary of transmission feature costs.
+ These costs are used to compute transmission capital cost
+ if the input transmission tables do not have a
+ ``"trans_cap_cost"`` column (this input is ignored
+ otherwise). The dictionary must include:
+
+ - line_tie_in_cost
+ - line_cost
+ - station_tie_in_cost
+ - center_tie_in_cost
+ - sink_tie_in_cost
+
+ By default, ``None``.
+ consider_friction : bool, optional
+ Flag to add a new ``"total_lcoe_friction"`` column to the
+ supply curve output that contains the sum of the computed
+ ``"total_lcoe"`` value and the input
+ ``"mean_lcoe_friction"`` values. If ``"mean_lcoe_friction"``
+ is not in the `sc_points` input, this option is ignored.
+ By default, ``True``.
+ sort_on : str, optional
+ Column label to sort the supply curve table on. This affects
+ the build priority when doing a "full" sort - connections
+ with the lowest value in this column will be built first.
+ For a "simple" sort, only connections with the lowest value
+ in this column will be considered. If ``None``, the sort is
+ performed on the total LCOE *without* any reinforcement
+ costs added (this is typically what you want - it avoids
+ unrealistically long spur-line connections).
+ By default ``None``.
+ columns : list | tuple, optional
+ Columns to preserve in output supply curve dataframe.
+ By default, :obj:`DEFAULT_COLUMNS`.
+ max_workers : int, optional
+ Number of workers to use to compute LCOT. If > 1,
+ computation is run in parallel. If ``None``, computation
+ uses all available CPU's. By default, ``None``.
+ competition : dict, optional
+ Optional dictionary of arguments for competitive wind farm
+ exclusions, which removes supply curve points upwind (and
+ optionally downwind) of the lowest LCOE supply curves.
+ If ``None``, no competition is applied. Otherwise, this
+ dictionary can have up to four keys:
+
+ - ``wind_dirs`` (required) : A path to a CSV file or
+ :py:class:`reVX ProminentWindDirections
+ <reVX.wind_dirs.prominent_wind_dirs.ProminentWindDirections>`
+ output with the neighboring supply curve point gids
+ and power-rose values at each cardinal direction.
+ - ``n_dirs`` (optional) : An integer representing the
+ number of prominent directions to use during wind farm
+ competition. By default, ``2``.
+ - ``downwind`` (optional) : A flag indicating that
+ downwind neighbors should be removed in addition to
+ upwind neighbors during wind farm competition.
+ By default, ``False``.
+ - ``offshore_compete`` (optional) : A flag indicating
+ that offshore farms should be included during wind
+ farm competition. By default, ``False``.
+
+ By default ``None``.
+
+ Returns
+ -------
+ str
+ Path to output supply curve.
+ """
+ kwargs={
+ "fcr":fixed_charge_rate,
+ "transmission_costs":transmission_costs,
+ "consider_friction":consider_friction,
+ "sort_on":sort_on,
+ "columns":columns,
+ "max_workers":max_workers,
+ }
+ kwargs.update(competitionor{})
+
+ ifsimple:
+ supply_curve=self.simple_sort(**kwargs)
+ else:
+ kwargs["avail_cap_frac"]=avail_cap_frac
+ kwargs["line_limited"]=line_limited
+ supply_curve=self.full_sort(**kwargs)
+
+ out_fpath=_format_sc_out_fpath(out_fpath)
+ supply_curve.to_csv(out_fpath,index=False)
+
+ returnout_fpath
+
+
+def_format_sc_out_fpath(out_fpath):
+"""Add CSV file ending and replace underscore, if necessary."""
+ ifnotout_fpath.endswith(".csv"):
+ out_fpath="{}.csv".format(out_fpath)
+
+ project_dir,out_fn=os.path.split(out_fpath)
+ out_fn=out_fn.replace("supply_curve","supply-curve")
+ returnos.path.join(project_dir,out_fn)
+
+
+def_column_sort_key(col):
+"""Determine the sort order of the input column. """
+ col_value=_REQUIRED_COMPUTE_AND_OUTPUT_COLS.get(col)
+ ifcol_valueisNone:
+ col_value=_REQUIRED_OUTPUT_COLS.get(col)
+ ifcol_valueisNone:
+ col_value=1e6
+
+ returncol_value,str(col)
+
+# -*- coding: utf-8 -*-
+"""reV tech mapping framework.
+
+This module manages the exclusions-to-resource mapping.
+The core of this module is a parallel cKDTree.
+
+Created on Fri Jun 21 16:05:47 2019
+
+@author: gbuster
+"""
+importlogging
+importos
+fromconcurrent.futuresimportas_completed
+frommathimportceil
+fromwarningsimportwarn
+
+importh5py
+importnumpyasnp
+fromrex.resourceimportResource
+fromrex.utilities.executionimportSpawnProcessPool
+fromrex.utilities.utilitiesimportres_dist_threshold
+fromscipy.spatialimportcKDTree
+
+fromreV.supply_curve.extentimportSupplyCurveExtent,LATITUDE,LONGITUDE
+fromreV.utilities.exceptionsimportFileInputError,FileInputWarning
+
+logger=logging.getLogger(__name__)
+
+
+
[docs]classTechMapping:
+"""Framework to create map between tech layer (exclusions), res, and gen"""
+
+ def__init__(
+ self,excl_fpath,res_fpath,sc_resolution=2560,dist_margin=1.05
+ ):
+"""
+ Parameters
+ ----------
+ excl_fpath : str
+ Filepath to exclusions h5 file, must contain latitude and longitude
+ arrays to allow for mapping to resource points
+ res_fpath : str
+ Filepath to .h5 resource file that we're mapping to.
+ sc_resolution : int | None, optional
+ Supply curve resolution, does not affect the exclusion to resource
+ (tech) mapping, but defines how many exclusion pixels are mapped
+ at a time, by default 2560
+ dist_margin : float, optional
+ Extra margin to multiply times the computed distance between
+ neighboring resource points, by default 1.05
+ """
+ self._excl_fpath=excl_fpath
+ self._check_fout()
+
+ self._tree,self._dist_thresh=self._build_tree(
+ res_fpath,dist_margin=dist_margin
+ )
+
+ withSupplyCurveExtent(
+ self._excl_fpath,resolution=sc_resolution
+ )assc:
+ self._sc_resolution=sc.resolution
+ self._gids=np.array(list(range(len(sc))),dtype=np.uint32)
+ self._excl_shape=sc.exclusions.shape
+ self._n_excl=np.product(self._excl_shape)
+ self._sc_row_indices=sc.row_indices
+ self._sc_col_indices=sc.col_indices
+ self._excl_row_slices=sc.excl_row_slices
+ self._excl_col_slices=sc.excl_col_slices
+ logger.info(
+ "Initialized TechMapping object with {} calc chunks "
+ "for {} tech exclusion points".format(
+ len(self._gids),self._n_excl
+ )
+ )
+
+ @property
+ defdistance_threshold(self):
+"""Get the upper bound on NN distance between excl and res points.
+
+ Returns
+ -------
+ float
+ Estimate the distance between resource points. Calculated as half
+ of the diagonal between closest resource points, with desired
+ extra margin
+ """
+ returnself._dist_thresh
+
+ @staticmethod
+ def_build_tree(res_fpath,dist_margin=1.05):
+"""
+ Build cKDTree from resource lat, lon coordinates. Compute minimum
+ intra point distance between resource gids with provided extra margin.
+
+ Parameters
+ ----------
+ res_fpath : str
+ Filepath to .h5 resource file that we're mapping to.
+ dist_margin : float, optional
+ Extra margin to multiply times the computed distance between
+ neighboring resource points, by default 1.05
+
+ Returns
+ -------
+ tree : cKDTree
+ cKDTree built from resource lat, lon coordinates
+ dist_tresh : float
+ Estimate the distance between resource points. Calculated as half
+ of the diagonal between closest resource points, with desired
+ extra margin
+ """
+ withResource(res_fpath)asf:
+ lat_lons=f.lat_lon
+
+ # pylint: disable=not-callable
+ tree=cKDTree(lat_lons)
+
+ dist_thresh=res_dist_threshold(
+ lat_lons,tree=tree,margin=dist_margin
+ )
+
+ returntree,dist_thresh
+
+ @staticmethod
+ def_make_excl_iarr(shape):
+"""
+ Create 2D array of 1D index values for the flattened h5 excl extent
+
+ Parameters
+ ----------
+ shape : tuple
+ exclusion extent shape
+
+ Returns
+ -------
+ iarr : ndarray
+ 2D array of 1D index values for the flattened h5 excl extent
+ """
+ iarr=np.arange(np.product(shape),dtype=np.uint32)
+
+ returniarr.reshape(shape)
+
+ @staticmethod
+ def_get_excl_slices(
+ gid,sc_row_indices,sc_col_indices,excl_row_slices,excl_col_slices
+ ):
+"""
+ Get the row and column slices of the exclusions grid corresponding
+ to the supply curve point gid.
+
+ Parameters
+ ----------
+ gid : int
+ Supply curve point gid.
+ sc_row_indices : list
+ List of row indices in exclusion array for for every sc_point gid
+ sc_col_indices : list
+ List of column indices in exclusion array for for every sc_point
+ gid
+ excl_row_slices : list
+ List representing the supply curve points rows. Each list entry
+ contains the exclusion row slice that are included in the sc
+ point.
+ excl_col_slices : list
+ List representing the supply curve points columns. Each list entry
+ contains the exclusion columns slice that are included in the sc
+ point.
+
+ Returns
+ -------
+ row_slice : int
+ Exclusions grid row index slice corresponding to the sc point gid.
+ col_slice : int
+ Exclusions grid col index slice corresponding to the sc point gid.
+ """
+
+ row_slice=excl_row_slices[sc_row_indices[gid]]
+ col_slice=excl_col_slices[sc_col_indices[gid]]
+
+ returnrow_slice,col_slice
+
+ @classmethod
+ def_get_excl_coords(cls,excl_fpath,gids,sc_row_indices,sc_col_indices,
+ excl_row_slices,excl_col_slices,
+ coord_labels=(LATITUDE,LONGITUDE)):
+"""
+ Extract the exclusion coordinates for the desired gids for TechMapping.
+
+ Parameters
+ ----------
+ gids : np.ndarray
+ Supply curve gids with tech exclusion points to map to the
+ resource meta points.
+ excl_fpath : str
+ Filepath to exclusions h5 file, must contain latitude and longitude
+ arrays to allow for mapping to resource points
+ sc_row_indices : list
+ List of row indices in exclusion array for for every sc_point gid
+ sc_col_indices : list
+ List of column indices in exclusion array for for every sc_point
+ gid
+ excl_row_slices : list
+ List representing the supply curve points rows. Each list entry
+ contains the exclusion row slice that are included in the sc
+ point.
+ excl_col_slices : list
+ List representing the supply curve points columns. Each list entry
+ contains the exclusion columns slice that are included in the sc
+ point.
+ coord_labels : tuple
+ Labels for the coordinate datasets.
+
+ Returns
+ -------
+ coords_out : list
+ List of arrays of the un-projected latitude, longitude array of
+ tech exclusion points. List entries correspond to input gids.
+ """
+ coords_out=[]
+ withh5py.File(excl_fpath,"r")asf:
+ forgidingids:
+ row_slice,col_slice=cls._get_excl_slices(
+ gid,
+ sc_row_indices,
+ sc_col_indices,
+ excl_row_slices,
+ excl_col_slices,
+ )
+ try:
+ lats=f[coord_labels[0]][row_slice,col_slice]
+ lons=f[coord_labels[1]][row_slice,col_slice]
+ emeta=np.vstack((lats.flatten(),lons.flatten())).T
+ exceptExceptionase:
+ m=(
+ "Could not unpack coordinates for gid {} with "
+ "row/col slice {}/{}. Received the following "
+ "error:\n{}".format(gid,row_slice,col_slice,e)
+ )
+ logger.error(m)
+ raisee
+
+ coords_out.append(emeta)
+
+ returncoords_out
+
+
[docs]@classmethod
+ defmap_resource_gids(
+ cls,
+ gids,
+ excl_fpath,
+ sc_row_indices,
+ sc_col_indices,
+ excl_row_slices,
+ excl_col_slices,
+ tree,
+ dist_thresh,
+ ):
+"""Map exclusion gids to the resource meta.
+
+ Parameters
+ ----------
+ gids : np.ndarray
+ Supply curve gids with tech exclusion points to map to the
+ resource meta points.
+ excl_fpath : str
+ Filepath to exclusions h5 file, must contain latitude and longitude
+ arrays to allow for mapping to resource points
+ sc_row_indices : list
+ List of row indices in exclusion array for for every sc_point gid
+ sc_col_indices : list
+ List of column indices in exclusion array for for every sc_point
+ gid
+ excl_row_slices : list
+ List representing the supply curve points rows. Each list entry
+ contains the exclusion row slice that are included in the sc
+ point.
+ excl_col_slices : list
+ List representing the supply curve points columns. Each list entry
+ contains the exclusion columns slice that are included in the sc
+ point.
+ tree : cKDTree
+ cKDTree built from resource lat, lon coordinates
+ dist_tresh : float
+ Estimate the distance between resource points. Calculated as half
+ of the diagonal between closest resource points, with an extra
+ 5% margin
+
+ Returns
+ -------
+ ind : list
+ List of arrays of index values from the NN. List entries correspond
+ to input gids.
+ """
+ logger.debug(
+ "Getting tech map coordinates for chunks {} through {}".format(
+ gids[0],gids[-1]
+ )
+ )
+ ind_out=[]
+ coords_out=cls._get_excl_coords(
+ excl_fpath,
+ gids,
+ sc_row_indices,
+ sc_col_indices,
+ excl_row_slices,
+ excl_col_slices,
+ )
+
+ logger.debug(
+ "Running tech mapping for chunks {} through {}".format(
+ gids[0],gids[-1]
+ )
+ )
+ fori,_inenumerate(gids):
+ dist,ind=tree.query(coords_out[i])
+ ind[(dist>=dist_thresh)]=-1
+ ind_out.append(ind)
+
+ returnind_out
+
+
[docs]@staticmethod
+ defsave_tech_map(
+ excl_fpath,
+ dset,
+ indices,
+ distance_threshold=None,
+ res_fpath=None,
+ chunks=(128,128),
+ ):
+"""Save tech mapping indices and coordinates to an h5 output file.
+
+ Parameters
+ ----------
+ excl_fpath : str
+ Filepath to exclusions h5 file to add techmap to as 'dset'
+ dset : str
+ Dataset name in fpath_out to save mapping results to.
+ indices : np.ndarray
+ Index values of the NN resource point. -1 if no res point found.
+ 2D integer array with shape equal to the exclusions extent shape.
+ distance_threshold : float
+ Distance upper bound to save as attr.
+ res_fpath : str, optional
+ Filepath to .h5 resource file that we're mapping to,
+ by default None
+ chunks : tuple
+ Chunk shape of the 2D output datasets.
+ """
+ logger.info('Writing tech map "{}" to {}'.format(dset,excl_fpath))
+
+ shape=indices.shape
+ chunks=(np.min((shape[0],chunks[0])),np.min((shape[1],chunks[1])))
+
+ withh5py.File(excl_fpath,"a")asf:
+ ifdsetinlist(f):
+ wmsg=(
+ 'TechMap results dataset "{}" is being replaced '
+ 'in pre-existing Exclusions TechMapping file "{}"'.format(
+ dset,excl_fpath
+ )
+ )
+ logger.warning(wmsg)
+ warn(wmsg,FileInputWarning)
+ f[dset][...]=indices
+ else:
+ f.create_dataset(
+ dset,
+ shape=shape,
+ dtype=indices.dtype,
+ data=indices,
+ chunks=chunks,
+ )
+
+ ifdistance_threshold:
+ f[dset].attrs["distance_threshold"]=distance_threshold
+
+ ifres_fpath:
+ f[dset].attrs["src_res_fpath"]=res_fpath
+
+ logger.info(
+ 'Successfully saved tech map "{}" to {}'.format(dset,excl_fpath)
+ )
[docs]defmap_resource(self,max_workers=None,points_per_worker=10):
+"""
+ Map all resource gids to exclusion gids
+
+ Parameters
+ ----------
+ max_workers : int, optional
+ Number of cores to run mapping on. None uses all available cpus,
+ by default None
+ points_per_worker : int, optional
+ Number of supply curve points to map to resource gids on each
+ worker, by default 10
+
+ Returns
+ -------
+ indices : np.ndarray
+ Index values of the NN resource point. -1 if no res point found.
+ 2D integer array with shape equal to the exclusions extent shape.
+ """
+ gid_chunks=ceil(len(self._gids)/points_per_worker)
+ gid_chunks=np.array_split(self._gids,gid_chunks)
+
+ # init full output arrays
+ indices=-1*np.ones((self._n_excl,),dtype=np.int32)
+ iarr=self._make_excl_iarr(self._excl_shape)
+
+ futures={}
+ loggers=[__name__,"reV"]
+ withSpawnProcessPool(max_workers=max_workers,loggers=loggers)asexe:
+ # iterate through split executions, submitting each to worker
+ fori,gid_setinenumerate(gid_chunks):
+ # submit executions and append to futures list
+ futures[
+ exe.submit(
+ self.map_resource_gids,
+ gid_set,
+ self._excl_fpath,
+ self._sc_row_indices,
+ self._sc_col_indices,
+ self._excl_row_slices,
+ self._excl_col_slices,
+ self._tree,
+ self.distance_threshold,
+ )
+ ]=i
+
+ n_finished=0
+ forfutureinas_completed(futures):
+ n_finished+=1
+ logger.info(
+ "Parallel TechMapping futures collected: "
+ "{} out of {}".format(n_finished,len(futures))
+ )
+
+ i=futures[future]
+ result=future.result()
+
+ forj,gidinenumerate(gid_chunks[i]):
+ row_slice,col_slice=self._get_excl_slices(
+ gid,
+ self._sc_row_indices,
+ self._sc_col_indices,
+ self._excl_row_slices,
+ self._excl_col_slices,
+ )
+ ind_slice=iarr[row_slice,col_slice].flatten()
+ indices[ind_slice]=result[j]
+
+ indices=indices.reshape(self._excl_shape)
+
+ returnindices
+
+
[docs]@classmethod
+ defrun(
+ cls,
+ excl_fpath,
+ res_fpath,
+ dset=None,
+ sc_resolution=2560,
+ dist_margin=1.05,
+ max_workers=None,
+ points_per_worker=10,
+ ):
+"""Run parallel mapping and save to h5 file.
+
+ Parameters
+ ----------
+ excl_fpath : str
+ Filepath to exclusions h5 (tech layer). dset will be
+ created in excl_fpath.
+ res_fpath : str
+ Filepath to .h5 resource file that we're mapping to.
+ dset : str, optional
+ Dataset name in excl_fpath to save mapping results to, if None
+ do not save tech_map to excl_fpath, by default None
+ sc_resolution : int | None, optional
+ Supply curve resolution, does not affect the exclusion to resource
+ (tech) mapping, but defines how many exclusion pixels are mapped
+ at a time, by default 2560
+ dist_margin : float, optional
+ Extra margin to multiply times the computed distance between
+ neighboring resource points, by default 1.05
+ max_workers : int, optional
+ Number of cores to run mapping on. None uses all available cpus,
+ by default None
+ points_per_worker : int, optional
+ Number of supply curve points to map to resource gids on each
+ worker, by default 10
+
+ Returns
+ -------
+ indices : np.ndarray
+ Index values of the NN resource point. -1 if no res point found.
+ 2D integer array with shape equal to the exclusions extent shape.
+ """
+ kwargs={"dist_margin":dist_margin,"sc_resolution":sc_resolution}
+ mapper=cls(excl_fpath,res_fpath,**kwargs)
+ indices=mapper.map_resource(
+ max_workers=max_workers,points_per_worker=points_per_worker
+ )
+
+ ifdset:
+ mapper.save_tech_map(
+ excl_fpath,
+ dset,
+ indices,
+ distance_threshold=mapper.distance_threshold,
+ res_fpath=res_fpath,
+ )
+
+ returnindices
[docs]classFieldEnum(str,Enum):
+"""Base Field enum with some mapping methods."""
+
+
[docs]@classmethod
+ defmap_to(cls,other):
+"""Return a rename map from this enum to another.
+
+ Mapping is performed on matching enum names. In other words, if
+ both enums have a `ONE` attribute, this will be mapped from one
+ enum to another.
+
+ Parameters
+ ----------
+ other : :class:`Enum`
+ ``Enum`` subclass with ``__members__`` attribute.
+
+ Returns
+ -------
+ dict
+ Dictionary mapping matching values from one enum to another.
+
+ Examples
+ --------
+ >>> class Test1(FieldEnum):
+ >>> ONE = "one_x"
+ >>> TWO = "two"
+ >>>
+ >>> class Test2(Enum):
+ >>> ONE = "one_y"
+ >>> THREE = "three"
+ >>>
+ >>> Test1.map_to(Test2)
+ {<Test1.ONE: 'one_x'>: <Test2.ONE: 'one_y'>}
+ """
+ return{
+ cls[mem]:other[mem]
+ formemincls.__members__
+ ifmeminother.__members__
+ }
+
+
[docs]@classmethod
+ defmap_from(cls,other):
+"""Map from a dictionary of name / member pairs to this enum.
+
+ Parameters
+ ----------
+ other : dict
+ Dictionary mapping key values (typically old aliases) to
+ enum values. For example, ``{'sc_gid': 'SC_GID'}`` would
+ return a dictionary that maps ``'sc_gid'`` to the ``SC_GID``
+ member of this enum.
+
+ Returns
+ -------
+ dict
+ Mapping of input dictionary keys to member values of this
+ enum.
+
+ Examples
+ --------
+ >>> class Test(FieldEnum):
+ >>> ONE = "one_x"
+ >>> TWO = "two_y"
+ >>>
+ >>> Test.map_from({1: "ONE", 2: "TWO"})
+ {1: <Test.ONE: 'one_x'>, 2: <Test.TWO: 'two_y'>}
+ """
+ return{name:cls[mem]forname,meminother.items()}
[docs]classSiteDataField(FieldEnum):
+"""An enumerated map to site data column names."""
+
+ GID="gid"
+ CONFIG="config"
+
+
+
[docs]classResourceMetaField(FieldEnum):
+"""An enumerated map to resource meta column names.
+
+ Each output name should match the name of a key the resource file
+ meta table.
+ """
+
+ GID="gid"
+ LATITUDE="latitude"
+ LONGITUDE="longitude"
+ ELEVATION="elevation"
+ TIMEZONE="timezone"
+ COUNTY="county"
+ STATE="state"
+ COUNTRY="country"
+ OFFSHORE="offshore"
[docs]@classmethod
+ defmap_from_legacy(cls):
+"""Map of legacy names to current values.
+
+ Returns
+ -------
+ dict
+ Dictionary that maps legacy supply curve column names to
+ members of this enum.
+ """
+ legacy_map={}
+ forcurrent_field,old_fieldincls.map_to(_LegacySCAliases).items():
+ aliases=old_field.value
+ ifisinstance(aliases,str):
+ aliases=[aliases]
+ legacy_map.update({alias:current_fieldforaliasinaliases})
+
+ returnlegacy_map
+
+
+class_LegacySCAliases(Enum):
+"""Legacy supply curve column names.
+
+ Enum values can be either a single string or an iterable of string
+ values where each string value represents a previously known alias.
+ """
+
+ ELEVATION="elevation"
+ MEAN_RES="mean_res"
+ MEAN_CF_AC="mean_cf"
+ MEAN_LCOE="mean_lcoe"
+ CAPACITY_AC_MW="capacity"
+ AREA_SQ_KM="area_sq_km"
+ MEAN_FRICTION="mean_friction"
+ MEAN_LCOE_FRICTION="mean_lcoe_friction"
+ RAW_LCOE="raw_lcoe"
+ TRANS_TYPE="category"
+ TRANS_CAPACITY="avail_cap"
+ DIST_SPUR_KM="dist_km"
+ REINFORCEMENT_DIST_KM="reinforcement_dist_km"
+ TIE_LINE_COST_PER_MW="tie_line_cost_per_mw"
+ CONNECTION_COST_PER_MW="connection_cost_per_mw"
+ REINFORCEMENT_COST_PER_MW="reinforcement_cost_per_mw"
+ TOTAL_TRANS_CAP_COST_PER_MW="trans_cap_cost_per_mw"
+ LCOT="lcot"
+ TOTAL_LCOE="total_lcoe"
+ TOTAL_LCOE_FRICTION="total_lcoe_friction"
+ N_PARALLEL_TRANS="n_parallel_trans"
+ EOS_MULT="eos_mult","capital_cost_multiplier"
+ REG_MULT="reg_mult"
+ SC_POINT_ANNUAL_ENERGY_MW="sc_point_annual_energy"
+ POI_LAT="poi_lat"
+ POI_LON="poi_lon"
+ REINFORCEMENT_POI_LAT="reinforcement_poi_lat"
+ REINFORCEMENT_POI_LON="reinforcement_poi_lon"
+ BESPOKE_AEP="bespoke_aep"
+ BESPOKE_OBJECTIVE="bespoke_objective"
+ BESPOKE_CAPITAL_COST="bespoke_capital_cost"
+ BESPOKE_FIXED_OPERATING_COST="bespoke_fixed_operating_cost"
+ BESPOKE_VARIABLE_OPERATING_COST="bespoke_variable_operating_cost"
+ BESPOKE_BALANCE_OF_SYSTEM_COST="bespoke_balance_of_system_cost"
+ INCLUDED_AREA="included_area"
+ INCLUDED_AREA_CAPACITY_DENSITY="included_area_capacity_density"
+ CONVEX_HULL_AREA="convex_hull_area"
+ CONVEX_HULL_CAPACITY_DENSITY="convex_hull_capacity_density"
+ FULL_CELL_CAPACITY_DENSITY="full_cell_capacity_density"
+
+
+
[docs]classModuleName(str,Enum):
+"""A collection of the module names available in reV.
+
+ Each module name should match the name of the click command
+ that will be used to invoke its respective cli. As of 3/1/2022,
+ this means that all commands are lowercase with underscores
+ replaced by dashes.
+
+ Reference
+ ---------
+ See this line in the click source code to get the most up-to-date
+ click name conversions: https://tinyurl.com/4rehbsvf
+ """
+
+ BESPOKE="bespoke"
+ COLLECT="collect"
+ ECON="econ"
+ GENERATION="generation"
+ HYBRIDS="hybrids"
+ MULTI_YEAR="multi-year"
+ NRWAL="nrwal"
+ QA_QC="qa-qc"
+ REP_PROFILES="rep-profiles"
+ SUPPLY_CURVE="supply-curve"
+ SUPPLY_CURVE_AGGREGATION="supply-curve-aggregation"
+
+ def__str__(self):
+ returnself.value
+
+ def__format__(self,format_spec):
+ returnstr.__format__(self.value,format_spec)
+
+
[docs]@classmethod
+ defall_names(cls):
+"""All module names.
+
+ Returns
+ -------
+ set
+ The set of all module name strings.
+ """
+ # pylint: disable=no-member
+ return{v.valueforvincls.__members__.values()}
+
+
+
[docs]deflog_versions(logger):
+"""Log package versions:
+ - rex and reV to info
+ - h5py, numpy, pandas, scipy, and PySAM to debug
+
+ Parameters
+ ----------
+ logger : logging.Logger
+ Logger object to log memory message to.
+ """
+ logger.info("Running with reV version {}".format(__version__))
+ rex_log_versions(logger)
+ logger.debug("- PySAM version {}".format(PySAM.__version__))
[docs]definit_cli_logging(name,log_directory,verbose):
+"""Initialize CLI logger
+
+ Parameters
+ ----------
+ name : str
+ The name to use for the log file written to disk.
+ log_directory : str
+ Path to log file output directory.
+ verbose : bool
+ Option to make logger verbose (DEBUG).
+ """
+ init_mult(name,log_directory,modules=['reV','rex'],verbose=verbose)
+ logger.info("Initialized reV/rex {}loggers with name {!r} and log "
+ "directory {!r}"
+ .format("verbose "ifverboseelse"",name,
+ str(log_directory)))
+
+
+
[docs]defformat_analysis_years(analysis_years=None):
+"""Format user's analysis_years input
+
+ Parameters
+ ----------
+ analysis_years : int | str | list, optional
+ Years to run reV analysis on. Can be an integer or string, or a
+ list of integers or strings (or ``None``). This input will get
+ converted to a list of values automatically. If ``None``, a
+ ``ConfigWarning`` will be thrown. By default, ``None``.
+
+ Returns
+ -------
+ list
+ List of analysis years. This list will never be empty, but it
+ can contain ``None`` as the only value.
+ """
+
+ ifnotisinstance(analysis_years,list):
+ analysis_years=[analysis_years]
+
+ ifanalysis_years[0]isNone:
+ warn('Years may not have been specified, may default '
+ 'to available years in inputs files.',ConfigWarning)
+
+ returnanalysis_years
+
+
+
[docs]defparse_from_pipeline(config,out_dir,config_key,target_modules):
+"""Parse the out file from target modules and set as the values for key.
+
+ This function only updates the ``config_key`` input if it is set to
+ ``"PIPELINE"``.
+
+ Parameters
+ ----------
+ config : dict
+ Configuration dictionary. The ``config_key`` will be updated in
+ this dictionary if it is set to ``"PIPELINE"``.
+ out_dir : str
+ Path to pipeline project directory where config and status files
+ are located. The status file is expected to be in this
+ directory.
+ config_key : str
+ Key in config files to replace with ``"out_file"`` value(s) from
+ previous pipeline step.
+ target_modules : list of str | list of `ModuleName`
+ List of (previous) target modules to parse for the
+ ``config_key``.
+
+ Returns
+ -------
+ dict
+ Input config dictionary with updated ``config_key`` input.
+
+ Raises
+ ------
+ PipelineError
+ If ``"out_file"`` not found in previous target module status
+ files.
+ """
+ ifconfig.get(config_key,None)=='PIPELINE':
+ fortarget_moduleintarget_modules:
+ gen_config_key="gen"inconfig_key
+ module_sca=target_module==ModuleName.SUPPLY_CURVE_AGGREGATION
+ ifgen_config_keyandmodule_sca:
+ target_key="gen_fpath"
+ else:
+ target_key="out_file"
+ val=Status.parse_step_status(out_dir,target_module,target_key)
+ iflen(val)==1:
+ break
+ else:
+ raisePipelineError('Could not parse {} from previous '
+ 'pipeline jobs.'.format(config_key))
+
+ config[config_key]=val[0]
+ logger.info('Config using the following pipeline input for {}: {}'
+ .format(config_key,val[0]))
+
+ returnconfig
[docs]defcurtail(resource,curtailment,random_seed=0):
+"""Curtail the SAM wind resource object based on project points.
+
+ Parameters
+ ----------
+ resource : rex.sam_resource.SAMResource
+ SAM resource object for WIND resource.
+ curtailment : reV.config.curtailment.Curtailment
+ Curtailment config object.
+ random_seed : int | NoneType
+ Number to seed the numpy random number generator. Used to generate
+ reproducable psuedo-random results if the probability of curtailment
+ is not set to 1. Numpy random will be seeded with the system time if
+ this is None.
+
+ Returns
+ -------
+ resource : reV.handlers.sam_resource.SAMResource
+ Same as the input argument but with the wind speed dataset set to zero
+ where curtailment is in effect.
+ """
+
+ shape=resource.shape
+
+ # start with curtailment everywhere
+ curtail_mult=np.zeros(shape)
+
+ ifcurtailment.date_rangeisnotNone:
+ year=resource.time_index.year[0]
+ d0=pd.to_datetime(datetime.datetime(
+ month=int(curtailment.date_range[0][:2]),
+ day=int(curtailment.date_range[0][2:]),
+ year=year),utc=True)
+ d1=pd.to_datetime(datetime.datetime(
+ month=int(curtailment.date_range[1][:2]),
+ day=int(curtailment.date_range[1][2:]),
+ year=year),utc=True)
+ time_index=check_tz(resource.time_index)
+ mask=(time_index>=d0)&(time_index<d1)
+ mask=np.tile(np.expand_dims(mask,axis=1),shape[1])
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ elifcurtailment.monthsisnotNone:
+ # Curtail resource when in curtailment months
+ mask=np.isin(resource.time_index.month,curtailment.months)
+ mask=np.tile(np.expand_dims(mask,axis=1),shape[1])
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ else:
+ msg=('You must specify either months or date_range over '
+ 'which curtailment is possible!')
+ logger.error(msg)
+ raiseKeyError(msg)
+
+ # Curtail resource when curtailment is possible and is nighttime
+ lat_lon_cols=get_lat_lon_cols(resource.meta)
+ solar_zenith_angle=SolarPosition(
+ resource.time_index,
+ resource.meta[lat_lon_cols].values).zenith
+ mask=(solar_zenith_angle>curtailment.dawn_dusk)
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ # Curtail resource when curtailment is possible and not raining
+ ifcurtailment.precipitationisnotNone:
+ if'precipitationrate'notinresource._res_arrays:
+ warn('Curtailment has a precipitation threshold of "{}", but '
+ '"precipitationrate" was not found in the SAM resource '
+ 'variables. The following resource variables were '
+ 'available: {}.'
+ .format(curtailment.precipitation,
+ list(resource._res_arrays.keys())),
+ HandlerWarning)
+ else:
+ mask=(resource._res_arrays['precipitationrate']
+ <curtailment.precipitation)
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ # Curtail resource when curtailment is possible and temperature is high
+ ifcurtailment.temperatureisnotNone:
+ mask=(resource._res_arrays['temperature']
+ >curtailment.temperature)
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ # Curtail resource when curtailment is possible and not that windy
+ ifcurtailment.wind_speedisnotNone:
+ mask=(resource._res_arrays['windspeed']
+ <curtailment.wind_speed)
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ ifcurtailment.equationisnotNone:
+ # pylint: disable=W0123,W0612
+ wind_speed=resource._res_arrays['windspeed']
+ temperature=resource._res_arrays['temperature']
+ if'precipitationrate'inresource._res_arrays:
+ precipitation_rate=resource._res_arrays['precipitationrate']
+ mask=eval(curtailment.equation)
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ # Apply probability mask when curtailment is possible.
+ ifcurtailment.probability!=1:
+ np.random.seed(seed=random_seed)
+ mask=np.random.rand(shape[0],shape[1])<curtailment.probability
+ curtail_mult=np.where(mask,curtail_mult,1)
+
+ # Apply curtailment multiplier directly to resource
+ resource.curtail_windspeed(resource.sites,curtail_mult)
+
+ returnresource
[docs]defpd_date_range(*args,**kwargs):
+"""A simple wrapper on the pd.date_range() method that handles the closed
+ vs. inclusive kwarg change in pd 1.4.0"""
+ incl=version.parse(pd.__version__)>=version.parse('1.4.0')
+
+ ifincland'closed'inkwargs:
+ kwargs['inclusive']=kwargs.pop('closed')
+ elifnotincland'inclusive'inkwargs:
+ kwargs['closed']=kwargs.pop('inclusive')
+ ifkwargs['closed']=='both':
+ kwargs['closed']=None
+
+ returnpd.date_range(*args,**kwargs)
+
+
+
[docs]defwrite_chunk(meta,times,data,features,out_file):
+"""Write data chunk to an h5 file
+
+ Parameters
+ ----------
+ meta : dict
+ Dictionary of meta data for this chunk. Includes flattened lat and lon
+ arrays
+ times : pd.DatetimeIndex
+ times in this chunk
+ features : list
+ List of feature names in this chunk
+ out_file : str
+ Name of output file
+ """
+ withRexOutputs(out_file,'w')asfh:
+ fh.meta=meta
+ fh.time_index=times
+ forfeatureinfeatures:
+ flat_data=data.reshape((-1,len(times)))
+ flat_data=np.transpose(flat_data,(1,0))
+ fh.add_dataset(out_file,feature,flat_data,dtype=np.float32)
+
+
+
[docs]defmake_fake_h5_chunks(td,features,shuffle=False):
+"""Make fake h5 chunks to test collection
+
+ Parameters
+ ----------
+ td : tempfile.TemporaryDirectory
+ Test TemporaryDirectory
+ features : list
+ List of dsets to write to chunks
+ shuffle : bool
+ Whether to shuffle gids
+
+ Returns
+ -------
+ out_pattern : str
+ Pattern for output file names
+ data : ndarray
+ Full non-chunked data array
+ features : list
+ List of feature names in output
+ s_slices : list
+ List of spatial slices used to chunk full data array
+ times : pd.DatetimeIndex
+ Times in output
+ """
+ shape=(50,50,48)
+ data=np.random.uniform(0,20,shape)
+ lat=np.linspace(90,0,50)
+ lon=np.linspace(-180,0,50)
+ lon,lat=np.meshgrid(lon,lat)
+ gids=np.arange(np.product(lat.shape))
+ ifshuffle:
+ np.random.shuffle(gids)
+ gids=gids.reshape(shape[:-1])
+ times=pd_date_range('20220101','20220103',freq='3600s',
+ inclusive='left')
+ s_slices=[slice(0,25),slice(25,50)]
+ out_pattern=os.path.join(td,'chunks_{i}_{j}.h5')
+
+ fori,s1inenumerate(s_slices):
+ forj,s2inenumerate(s_slices):
+ out_file=out_pattern.format(i=i,j=j)
+ meta=pd.DataFrame(
+ {ResourceMetaField.LATITUDE:lat[s1,s2].flatten(),
+ ResourceMetaField.LONGITUDE:lon[s1,s2].flatten(),
+ ResourceMetaField.GID:gids[s1,s2].flatten()})
+ write_chunk(meta=meta,times=times,data=data[s1,s2],
+ features=features,out_file=out_file)
+
+ out=(out_pattern.format(i='*',j='*'),data,features,s_slices,times)
+ returnout
[docs]classSlottedDict:
+"""Slotted memory dictionary emulator."""
+
+ # make attribute slots for all dictionary keys
+ __slots__=['var_list']
+
+ def__init__(self):
+ self.var_list=[]
+
+ def__setitem__(self,key,value):
+"""Send data to a slot. Raise KeyError if key is not recognized"""
+ ifkeyinself.__slots__:
+ ifkeynotinself.var_list:
+ self.var_list.append(key)
+ setattr(self,key,value)
+ else:
+ raiseKeyError('Could not save "{}" to slotted dictionary. '
+ 'The following output variable slots are '
+ 'available: {}'.format(key,self.__slots__))
+
+ def__getitem__(self,key):
+"""Retrieve data from slot. Raise KeyError if key is not recognized"""
+ ifkeyinself.var_list:
+ returngetattr(self,key)
+ else:
+ raiseKeyError('Variable "{}" has not been saved to this slotted '
+ 'dictionary instance. Saved variables are: {}'
+ .format(key,self.keys()))
+
+
[docs]defupdate(self,slotted_dict):
+"""Add output variables from another instance into this instance.
+
+ Parameters
+ ----------
+ slotted_dict : SlottedDict
+ An different instance of this class (slotted dictionary class) to
+ merge into this instance. Variable data in this instance could be
+ overwritten by the new data.
+ """
+
+ attrs=slotted_dict.var_list
+ forattrinattrs:
+ ifattrinself.__slots__:
+ value=getattr(slotted_dict,attr,None)
+ ifvalueisnotNone:
+ self[attr]=value
+
+
[docs]defitems(self):
+"""Get an items iterator similar to a dictionary.
+
+ Parameters
+ ----------
+ items : iterator
+ [key, value] iterator similar to the output of dict.items()
+ """
+
+ keys=self.keys()
+ values=self.values()
+ returnzip(keys,values)
+
+
[docs]defkeys(self):
+"""Get a keys list similar to a dictionary.
+
+ Parameters
+ ----------
+ key : list
+ List of slotted variable names that have been set.
+ """
+ returnself.var_list
+
+
[docs]defvalues(self):
+"""Get a values list similar to a dictionary.
+
+ Parameters
+ ----------
+ values : list
+ List of slotted variable values that have been set.
+ """
+ return[self[k]forkinself.var_list]
+# -*- coding: utf-8 -*-
+"""
+Classes to handle h5 output files.
+"""
+importjson
+importlogging
+importnumpyasnp
+importpandasaspd
+importtime
+importsys
+importclick
+importh5py
+importh5pyd
+importscipy
+
+fromrex.versionimport__version__
+fromrex.utilities.exceptionsimport(HandlerRuntimeError,HandlerValueError,
+ ResourceKeyError)
+fromrex.resourceimportBaseResource
+fromrex.utilities.parse_keysimportparse_keys,parse_slice
+fromrex.utilities.utilitiesimportto_records_array
+
+logger=logging.getLogger(__name__)
+
+
+classOutputs(BaseResource):
+"""
+ Base class to handle output data in .h5 format
+
+ Examples
+ --------
+ The Outputs handler can be used to initialize h5 files in the standard
+ reV/rex resource data format.
+
+ >>> from rex import Outputs
+ >>> import pandas as pd
+ >>> import numpy as np
+ >>>
+ >>> meta = pd.DataFrame({'latitude': np.ones(100),
+ >>> 'longitude': np.ones(100)})
+ >>>
+ >>> time_index = pd.date_range('20210101', '20220101', freq='1h',
+ >>> closed='right')
+ >>>
+ >>> with Outputs('test.h5', 'w') as f:
+ >>> f.meta = meta
+ >>> f.time_index = time_index
+
+ You can also use the Outputs handler to read output h5 files from disk.
+ The Outputs handler will automatically parse the meta data and time index
+ into the expected pandas objects (DataFrame and DatetimeIndex,
+ respectively).
+
+ >>> with Outputs('test.h5') as f:
+ >>> print(f.meta.head())
+ >>>
+ latitude longitude
+ gid
+ 0 1.0 1.0
+ 1 1.0 1.0
+ 2 1.0 1.0
+ 3 1.0 1.0
+ 4 1.0 1.0
+
+ >>> with Outputs('test.h5') as f:
+ >>> print(f.time_index)
+ DatetimeIndex(['2021-01-01 01:00:00+00:00', '2021-01-01 02:00:00+00:00',
+ '2021-01-01 03:00:00+00:00', '2021-01-01 04:00:00+00:00',
+ '2021-01-01 05:00:00+00:00', '2021-01-01 06:00:00+00:00',
+ '2021-01-01 07:00:00+00:00', '2021-01-01 08:00:00+00:00',
+ '2021-01-01 09:00:00+00:00', '2021-01-01 10:00:00+00:00',
+ ...
+ '2021-12-31 15:00:00+00:00', '2021-12-31 16:00:00+00:00',
+ '2021-12-31 17:00:00+00:00', '2021-12-31 18:00:00+00:00',
+ '2021-12-31 19:00:00+00:00', '2021-12-31 20:00:00+00:00',
+ '2021-12-31 21:00:00+00:00', '2021-12-31 22:00:00+00:00',
+ '2021-12-31 23:00:00+00:00', '2022-01-01 00:00:00+00:00'],
+ dtype='datetime64[ns, UTC]', length=8760, freq=None)
+
+ There are a few ways to use the Outputs handler to write data to a file.
+ Here is one example using the pre-initialized file we created earlier.
+ Note that the Outputs handler will automatically scale float data using
+ the "scale_factor" attribute. The Outputs handler will unscale the data
+ while being read unless the unscale kwarg is explicityly set to False.
+ This behavior is intended to reduce disk storage requirements for big
+ data and can be disabled by setting dtype=np.float32 or dtype=np.float64
+ when writing data.
+
+ >>> Outputs.add_dataset(h5_file='test.h5', dset_name='dset1',
+ >>> dset_data=np.ones((8760, 100)) * 42.42,
+ >>> attrs={'scale_factor': 100}, dtype=np.int32)
+
+
+ >>> with Outputs('test.h5') as f:
+ >>> print(f['dset1'])
+ >>> print(f['dset1'].dtype)
+ [[42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ ...
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]
+ [42.42 42.42 42.42 ... 42.42 42.42 42.42]]
+ float32
+
+ >>> with Outputs('test.h5', unscale=False) as f:
+ >>> print(f['dset1'])
+ >>> print(f['dset1'].dtype)
+ [[4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]
+ ...
+ [4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]
+ [4242 4242 4242 ... 4242 4242 4242]]
+ int32
+
+ Note that the Outputs handler is specifically designed to read and
+ write spatiotemporal data. It is therefore important to intialize the meta
+ data and time index objects even if your data is only spatial or only
+ temporal. Furthermore, the Outputs handler will always assume that 1D
+ datasets represent scalar data (non-timeseries) that corresponds to the
+ meta data shape, and that 2D datasets represent spatiotemporal data whose
+ shape corresponds to (len(time_index), len(meta)). You can see these
+ constraints here:
+
+ >>> Outputs.add_dataset(h5_file='test.h5', dset_name='bad_shape',
+ dset_data=np.ones((1, 100)) * 42.42,
+ attrs={'scale_factor': 100}, dtype=np.int32)
+ HandlerValueError: 2D data with shape (1, 100) is not of the proper
+ spatiotemporal shape: (8760, 100)
+
+ >>> Outputs.add_dataset(h5_file='test.h5', dset_name='bad_shape',
+ dset_data=np.ones((8760,)) * 42.42,
+ attrs={'scale_factor': 100}, dtype=np.int32)
+ HandlerValueError: 1D data with shape (8760,) is not of the proper
+ spatial shape: (100,)
+ """
+
+ def__init__(self,h5_file,mode='r',unscale=True,str_decode=True,
+ group=None):
+"""
+ Parameters
+ ----------
+ h5_file : str
+ Path to .h5 resource file
+ mode : str, optional
+ Mode to instantiate h5py.File instance, by default 'r'
+ unscale : bool, optional
+ Boolean flag to automatically unscale variables on extraction,
+ by default True
+ str_decode : bool, optional
+ Boolean flag to decode the bytestring meta data into normal
+ strings. Setting this to False will speed up the meta data read,
+ by default True
+ group : str, optional
+ Group within .h5 resource file to open, by default None
+ """
+ super().__init__(h5_file,unscale=unscale,hsds=False,
+ str_decode=str_decode,group=group,mode=mode)
+ self._mode=mode
+ self._group=self._check_group(group)
+ self._shape=None
+
+ ifself.writable:
+ self.set_version_attr()
+
+ def__len__(self):
+ _len=0
+ if'meta'inself.datasets:
+ _len=self.h5['meta'].shape[0]
+
+ return_len
+
+ def__setitem__(self,keys,arr):
+ ifself.writable:
+ ds,ds_slice=parse_keys(keys)
+
+ slice_test=False
+ ifisinstance(ds_slice,tuple):
+ slice_test=ds_slice[0]==slice(None,None,None)
+
+ ifds.endswith('meta')andslice_test:
+ self._set_meta(ds,arr)
+ elifds.endswith('time_index')andslice_test:
+ self._set_time_index(ds,arr)
+ else:
+ self._set_ds_array(ds,arr,ds_slice)
+
+ @property
+ deffull_version_record(self):
+"""Get record of versions for dependencies
+
+ Returns
+ -------
+ dict
+ Dictionary of package versions for dependencies
+ """
+ versions={'rex':__version__,
+ 'pandas':pd.__version__,
+ 'numpy':np.__version__,
+ 'python':sys.version,
+ 'click':click.__version__,
+ 'h5py':h5py.__version__,
+ 'h5pyd':h5pyd.__version__,
+ 'scipy':scipy.__version__
+ }
+ returnversions
+
+ defset_version_attr(self):
+"""Set the version attribute to the h5 file."""
+ self.h5.attrs['version']=__version__
+ self.h5.attrs['full_version_record']=json.dumps(
+ self.full_version_record)
+ self.h5.attrs['package']='rex'
+
+ @property
+ defversion(self):
+"""
+ Version of package used to create file
+
+ Returns
+ -------
+ str
+ """
+ returnself.h5.attrs['version']
+
+ @property
+ defpackage(self):
+"""
+ Package used to create file
+
+ Returns
+ -------
+ str
+ """
+ returnself.h5.attrs['package']
+
+ @property
+ defsource(self):
+"""
+ Package and version used to create file
+
+ Returns
+ -------
+ str
+ """
+ out=("{}_{}"
+ .format(self.h5.attrs['package'],self.h5.attrs['version']))
+ returnout
+
+ @property
+ defshape(self):
+"""
+ Variable array shape from time_index and meta
+
+ Returns
+ -------
+ tuple
+ shape of variables arrays == (time, locations)
+ """
+ ifself._shapeisNone:
+ dsets=self.datasets
+ if'meta'indsets:
+ self._shape=self.h5['meta'].shape
+ if'time_index'indsets:
+ self._shape=self.h5['time_index'].shape+self._shape
+
+ returnself._shape
+
+ @property
+ defwritable(self):
+"""
+ Check to see if h5py.File instance is writable
+
+ Returns
+ -------
+ is_writable : bool
+ Flag if mode is writable
+ """
+ is_writable=True
+ mode=['a','w','w-','x']
+ ifself._modenotinmode:
+ is_writable=False
+
+ returnis_writable
+
+ @BaseResource.meta.setter# pylint: disable-msg=E1101
+ defmeta(self,meta):
+"""
+ Write meta data to disk, convert type if neccessary
+
+ Parameters
+ ----------
+ meta : pandas.DataFrame | numpy.recarray
+ Locational meta data
+ """
+ self._set_meta('meta',meta)
+
+ @BaseResource.time_index.setter# pylint: disable-msg=E1101
+ deftime_index(self,time_index):
+"""
+ Write time_index to dics, convert type if neccessary
+
+ Parameters
+ ----------
+ time_index : pandas.DatetimeIndex | ndarray
+ Temporal index of timesteps
+ """
+ self._set_time_index('time_index',time_index)
+
+ @property
+ defSAM_configs(self):
+"""
+ SAM configuration JSONs used to create CF profiles
+
+ Returns
+ -------
+ configs : dict
+ Dictionary of SAM configuration JSONs
+ """
+ if'meta'inself.datasets:
+ configs={k:json.loads(v)
+ fork,vinself.h5['meta'].attrs.items()}
+ else:
+ configs={}
+
+ returnconfigs
+
+ @property
+ defrun_attrs(self):
+"""
+ Runtime attributes stored at the global (file) level
+
+ Returns
+ -------
+ global_attrs : dict
+ """
+ returnself.global_attrs
+
+ @run_attrs.setter
+ defrun_attrs(self,run_attrs):
+"""
+ Set runtime attributes as global (file) attributes
+
+ Parameters
+ ----------
+ run_attrs : dict
+ Dictionary of runtime attributes (args, kwargs)
+ """
+ ifself.writable:
+ fork,vinrun_attrs.items():
+ self.h5.attrs[k]=v
+
+ @staticmethod
+ def_check_data_dtype(dset_name,data,dtype,attrs=None):
+"""
+ Check data dtype and scale if needed
+
+ Parameters
+ ----------
+ dset_name : str
+ Name of dataset being written to disk
+ data : ndarray
+ Data to be written to disc
+ dtype : str
+ dtype of data on disc
+ attrs : dict, optional
+ Attributes to be set. May include 'scale_factor',
+ by default None
+
+ Returns
+ -------
+ data : ndarray
+ Data ready for writing to disc:
+ - Scaled and converted to dtype
+ """
+ ifattrsisNone:
+ attrs={}
+
+ scale_factor=attrs.get('scale_factor',None)
+
+ scale=(scale_factorisnotNone
+ andnotnp.issubdtype(data.dtype,np.integer))
+ ifscale:
+ ifscale_factor!=1andnotnp.issubdtype(dtype,np.integer):
+ msg=('Output dtype for "{}" must be an integer in '
+ 'order to apply scale factor {}".'
+ .format(dset_name,scale_factor))
+ logger.error(msg)
+ raiseHandlerRuntimeError(msg)
+
+ data_type_differs=notnp.issubdtype(data.dtype,np.dtype(dtype))
+ is_integer=np.issubdtype(dtype,np.integer)
+ ifdata_type_differsandis_integer:
+ # apply scale factor and dtype
+ data=np.round(data*scale_factor).astype(dtype)
+
+ elif(notnp.issubdtype(data.dtype,np.dtype(dtype))
+ andnotnp.issubdtype(np.dtype(dtype),np.floating)):
+ msg=('A scale_factor is needed to scale '
+ '"{}" of type "{}" to "{}".'
+ .format(dset_name,data.dtype,dtype))
+ raiseHandlerRuntimeError(msg)
+
+ returndata
+
+ def_check_group(self,group):
+"""
+ Ensure group is in .h5 file
+
+ Parameters
+ ----------
+ group : str
+ Group of interest
+ """
+ ifgroupisnotNone:
+ ifgroupnotinself._h5:
+ try:
+ ifself.writable:
+ self._h5.create_group(group)
+ exceptExceptionasex:
+ msg=('Cannot create group {}: {}'
+ .format(group,ex))
+ raiseHandlerRuntimeError(msg)fromex
+
+ returngroup
+
+ def_set_meta(self,ds,meta,attrs=None):
+"""
+ Write meta data to disk
+
+ Parameters
+ ----------
+ ds : str
+ meta dataset name
+ meta : pandas.DataFrame | numpy.recarray
+ Locational meta data
+ attrs : dict
+ Attributes to add to the meta data dataset
+ """
+ # pylint: disable=attribute-defined-outside-init
+ self._meta=meta
+ ifisinstance(meta,pd.DataFrame):
+ meta=to_records_array(meta)
+
+ ifdsinself.datasets:
+ self.update_dset(ds,meta)
+ else:
+ self._create_dset(ds,meta.shape,meta.dtype,data=meta,
+ attrs=attrs)
+
+ def_set_time_index(self,ds,time_index,attrs=None):
+"""
+ Write time index to disk
+
+ Parameters
+ ----------
+ ds : str
+ time index dataset name
+ time_index : pandas.DatetimeIndex | ndarray
+ Temporal index of timesteps
+ attrs : dict
+ Attributes to add to the meta data dataset
+ """
+ # pylint: disable=attribute-defined-outside-init
+ self._time_index=time_index
+ ifisinstance(time_index,pd.DatetimeIndex):
+ time_index=time_index.astype(str)
+ dtype="S{}".format(len(time_index[0]))
+ time_index=np.array(time_index,dtype=dtype)
+
+ ifdsinself.datasets:
+ self.update_dset(ds,time_index)
+ else:
+ self._create_dset(ds,time_index.shape,time_index.dtype,
+ data=time_index,attrs=attrs)
+
+
[docs]defget_config(self,config_name):
+"""
+ Get SAM config
+
+ Parameters
+ ----------
+ config_name : str
+ Name of config
+
+ Returns
+ -------
+ config : dict
+ SAM config JSON as a dictionary
+ """
+ if'meta'inself.datasets:
+ config=json.loads(self.h5['meta'].attrs[config_name])
+ else:
+ config=None
+
+ returnconfig
+
+
[docs]defset_configs(self,SAM_configs):
+"""
+ Set SAM configuration JSONs as attributes of 'meta'
+
+ Parameters
+ ----------
+ SAM_configs : dict
+ Dictionary of SAM configuration JSONs
+ """
+ ifself.writable:
+ forkey,configinSAM_configs.items():
+ ifisinstance(config,dict):
+ config=json.dumps(config)
+
+ ifnotisinstance(key,str):
+ key=str(key)
+
+ self.h5['meta'].attrs[key]=config
+
+ def_set_ds_array(self,ds_name,arr,ds_slice):
+"""
+ Write ds to disk
+
+ Parameters
+ ----------
+ ds_name : str
+ Dataset name
+ arr : ndarray
+ Dataset data array
+ ds_slice : tuple
+ Dataset slicing that corresponds to arr
+ """
+ ifds_namenotinself.datasets:
+ msg='{} must be initialized!'.format(ds_name)
+ raiseHandlerRuntimeError(msg)
+
+ dtype=self.h5[ds_name].dtype
+ attrs=self.get_attrs(ds_name)
+ ds_slice=parse_slice(ds_slice)
+ self.h5[ds_name][ds_slice]=self._check_data_dtype(
+ ds_name,arr,dtype,attrs=attrs)
+
+ def_check_chunks(self,chunks,data=None):
+"""
+ Convert dataset chunk size into valid tuple based on variable array
+ shape
+
+ Parameters
+ ----------
+ chunks : tuple
+ Desired dataset chunk size
+ data : ndarray
+ Dataset array being chunked
+
+ Returns
+ -------
+ ds_chunks : tuple | None
+ dataset chunk size
+ """
+ ifchunksisNone:
+ returnNone
+
+ ifdataisnotNone:
+ shape=data.shape
+ else:
+ shape=self.shape
+
+ iflen(shape)!=len(chunks):
+ msg=('Shape dimensions ({}) are not the same length as chunks '
+ '({}). Please provide a single chunk value for each '
+ 'dimension!'
+ .format(shape,chunks))
+ logger.error(msg)
+ raiseHandlerRuntimeError(msg)
+
+ returntuple(np.min((s,sifcisNoneelsec))
+ fors,cinzip(shape,chunks))
+
+ def_create_dset(self,ds_name,shape,dtype,chunks=None,attrs=None,
+ data=None,replace=True):
+"""
+ Initialize dataset
+
+ Parameters
+ ----------
+ ds_name : str
+ Dataset name
+ shape : tuple
+ Dataset shape
+ dtype : str
+ Dataset numpy dtype
+ chunks : tuple
+ Dataset chunk size
+ attrs : dict
+ Dataset attributes
+ data : ndarray
+ Dataset data array
+ replace : bool
+ If previous dataset exists with the same name, it will be replaced.
+ """
+ ds=None
+ ifself.writable:
+ ifds_nameinself.datasetsandreplace:
+ delself.h5[ds_name]
+
+ elifds_nameinself.datasets:
+ old_shape,old_dtype,_=self.get_dset_properties(ds_name)
+ ifold_shape!=shapeorold_dtype!=dtype:
+ e=('Trying to create dataset "{}", but already exists '
+ 'with mismatched shape and dtype. New shape/dtype '
+ 'is {}/{}, previous shape/dtype is {}/{}'
+ .format(ds_name,shape,dtype,old_shape,old_dtype))
+ logger.error(e)
+ raiseHandlerRuntimeError(e)
+
+ ifds_namenotinself.datasets:
+ chunks=self._check_chunks(chunks,data=data)
+ try:
+ ds=self.h5.create_dataset(ds_name,shape=shape,
+ dtype=dtype,chunks=chunks)
+ exceptExceptionase:
+ msg=('Could not create dataset "{}" in file!'
+ .format(ds_name))
+ logger.error(msg)
+ raiseIOError(msg)frome
+
+ ifattrsisnotNone:
+ self._create_ds_attrs(ds,ds_name,attrs)
+
+ ifdataisnotNone:
+ ds[...]=data
+
+ @staticmethod
+ def_create_ds_attrs(ds,ds_name,attrs):
+"""Create dataset attributes.
+
+ Parameters
+ ----------
+ ds : h5py.Dataset
+ Dataset object to write attributes to.
+ ds_name : str
+ Dataset name for logging / debugging
+ attrs : dict | None
+ Dataset attributes to write (None if no attributes to write).
+ """
+ ifattrsisnotNone:
+ forkey,valueinattrs.items():
+ try:
+ ds.attrs[key]=value
+ exceptExceptionase:
+ msg=('Could not save datset "{}" attribute "{}" '
+ 'to value: {}'.format(ds_name,key,value))
+ logger.error(msg)
+ raiseIOError(msg)frome
+
+ def_check_dset_shape(self,dset_name,dset_data):
+"""
+ Check to ensure that dataset array is of the proper shape
+
+ Parameters
+ ----------
+ dset_name : str
+ Dataset name being written to disk.
+ dset_data : ndarray
+ Dataset data array
+ """
+ dset_shape=dset_data.shape
+ iflen(dset_shape)==1:
+ possible_shapes={}
+ try:
+ possible_shapes["spatial"]=(len(self.meta),)
+ exceptResourceKeyError:
+ pass
+ try:
+ possible_shapes["temporal"]=(len(self.time_index),)
+ exceptResourceKeyError:
+ pass
+
+ ifnotpossible_shapes:
+ msg=("Please load either 'meta' or 'time_index' before "
+ "loading a 1D dataset.")
+ logger.error(msg)
+ raiseHandlerRuntimeError(msg)
+
+ ifdset_shapenotinpossible_shapes.values():
+ possible_shapes_str=" or ".join(["{}{}".format(k,v)
+ fork,v
+ inpossible_shapes.items()])
+ msg=('1D dataset "{}" with shape {} is not of '
+ 'the proper {} shape!'
+ .format(dset_name,dset_shape,possible_shapes_str))
+ logger.error(msg)
+ raiseHandlerValueError(msg)
+ else:
+ shape=self.shape
+ ifshape:
+ ifdset_shape!=shape:
+ msg=('2D dataset "{}" with shape {} is not of the '
+ 'proper spatiotemporal shape: {}'
+ .format(dset_name,dset_shape,shape))
+ logger.error(msg)
+ raiseHandlerValueError(msg)
+ else:
+ msg=("'meta' and 'time_index' have not been loaded")
+ logger.error(msg)
+ raiseHandlerRuntimeError(msg)
+
+ def_add_dset(self,dset_name,data,dtype,chunks=None,attrs=None):
+"""
+ Write dataset to disk. Dataset it created in .h5 file and data is
+ scaled if needed.
+
+ Parameters
+ ----------
+ dset_name : str
+ Name of dataset to be added to h5 file.
+ data : ndarray
+ Data to be added to h5 file.
+ dtype : str
+ Intended dataset datatype after scaling.
+ chunks : tuple
+ Chunk size for capacity factor means dataset.
+ attrs : dict
+ Attributes to be set. May include 'scale_factor'.
+ """
+ self._check_dset_shape(dset_name,data)
+
+ data=self._check_data_dtype(dset_name,data,dtype,attrs=attrs)
+
+ self._create_dset(dset_name,data.shape,dtype,
+ chunks=chunks,attrs=attrs,data=data)
+
+
[docs]defupdate_dset(self,dset,dset_array,dset_slice=None):
+"""
+ Check to see if dset needs to be updated on disk
+ If so write dset_array to disk
+
+ Parameters
+ ----------
+ dset : str
+ dataset to update
+ dset_array : ndarray
+ dataset array
+ dset_slice : tuple
+ slice of dataset to update, it None update all
+ """
+ ifdset_sliceisNone:
+ dset_slice=(slice(None,None,None),)
+
+ keys=(dset,)+dset_slice
+
+ arr=self.__getitem__(keys)
+ ifnotnp.array_equal(arr,dset_array):
+ self._set_ds_array(dset,dset_array,dset_slice)
+
+
[docs]defwrite_dataset(self,dset_name,data,dtype,chunks=None,attrs=None):
+"""
+ Write dataset to disk. Dataset it created in .h5 file and data is
+ scaled if needed.
+
+ Parameters
+ ----------
+ dset_name : str
+ Name of dataset to be added to h5 file.
+ data : ndarray
+ Data to be added to h5 file.
+ dtype : str
+ Intended dataset datatype after scaling.
+ chunks : tuple
+ Chunk size for capacity factor means dataset.
+ attrs : dict
+ Attributes to be set. May include 'scale_factor'.
+ """
+ self._add_dset(dset_name,data,dtype,chunks=chunks,attrs=attrs)
+
+
[docs]@classmethod
+ defwrite_profiles(cls,h5_file,meta,time_index,dset_name,profiles,
+ dtype,attrs=None,SAM_configs=None,chunks=(None,100),
+ unscale=True,mode='w-',str_decode=True,group=None):
+"""
+ Write profiles to disk
+
+ Parameters
+ ----------
+ h5_file : str
+ Path to .h5 resource file
+ meta : pandas.Dataframe
+ Locational meta data
+ time_index : pandas.DatetimeIndex
+ Temporal timesteps
+ dset_name : str
+ Name of the target dataset (should identify the profiles).
+ profiles : ndarray
+ output result timeseries profiles
+ dtype : str
+ Intended dataset datatype after scaling.
+ attrs : dict, optional
+ Attributes to be set. May include 'scale_factor', by default None
+ SAM_configs : dict, optional
+ Dictionary of SAM configuration JSONs used to compute cf means,
+ by default None
+ chunks : tuple, optional
+ Chunk size for capacity factor means dataset,
+ by default (None, 100)
+ unscale : bool, optional
+ Boolean flag to automatically unscale variables on extraction,
+ by default True
+ mode : str, optional
+ Mode to instantiate h5py.File instance, by default 'w-'
+ str_decode : bool, optional
+ Boolean flag to decode the bytestring meta data into normal
+ strings. Setting this to False will speed up the meta data read,
+ by default True
+ group : str, optional
+ Group within .h5 resource file to open, by default None
+ """
+ logger.info("Saving profiles ({}) to {}".format(dset_name,h5_file))
+ ifprofiles.shape!=(len(time_index),len(meta)):
+ raiseHandlerValueError("Profile dimensions does not match"
+ "'time_index' and 'meta'")
+ ts=time.time()
+ kwargs={"unscale":unscale,"mode":mode,"str_decode":str_decode,
+ "group":group}
+ withcls(h5_file,**kwargs)asf:
+ # Save time index
+ f['time_index']=time_index
+ logger.debug("\t- 'time_index' saved to disc")
+ # Save meta
+ f['meta']=meta
+ logger.debug("\t- 'meta' saved to disc")
+ # Add SAM configurations as attributes to meta
+ ifSAM_configsisnotNone:
+ f.set_configs(SAM_configs)
+ logger.debug("\t- SAM configurations saved as attributes "
+ "on 'meta'")
+
+ # Write dset to disk
+ f._add_dset(dset_name,profiles,dtype,
+ chunks=chunks,attrs=attrs)
+ logger.debug("\t- '{}' saved to disc".format(dset_name))
+
+ tt=(time.time()-ts)/60
+ logger.info('{} is complete'.format(h5_file))
+ logger.debug('\t- Saving to disc took {:.4f} minutes'
+ .format(tt))
+
+
[docs]@classmethod
+ defwrite_means(cls,h5_file,meta,dset_name,means,dtype,attrs=None,
+ SAM_configs=None,chunks=None,unscale=True,mode='w-',
+ str_decode=True,group=None):
+"""
+ Write means array to disk
+
+ Parameters
+ ----------
+ h5_file : str
+ Path to .h5 resource file
+ meta : pandas.Dataframe
+ Locational meta data
+ dset_name : str
+ Name of the target dataset (should identify the means).
+ means : ndarray
+ output means array.
+ dtype : str
+ Intended dataset datatype after scaling.
+ attrs : dict, optional
+ Attributes to be set. May include 'scale_factor', by default None
+ SAM_configs : dict, optional
+ Dictionary of SAM configuration JSONs used to compute cf means,
+ by default None
+ chunks : tuple, optional
+ Chunk size for capacity factor means dataset, by default None
+ unscale : bool, optional
+ Boolean flag to automatically unscale variables on extraction,
+ by default True
+ mode : str, optional
+ Mode to instantiate h5py.File instance, by default 'w-'
+ str_decode : bool, optional
+ Boolean flag to decode the bytestring meta data into normal
+ strings. Setting this to False will speed up the meta data read,
+ by default True
+ group : str, optional
+ Group within .h5 resource file to open, by default None
+ """
+ logger.info("Saving means ({}) to {}".format(dset_name,h5_file))
+ iflen(means)!=len(meta):
+ msg='Number of means does not match meta'
+ raiseHandlerValueError(msg)
+
+ ts=time.time()
+ kwargs={"unscale":unscale,"mode":mode,"str_decode":str_decode,
+ "group":group}
+ withcls(h5_file,**kwargs)asf:
+ # Save meta
+ f['meta']=meta
+ logger.debug("\t- 'meta' saved to disc")
+ # Add SAM configurations as attributes to meta
+ ifSAM_configsisnotNone:
+ f.set_configs(SAM_configs)
+ logger.debug("\t- SAM configurations saved as attributes "
+ "on 'meta'")
+
+ # Write dset to disk
+ f._add_dset(dset_name,means,dtype,
+ chunks=chunks,attrs=attrs)
+ logger.debug("\t- '{}' saved to disc".format(dset_name))
+
+ tt=(time.time()-ts)/60
+ logger.info('{} is complete'.format(h5_file))
+ logger.debug('\t- Saving to disc took {:.4f} minutes'
+ .format(tt))
+
+
[docs]@classmethod
+ defadd_dataset(cls,h5_file,dset_name,dset_data,dtype,attrs=None,
+ chunks=None,unscale=True,mode='a',str_decode=True,
+ group=None):
+"""
+ Add dataset to h5_file
+
+ Parameters
+ ----------
+ h5_file : str
+ Path to .h5 resource file
+ dset_name : str
+ Name of dataset to be added to h5 file
+ dset_data : ndarray
+ Data to be added to h5 file
+ dtype : str
+ Intended dataset datatype after scaling.
+ attrs : dict, optional
+ Attributes to be set. May include 'scale_factor', by default None
+ unscale : bool, optional
+ Boolean flag to automatically unscale variables on extraction,
+ by default True
+ mode : str, optional
+ Mode to instantiate h5py.File instance, by default 'a'
+ str_decode : bool, optional
+ Boolean flag to decode the bytestring meta data into normal
+ strings. Setting this to False will speed up the meta data read,
+ by default True
+ group : str, optional
+ Group within .h5 resource file to open, by default None
+ """
+ logger.info("Adding {} to {}".format(dset_name,h5_file))
+ ts=time.time()
+ kwargs={"unscale":unscale,"mode":mode,"str_decode":str_decode,
+ "group":group}
+ withcls(h5_file,**kwargs)asf:
+ f._add_dset(dset_name,dset_data,dtype,
+ chunks=chunks,attrs=attrs)
+
+ tt=(time.time()-ts)/60
+ logger.info('{} added'.format(dset_name))
+ logger.debug('\t- Saving to disc took {:.4f} minutes'
+ .format(tt))
+
+
[docs]@classmethod
+ definit_h5(cls,h5_file,dsets,shapes,attrs,chunks,dtypes,
+ meta,time_index=None,configs=None,unscale=True,mode='w',
+ str_decode=True,group=None,run_attrs=None):
+"""Init a full output file with the final intended shape without data.
+
+ Parameters
+ ----------
+ h5_file : str
+ Full h5 output filepath.
+ dsets : list
+ List of strings of dataset names to initialize (does not include
+ meta or time_index).
+ shapes : dict
+ Dictionary of dataset shapes (keys correspond to dsets).
+ attrs : dict
+ Dictionary of dataset attributes (keys correspond to dsets).
+ chunks : dict
+ Dictionary of chunk tuples (keys correspond to dsets).
+ dtypes : dict
+ dictionary of numpy datatypes (keys correspond to dsets).
+ meta : pd.DataFrame
+ Full meta data.
+ time_index : pd.datetimeindex | None
+ Full pandas datetime index. None implies that only 1D results
+ (no site profiles) are being written.
+ configs : dict | None
+ Optional input configs to set as attr on meta.
+ unscale : bool
+ Boolean flag to automatically unscale variables on extraction
+ mode : str
+ Mode to instantiate h5py.File instance
+ str_decode : bool
+ Boolean flag to decode the bytestring meta data into normal
+ strings. Setting this to False will speed up the meta data read.
+ group : str
+ Group within .h5 resource file to open
+ run_attrs : dict | NoneType
+ Runtime attributes (args, kwargs) to add as global (file)
+ attributes
+ """
+
+ logger.debug("Initializing output file: {}".format(h5_file))
+ kwargs={"unscale":unscale,"mode":mode,"str_decode":str_decode,
+ "group":group}
+ withcls(h5_file,**kwargs)asf:
+ ifrun_attrsisnotNone:
+ f.run_attrs=run_attrs
+
+ f['meta']=meta
+
+ iftime_indexisnotNone:
+ f['time_index']=time_index
+
+ fordsetindsets:
+ ifdsetnotin('meta','time_index'):
+ # initialize each dset to disk
+ f._create_dset(dset,shapes[dset],dtypes[dset],
+ chunks=chunks[dset],attrs=attrs[dset])
+
+ ifconfigsisnotNone:
+ f.set_configs(configs)
+ logger.debug("\t- Configurations saved as attributes "
+ "on 'meta'")
+
+ logger.debug('Output file has been initialized.')
+ Symbols
+ | _
+ | A
+ | B
+ | C
+ | D
+ | E
+ | F
+ | G
+ | H
+ | I
+ | J
+ | K
+ | L
+ | M
+ | N
+ | O
+ | P
+ | Q
+ | R
+ | S
+ | T
+ | U
+ | V
+ | W
+ | Y
+
+
reV (the Renewable Energy Potential model)
+is an open-source geospatial techno-economic tool that
+estimates renewable energy technical potential (capacity and generation),
+system cost, and supply curves for solar photovoltaics (PV),
+concentrating solar power (CSP), geothermal, and wind energy.
+reV allows researchers to include exhaustive spatial representation
+of the built and natural environment into the generation and cost estimates
+that it computes.
+
reV is highly dynamic, allowing analysts to assess potential at varying levels
+of detail — from a single site up to an entire continent at temporal resolutions
+ranging from five minutes to hourly, spanning a single year or multiple decades.
+The reV model can (and has been used to) provide broad coverage across large spatial
+extents, including North America, South and Central Asia, the Middle East, South America,
+and South Africa to inform national and international-scale analyses. Still, reV is
+equally well-suited for regional infrastructure and deployment planning and analysis.
+
For a detailed description of reV capabilities and functionality, see the
+NREL reV technical report.
reV is a set of Python classes and functions
+that can be executed on HPC systems using CLI commands.
+A full reV execution consists of one or more compute modules
+(each consisting of their own Python class/CLI command)
+strung together using a pipeline framework,
+or configured using batch.
+
A typical reV workflow begins with input wind/solar/geothermal resource data
+(following the rex data format)
+that is passed through the generation module. This output is then collected across space and time
+(if executed on the HPC), before being sent off to be aggregated under user-specified land exclusion scenarios.
+Exclusion data is typically provided via a collection of high-resolution spatial data layers stored in an HDF5 file.
+This file must be readable by reV’s
+ExclusionLayers
+class. See the reVX Setbacks utility
+for instructions on generating setback exclusions for use in reV.
+Next, transmission costs are computed for each aggregated
+“supply-curve point” using user-provided transmission cost tables.
+See the reVX transmission cost calculator utility
+for instructions on generating transmission cost tables.
+Finally, the supply curves and initial generation data can be used to
+extract representative generation profiles for each supply curve point.
NOTE: The installation instruction below assume that you have python installed
+on your machine and are using conda
+as your package/environment manager.
+
+
Option 1: Install from PIP (recommended for analysts):
+
+
+
Create a new environment:
condacreate--namerevpython=3.9
+
+
+
+
+
Activate directory:
condaactivaterev
+
+
+
+
+
Install reV:
+
pipinstallNREL-reV or
+
+
NOTE: If you install using conda and want to use HSDS
+you will also need to install h5pyd manually: pipinstallh5pyd
+
+
+
+
+
+
+
+
+
+
Option 2: Clone repo (recommended for developers)
+
+
from home dir, gitclonegit@github.com:NREL/reV.git
+
+
Create reV environment and install package
+
Create a conda env: condacreate-nrev
+
Run the command: condaactivaterev
+
cd into the repo cloned in 1.
+
prior to running pip below, make sure the branch is correct (install
+from main!)
+
Install reV and its dependencies by running:
+pipinstall. (or pipinstall-e. if running a dev branch
+or working on the source code)
+
+
+
+
+
+
Check that reV was installed successfully
+
From any directory, run the following commands. This should return the
+help pages for the CLI’s.
Please cite both the technical paper and the software with the version and
+DOI you used:
+
Maclaurin, Galen J., Nicholas W. Grue, Anthony J. Lopez, Donna M. Heimiller,
+Michael Rossol, Grant Buster, and Travis Williams. 2019. “The Renewable Energy
+Potential (reV) Model: A Geospatial Platform for Technical Potential and Supply
+Curve Modeling.” Golden, Colorado, United States: National Renewable Energy
+Laboratory. NREL/TP-6A20-73067. https://doi.org/10.2172/1563140.
+
Grant Buster, Michael Rossol, Paul Pinchuk, Brandon N Benton, Robert Spencer,
+Mike Bannister, & Travis Williams. (2023).
+NREL/reV: reV 0.8.0 (v0.8.0). Zenodo. https://doi.org/10.5281/zenodo.8247528
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/misc/examples.advanced_econ_modeling.html b/misc/examples.advanced_econ_modeling.html
new file mode 100644
index 000000000..2f904753c
--- /dev/null
+++ b/misc/examples.advanced_econ_modeling.html
@@ -0,0 +1,669 @@
+
+
+
+
+
+
+
+
+ SAM Single Owner Modeling — reV 0.9.4 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This example set shows how several of the reV features (batching, pipeline,
+site-data) can be used in concert to create complex spatially-variant economic
+analyses.
+
This example modifies the tax rate and PPA price inputs for each state.
+More complex input sets on a site-by-site basis can be easily generated using a
+similar site_data input method.
The batching config in this example represents the high-level executed module.
+The user executes the following command:
+
reVbatch-c"../config_batch.json"
+
+
+
This creates and executes three batch job pipelines. You should be able to see
+in config_batch.json how the actual input generation files are
+parameterized. This is the power of the batch module - it’s sufficiently
+generic to modify ANY key-value pairs in any .json file, including other
+config files.
+
The first module executed in each job pipeline is the econ module. This example
+shows how the site-specific input .csv can be used (see the “site_data” key
+in the config_econ.json file).
+
The site_data.csv file sets site-specific input data corresponding to the
+gids in the project points file. Data inputs keyed by each column header in the
+site_data.csv file will be added to or replace an input in the
+“tech_configs” .json files (sam_files).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/misc/examples.aws_pcluster.html b/misc/examples.aws_pcluster.html
new file mode 100644
index 000000000..7fe5ca00b
--- /dev/null
+++ b/misc/examples.aws_pcluster.html
@@ -0,0 +1,925 @@
+
+
+
+
+
+
+
+
+ Running reV on an AWS Parallel Cluster — reV 0.9.4 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
reV was originally designed to run on the NREL high performance computer (HPC), but you can now run reV on AWS using the NREL renewable energy resource data (the NSRDB and WTK) that lives on S3. This example will guide you through how to set up reV on an AWS HPC environment with dynamically scaled EC2 compute resources and input resource data sourced from S3 via HSDS.
+
If you plan on only running reV for a handful of sites (less than 100), first check out our running with HSDS example, which will be a lot easier to get started with. Larger reV jobs require you stand up your own AWS parallel cluster and HSDS server. Very small jobs can be run locally using the NREL HSDS developer API.
+
Note that everything should be done in AWS region us-west-2 (Oregon) since this is where the NSRDB and WTK data live on S3.
Choose a basic instance for head node (master_instance_type), e.g. t2.micro, t2.large, c5.large, or c5.xlarge. Note that a t2 instance is free-tier eligible and is probably sufficient for the pcluster login node which will not be doing any of the compute or storage.
+
Choose a shared EBS storage volume (this is the /shared/ volume) with a “gp2” (volume_type) which can have SSD storage ranging from 1GB-16TB (volume_size).
Seems like EBS is probably fine and FSx is unnecessary for non-IO-intensive reV modules. Generation will retrieve resource data from HSDS and so is probably fine with EBS. SC-aggregation is probably fine with EBS if you set pre_extract_inclusions=True.
+
+
+
+
Login to your cluster from your cloud9 IDE: pclustersshpcluster_name-i~/.ssh/lab-3-key
+
Get Miniconda, install, and activate your conda environment.
Set up an HSDS service. At this time, it is recommended that you use HSDS Local Servers on your compute cluster. See the HSDS instructions below for details.
+
Install reV
+
+
+
You need to clone the reV repo to get the aws_pclusterexample files. reV example files do not ship with the pypi package.
+
You will have to first add the pcluster public ssh key (cat~/.ssh/id_rsa.pub) to your github ssh keys.
+
Put the reV repo in the /shared/ volume so that the aws_pcluster project directory is in the large EBS storage volume shared between compute nodes.
+
cd/shared/
+
gitclonegit@github.com:NREL/reV.git
+
cd/shared/reV/
+
pipinstall-e.
+
+
+
+
Try running the reV aws_pcluster example:
+
+
+
cd/shared/reV/examples/aws_pcluster
+
reVpipeline-cconfig_pipeline.json
+
Check the slurm queue with squeue and the compute cluster status in the ec2 console or with sinfo
+
Jobs will be in state CF (configuring) while the nodes spin up (this can take several minutes) and then R (running)
If you don’t configure a custom HSDS Service you will almost certainly see 503 errors from too many requests being processed. See the instructions below to configure an HSDS Service.
+
AWS EC2 instances usually have twice as many vCPUs as physical CPUs due to a default of two threads per physical CPU (at least for the c5 instances) (see disable_hyperthreading=false). The pcluster framework treats each thread as a “node” that can accept one reV job. For this reason, it is recommended that you scale the "nodes" entry in the reV generation config file but keep "max_workers":1. For example, if you use two c5.2xlarge instances in your compute fleet, this is a total of 16 vCPUs, each of which can be thought of as a HPC “node” that can run one process at a time.
+
If you setup an HSDS local server but the parallel cluster ends up sending too many requests (some nodes but not all will see 503 errors), you can try upping the max_task_count in the ~/hsds/admin/config/override.yml file.
+
If your HSDS local server nodes run out of memory (monitor with dockerstats), you can try upping the dn_ram or sn_ram options in the ~/hsds/admin/config/override.yml file.
+
The best way to stop your pcluster is using pclusterstoppcluster_name from the cloud9 IDE (not ssh’d into the pcluster) and then stop the login node in the AWS Console EC2 interface (find the “master” node and stop the instance). This will keep the EBS data intact and not charge you for EC2 costs. When you’re done with the pcluster you can call pclusterdeletepcluster_name but this will also delete all of the EBS data.
+
+
+
+
Setting up HSDS Local Servers on your Compute Cluster
+
The current recommended approach for setting up an HSDS service for reV is to start local HSDS servers on your AWS parallel cluster compute nodes. These instructions set up a shell script that each reV compute job will run on its respective compute node. The shell script checks that an HSDS local server is running, and will start one if not. These instructions are generally copied from the HSDS AWS README with a few modifications.
+
Note that these instructions were originally developed and tested in February 2022 and have not been maintained. The latest instructions for setting up HSDS local servers can be found in the rex docs page: HSDS local server instructions. The best way to run reV on an AWS PCluster with HSDS local servers may be a combination of the instructions below and the latest instructions from the rex docs page.
+
+
Make sure you have installed Miniconda but have not yet installed reV/rex.
+
Clone the HSDS Repository. into your home directory in the pcluster login node: gitclonegit@github.com:HDFGroup/hsds.git (you may have to set up your ssh keys first).
+
Install HSDS by running pythonsetup.pyinstall from the hsds repository folder (running pythonsetup.pyinstall is currently required as the setup script does some extra magic over a pip installation).
+
Copy the password file: cp~/hsds/admin/config/passwd.default~/hsds/admin/config/passwd.txt and (optionally) modify any username/passwords you wish.
+
Create an HSDS config file at ~/.hscfg with the following entries:
+
+
# Local HSDS server
+hs_endpoint=http://localhost:5101
+hs_username=admin
+hs_password=admin
+hs_api_key=None
+hs_bucket=nrel-pds-hsds
+
+
+
+
+
Copy the start_hsds.sh script from this example (source file) to your home directory in the pcluster login node (e.g. cp/shared/reV/examples/aws_pcluster/start_hsds.sh~/).
+
Replace the following environment variables in start_hsds.sh with your values: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and BUCKET_NAME (note that you should use AWS keys from an IAM user with admin privileges and not your AWS console root user).
+
Optional, to test your HSDS local server config, do the following:
+
+
+
Run the start script: sh~/start_hsds.sh
+
Run dockerps and verify that there are 4 or more HSDS services active (hsds_rangeget_1, hsds_sn_1, hsds_head_1, and an hsds_dn_* node for every available core)
+
Run hsinfo and verify that this doesn’t throw an error
+
Try running pipinstallh5pyd and then run the the h5pyd test (either the .py in this example or the h5pyd test snippet below).
+
+
+
+
Make sure this key-value pair is set in the execution_control block of the config_gen.json file: "sh_script":"sh~/start_hsds.sh"
+
Optional, copy the config override file: cp~/hsds/admin/config/config.yml~/hsds/admin/config/override.yml, update any config lines in the override.yml file that you wish to change, and remove all other lines (see notes on max_task_count and dn_ram).
+
You should be good to go! The line in the generation config file makes reV run the start_hsds.sh script before running the reV job. The script will install docker and make sure one HSDS server is running per EC2 instance.
Setting up your own HSDS Kubernetes service is one way to run a large reV job with full parallelization. This has not been trialed by the NREL team in full, but we have tested on the HSDS group’s Kubernetes cluster. If you want to pursue this route, you can follow the HSDS repository instructions for HSDS Kubernetes on AWS.
We’ve tested AWS Lambda functions as the HSDS service for reV workflows and we’ve found that Lambda functions require too much overhead to work well with the reV workflow. These instructions are included here for posterity, but HSDS-Lambda is _not_ recommended for the reV workflow.
+
These instructions are generally copied from the HSDS Lambda README with a few modifications.
+
It seems you cannot currently use the public ECR container image from the HSDS ECR repo so the first few bullets are instructions on how to set up your own HSDS image and push to a private ECR repo.
+
H5pyd cannot currently call a lambda function directly, so the instructions at the end show you how to set up an API gateway that interfaces between h5pyd and the lambda function.
+
Follow these instructions from your Cloud9 environment. None of this is directly related to the pcluster environment, except for the requirement to add the .hscfg file in the pcluster home directory.
In the AWS Management Console, create a new ECR repository called “hslambda”. Keep the default private repo settings.
+
Create an HSDS image and push to your hslambda ECR repo. This sublist is a combination of commands from the ECR push commands and the HSDS build instructions (make sure you use the actual push commands from your ECR repo with the actual region, repository name, and aws account id):
You should now see your new image appear in your hslambda ECR repo in the AWS Console. Get the URI from this image.
+
In the AWS Management Console, go to the Lambda service interface in your desired region (us-west-2, Oregon).
+
Click “Create Function” -> Choose “Container Image” option, function name is hslambda, use the Container Image URI from the image you just uploaded to your ECR repo, select “Create Function” and wait for the image to load.
+
You should see a banner saying you’ve successfully created the hslambda function. Yay!
+
Set the following in the configuration tab:
+
+
+
Use at least 1024MB of memory (feel free to tune this later for your workload)
+
Timeout of at least 30 seconds (feel free to tune this later for your workload)
+
Use an execution role that includes S3 read only access
+
Add an environment variable AWS_S3_GATEWAY: http://s3.us-west-2.amazonaws.com
+
+
+
+
Select the “Test” tab and click on the “Test” button. You should see a successful run with a status_code of 200 and an output like this:
+
+
{
+ "isBase64Encoded":false,
+ "statusCode":200,
+ "headers":"{\"Content-Type\": \"application/json; charset=utf-8\", \"Content-Length\": \"323\", \"Date\": \"Tue, 23 Nov 2021 22:27:08 GMT\", \"Server\": \"Python/3.8 aiohttp/3.8.1\"}",
+ "body":"{\"start_time\": 1637706428, \"state\": \"READY\", \"hsds_version\": \"0.7.0beta\", \"name\": \"HSDS on AWS Lambda\", \"greeting\": \"Welcome to HSDS!\", \"about\": \"HSDS is a webservice for HDF data\", \"node_count\": 1, \"dn_urls\": [\"http+unix://%2Ftmp%2Fhs1a1c917f%2Fdn_1.sock\"], \"dn_ids\": [\"dn-001\"], \"username\": \"anonymous\", \"isadmin\": false}"
+}
+
+
+
+
+
Now we need to create an API Gateway so that reV and h5pyd can interface with the lambda function. Go to the API Gateway page in the AWS console and do these things:
+
+
+
Create API -> choose HTTP API (build)
+
Add integration -> Lambda -> use us-west-2, select your lambda function, use some generic name like hslambda-api
+
Configure routes -> Method is ANY, the Resource path is $default, the integration target is your lambda function
+
Configure stages -> Stage name is $default and auto-deploy must be enabled
+
Create and get the API’s Invoke URL, something like https://XXXXXXX.execute-api.us-west-2.amazonaws.com
+
+
+
+
Make a .hscfg file in the home dir (/home/ec2-user/) in your Cloud9 env. Make sure you also have this config in your pcluster filesystem. The config file should have these entries:
Here’s a simple h5pyd test to make sure you can retrieve data from the NSRDB/WTK via HSDS. This python example should return a numpy.ndarray object with shape (17520,). Obviously you will need to install python and h5pyd before running this test.
Here are some initial compute cost results and estimates for running reV generation (the largest compute module in reV). All estimates are only for EC2 compute costs based on c5.2xlarge instances at the on-demand price of $0.34 per hour. These numbers are rough estimates! Consider making your own estimates before developing a budget. The EC2 costs could be reduced significantly if running in the EC2 spot market (see how to configure pcluster spot pricing here. The sites_per_worker input in the config_gen.json file will also influence the computational efficiency.
Each “batch” is a dictionary containing “args” and “files”.
+
“args” are the key/value pairs from which the batching combinations will be
+made. Each unique combination of args represents a job. If two args are
+specified with three possible values each, nine jobs will be run.
+
The unique combinations of “args” will be replaced in all files in the
+“files” list. The arg must already exist in the file for the new values to
+be inserted. The replacement is done recursively.
+
Batch jobs will be assigned names based on the args. Accordingly, the name
+field specification should be omitted in all configs.
Before submitting batch jobs, it is sometimes useful to perform a “dry-run”
+which will create all of the batch sub directories without submitting jobs to
+SLURM:
+
reVbatch-c"../config_batch.json"--dry-run
+
+
+
Once you are happy with the dry-run, or if you are confident in your job setup,
+you can submit all batch jobs using the following CLI call:
+
reVbatch-c"../config_batch.json"
+
+
+
If anything goes wrong, you can cancel all batch jobs using the command:
+
reVbatch-c"../config_batch.json"--cancel
+
+
+
New sub directories will be created in the folder with the batch config file
+for each sub job. All job files in the same directory (and sub directories) as
+the batch config file will be copied into the job folders. The reV pipeline
+manager will be executed in each sub directory. The above batch cli command
+can be issues repeatedly to clean up the sub directory status .jsons,
+kick off the next step in the pipeline, or to rerun failed jobs. See the reV
+pipeline execution example for more details on how the pipeline works.
+
You can also have the batch module submit pipeline monitoring background
+processes using the --monitor-background flag as shown below.
+
Please note that the stdout/stderr of the background processes will not be
+captured, so it’s important to set the log_file argument in the pipeline
+config.
All of the batch jobs can be collected into a single file using the multi-year
+collection utility. This utility is not part of the batch pipeline and needs to
+be executed and configured separately. See the config_multi-year.json file
+for details on how to setup this collection step. To execute, use the following
+command:
When running reV on Eagle, it’s only necessary to specify the allocation and
+the walltime. The partition will be chosen automatically and you will be given
+access to the node’s full memory. So a default execution control block in the
+config .json for the standard partition should look like the following:
Note that the way SLURM does memory allocations, if the memory is requested
+explicitly in the config .json and a larger node is received, the user can
+only use memory up to the requested memory value.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/misc/examples.full_pipeline_execution.html b/misc/examples.full_pipeline_execution.html
new file mode 100644
index 000000000..9a1f5b2ed
--- /dev/null
+++ b/misc/examples.full_pipeline_execution.html
@@ -0,0 +1,705 @@
+
+
+
+
+
+
+
+
+ Full Pipeline Execution — reV 0.9.4 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This set of example files demonstrates how to run the full reV pipeline using
+the pipeline manager.
+
The full pipeline can be executed using the following CLI call:
+
reVpipeline-c./config_pipeline.json
+
+
+
You can also use the --monitor flag to continuously monitor the pipeline
+and submit jobs for the next pipeline step when the current pipeline step is
+complete:
+
reVpipeline-c./config_pipeline.json--monitor
+
+
+
The continuous monitoring will stop when the full pipeline completes
+successfully or if any part of a pipeline step fails. The continuous monitoring
+can also be run in a nohup background process by
+adding the --background flag:
It’s important to note that background monitoring will not capture the
+stdout/stderr, so you should set the log_file argument in the pipeline
+config json file to log any important messages from the pipeline module.
+
Finally, if anything goes wrong you can cancel all the pipeline jobs using
+the --cancel flag:
The reV pipeline manager will perform several checks to ensure the following
+input requirements are satisfied. These requirements are necessary to track the
+pipeline status and to pipe i/o through the modules.
+
+
All pipeline modules must have the same output directory.
+
Only one pipeline can be run per output directory.
+
Each module run by the pipeline must have a unique job name (not specifying
+a name in the configs is preferred, and will use the directory name plus a
+suffix for each module).
The pipeline manager will keep a status of jobs that are submitted, running,
+successful, or failed. If any jobs fail in a pipeline step, the pipeline will
+wait until all jobs in that step have completed, then raise a failed message.
+Error messages can be found in the stdout/stderr files belonging to the
+respective failed job(s). The user can re-submit the full pipeline job and
+only the jobs that failed will be re-run. If full modules had previously
+finished successfully, those modules will be skipped.
This example leverages the new SAM marine hydrokinetic (MHK) energy models, the
+NREL hindcast wave data, the MHK cost models in NRWAL, and the integration of
+everything in to reV for large scale spatiotemporal analysis. The configs in
+this example run a batched project that estimates the spatiotemporal capacity
+factors and costs associated with three common wave energy reference models in
+the Atlantic and Pacific Oceans.
+
National Renewable Energy Laboratory. (2020). High Resolution Ocean Surface
+Wave Hindcast (US Wave) Data [data set]. Retrieved from
+https://dx.doi.org/10.15473/1647329.
This example includes configs to run a reV wind analysis for a small test
+extent off the east coast. This example is only meant to demonstrate how to set
+up an offshore wind LCOE analysis using reV + NRWAL. Note that some inputs and
+configurations are purely fictitious and should not be used in a real analysis.
+For example, this test case models the same turbine onshore and offshore. The
+substructure for the offshore turbines are also assumed to always be a floating
+semi-submersible which is not realistic, especially for shallow waters.
The pipeline includes the reV-NRWAL module (replaced the historical
+reV-offshore module), which is run after the generation module. The offshore
+module takes the gross generation (gross capacity factor, set offshore turbine
+losses to zero!) and uses NRWAL to calculate generation losses and LCOE.
+
Example NRWAL configs slightly modified for use with reV can be seen in this
+example. The primary modification for usage in reV is that NRWAL typically
+calculates the grid connection cost with the “grid” equations. Currently, reV
+uses NRWAL to calculate the array and export (to shore) tranmission costs and
+then uses the supply curve transmission cost tables to calculate the grid
+connection costs.
Offshore points are treated identically to onshore points in the supply curve
+(not run here). All resource pixels maintain their source resolution (usually
+the 2km WTK resolution) until the reV aggregation step, where exclusions are
+applied and the data is aggregated up to the supply curve grid. Supply curve
+tranmission cost tables must include transmission costs for offshore supply
+curve points. There is no seperate or special handling of offshore supply curve
+transmission connection.
At its most basic Project Points consists of the resource gid``sandthe
+``SAM configuration file associated it. This can be definited in a variety
+of ways:
+
+
From a project points .csv and a single or dictionary of SAM
+configuration files:
From a pair or pairs of latitude and longitude coordinates and a single
+SAM configuration file (NOTE: access to the resource file to be used
+for reVGen or reVEcon is needed to find the associated resource
+gids):
A geographic region or regions and a single SAM configuration file
+(NOTE: access to the resource file to be used for reVGen or
+reVEcon is needed to find the associated resource gids):
Instead of simple haircut losses, we can add power curve losses.
+The example transformation we will use is a horizontal power curve translation.
+To do so, we must specify the target_losses_percent as well as the name
+of the transformation. We specify both of these options with the
+'reV_power_curve_losses' key in the SAM config.
The reV losses module can be used to compute the power curve shift required to meet
+a target loss value for a single input site. To do this, the user must specify the
+resource at the site as well as the input power curve and target loss info. An
+example of this process is given below
We can also tell reV to stochastically schedule outages based on some
+outage information that we pass in. Specifically, we need to provide the
+outage duration, the number of outages (count), the allowed_months,
+as well as the percentage_of_capacity_lost for each outage.
reV Generation uses PySAM to
+compute technologically specific capcity factor means and profiles. reV Gen
+uses SAM technology terms and input configuration files
The Highly Scalable Distributed Service (HSDS) is a cloud optimized API to
+enable access to .h5 files hosted on AWS. The HSDS software was developed by
+the HDF Group and is hosted on Amazon Web
+Services (AWS) using a combination of EC2 (Elastic Compute) and S3 (Scalable
+Storage Service). You can read more about the HSDS service
+in this slide deck.
+You can use the NREL developer API as the HSDS endpoint for small workloads
+or stand up your own HSDS local server (instructions further below) for an
+enhanced parallelized data experience.
Please note that our HSDS service is for demonstration purposes only. The API in the example above is hosted on an NREL server and will have limits on the amount of data you can access via HSDS. It is common to get an error: OSError:Errorretrievingdata:Noneerrors if you attempt to access too much data or if the server is busy. Here are two references for scaling reV using HSDS and AWS:
Once h5pyd has been installed and configured, rex
+can pull data directly from AWS using HSDS
+To access the resource data used by reV (NSRDB or WTK) you have to turn on the
+hsds flag in the resource handlers:
reV generation (reV.Gen)
+will automatically infer if a file path is locally on disk or from HSDS.
+
Note that for all of these examples, the sam_file input points to files in
+the
+reV test directory
+that may not be copied in your install. You may want to download the relevant
+SAM system configs from that directory and point the sam_file variable to
+the correct filepath on your computer.
By default, a rev_status.json file will be created in the output directory.
+Each node utilized in a job will additionally generate their own status jsons
+upon completion. Each node makes its own status .json to avoid a parallel
+write conflict to the single rev_status.json file.
NOTE: The installation instruction below assume that you have python installed
+on your machine and are using conda
+as your package/environment manager.
+
+
Option 1: Install from PIP (recommended for analysts):
+
+
+
Create a new environment:
condacreate--namerevpython=3.9
+
+
+
+
+
Activate directory:
condaactivaterev
+
+
+
+
+
Install reV:
+
pipinstallNREL-reV or
+
+
NOTE: If you install using conda and want to use HSDS
+you will also need to install h5pyd manually: pipinstallh5pyd
+
+
+
+
+
+
+
+
+
+
Option 2: Clone repo (recommended for developers)
+
+
from home dir, gitclonegit@github.com:NREL/reV.git
+
+
Create reV environment and install package
+
Create a conda env: condacreate-nrev
+
Run the command: condaactivaterev
+
cd into the repo cloned in 1.
+
prior to running pip below, make sure the branch is correct (install
+from main!)
+
Install reV and its dependencies by running:
+pipinstall. (or pipinstall-e. if running a dev branch
+or working on the source code)
+
+
+
+
+
+
Check that reV was installed successfully
+
From any directory, run the following commands. This should return the
+help pages for the CLI’s.
Please cite both the technical paper and the software with the version and
+DOI you used:
+
Maclaurin, Galen J., Nicholas W. Grue, Anthony J. Lopez, Donna M. Heimiller,
+Michael Rossol, Grant Buster, and Travis Williams. 2019. “The Renewable Energy
+Potential (reV) Model: A Geospatial Platform for Technical Potential and Supply
+Curve Modeling.” Golden, Colorado, United States: National Renewable Energy
+Laboratory. NREL/TP-6A20-73067. https://doi.org/10.2172/1563140.
+
Grant Buster, Michael Rossol, Paul Pinchuk, Brandon N Benton, Robert Spencer,
+Mike Bannister, & Travis Williams. (2023).
+NREL/reV: reV 0.8.0 (v0.8.0). Zenodo. https://doi.org/10.5281/zenodo.8247528