Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reinforced Transmission Within State #181

Merged
merged 18 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ as your package/environment manager.
From your home directory ``/home/{user}/`` or another directory that you have permissions in, run the command ``git clone git@github.com:NREL/reVX.git`` and then go into your cloned repository: ``cd reVX``

#. Install reVX:
1) Follow the installation commands installation process that we use for our automated test suite `here <https://github.com/NREL/reVX/blob/main/.github/workflows/pull_request_tests.yml#L31-L36>`_. Make sure that you call ``pip install -e .`` from within the cloned repository directory e.g. ``/home/{user}/reVX/``
1) Follow the installation commands installation process that we use for our automated test suite `here <https://github.com/NREL/reVX/blob/main/.github/workflows/pull_request_tests.yml#L31-L34>`_. Make sure that you call ``pip install -e .`` from within the cloned repository directory e.g. ``/home/{user}/reVX/``

- NOTE: If you install using pip and want to run `exclusion setbacks <https://nrel.github.io/reVX/_cli/reVX.setbacks.setbacks.html>`_ you will need to install rtree manually:
* ``conda install rtree``
Expand Down
16 changes: 16 additions & 0 deletions reVX/config/least_cost_xmission.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ def barrier_mult(self):
"""
return self.get('barrier_mult', self._default_barrier_mult)

@property
def allow_connections_within_states(self):
"""
Boolean flag to allow supple curve points to connect to
substations outside their BA but within their own state.
"""
return self.get("allow_connections_within_states", False)


class LeastCostPathsConfig(AnalysisConfig):
"""Config framework for Least Cost Paths"""
Expand Down Expand Up @@ -306,6 +314,14 @@ def barrier_mult(self):
"""
return self.get('barrier_mult', self._default_barrier_mult)

@property
def allow_connections_within_states(self):
"""
Boolean flag to allow substations to connect to endpoints
outside their BA but within their own state.
"""
return self.get("allow_connections_within_states", False)

@property
def save_paths(self):
"""
Expand Down
4 changes: 3 additions & 1 deletion reVX/least_cost_xmission/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ In this methodology, total interconnection costs are comprised of two components
- Each Balancing Area has exactly one "Network Node" (typically a city or highly populated area)
- SC points **may only connect to substations**
- Substations that a SC point connects to **must be in the same Balancing Area as the SC point**
- This assumption can be relaxed to allow connections within the same state.
- Reinforcement costs are calculated based on the distance between the substation a SC point connected to and the Network Node in that Balancing Area
- The path used to calculate reinforcement costs is traced along existing transmission lines **for as long as possible**.
- The reinforcement cost is taken to be half (50%) of the total greenfield cost of the transmission line being traced. If a reinforcement path traces along multiple transmission lines, the corresponding greenfield costs are used for each segment. If multiple transmission lines are available in a single raster pixel, the cost for the highest-voltage line is used. Wherever there is no transmission line, a default greenfield cost assumption (specified by the user; typically 230 kV) is used.
Expand Down Expand Up @@ -149,7 +150,7 @@ Next, compute the reinforcement paths on multiple nodes. Use the file below as a
}
```

Note that we are specifying ``"capacity_class": "400"`` to use the 230 kV (400MW capacity) greenfield costs for portions of the reinforcement paths that do no have existing transmission. If you would like to save the reinforcement path geometries, simply add `"save_paths": true` to the file, but note that this may increase your data product size significantly.
Note that we are specifying ``"capacity_class": "400"`` to use the 230 kV (400MW capacity) greenfield costs for portions of the reinforcement paths that do no have existing transmission. If you would like to save the reinforcement path geometries, simply add `"save_paths": true` to the file, but note that this may increase your data product size significantly. If you would like to allow substations to connect to endpoints within the same state, add `"allow_connections_within_states": true` to the file.

After putting together your config file, simply call

Expand Down Expand Up @@ -183,6 +184,7 @@ You should now have a file containing all of the reinforcement costs for the sub
"name": "least_cost_transmission_1000MW"
}
```
If you would like to allow supply curve points to connect to substations within the same state, add `"allow_connections_within_states": true` to the file.

Kickoff the execution using the following command:

Expand Down
51 changes: 44 additions & 7 deletions reVX/least_cost_xmission/least_cost_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def end_features(self):

Returns
-------
geopandas.GeoDataFrame
pandas.DataFrame
"""
end_features = self._features.drop(index=self._start_feature_ind)
end_features['start_index'] = self._start_feature_ind
Expand Down Expand Up @@ -325,7 +325,7 @@ def process_least_cost_paths(self, capacity_class, barrier_mult=100,
end_features = end_features.drop(columns=['row', 'col'],
errors="ignore")
lcp = future.result()
lcp = pd.concat((end_features, lcp), axis=1)
lcp = pd.concat((lcp, end_features), axis=1)
least_cost_paths.append(lcp)
logger.debug('Least cost path {} of {} complete!'
.format(i + 1, len(futures)))
Expand All @@ -344,7 +344,7 @@ def process_least_cost_paths(self, capacity_class, barrier_mult=100,
save_paths=save_paths)
end_features = self.end_features.drop(columns=['row', 'col'],
errors="ignore")
lcp = pd.concat((end_features, lcp), axis=1)
lcp = pd.concat((lcp, end_features), axis=1)
least_cost_paths.append(lcp)

logger.debug('Least cost path {} of {} complete!'
Expand Down Expand Up @@ -534,14 +534,15 @@ def process_least_cost_paths(self, capacity_class, barrier_mult=100,
barrier_mult=barrier_mult,
save_paths=save_paths)
feats = self._features.drop(columns=['row', 'col'])
least_cost_paths = pd.concat((feats, lcp), axis=1)
least_cost_paths = pd.concat((lcp, feats), axis=1)

return least_cost_paths.drop("index", axis="columns", errors="ignore")

@classmethod
def run(cls, cost_fpath, features_fpath, network_nodes_fpath,
transmission_lines_fpath, capacity_class, xmission_config=None,
barrier_mult=100, indices=None, save_paths=False):
barrier_mult=100, indices=None,
allow_connections_within_states=False, save_paths=False):
"""
Find the reinforcement line paths between the network node and
the substations for the given tie-line capacity class
Expand Down Expand Up @@ -580,6 +581,10 @@ def run(cls, cost_fpath, features_fpath, network_nodes_fpath,
max_workers : int, optional
Number of workers to use for processing. If 1 run in serial,
if ``None`` use all available cores. By default, ``None``.
allow_connections_within_states : bool, optional
Allow substations to connect to network nodes outside of
their own BA, as long as all connections stay within the
same state. By default, ``False``.
save_paths : bool, optional
Flag to save reinforcement line path as a multi-line
geometry. By default, ``False``.
Expand Down Expand Up @@ -621,7 +626,15 @@ def run(cls, cost_fpath, features_fpath, network_nodes_fpath,
network_node = (network_nodes.iloc[index:index + 1]
.reset_index(drop=True))
ba_str = network_node["ba_str"].values[0]
node_substations = substations[substations["ba_str"] == ba_str]
if allow_connections_within_states:
state_nodes = network_nodes[network_nodes["state"]
== network_node["state"].values[0]]
allowed_bas = set(state_nodes["ba_str"])
else:
allowed_bas = {ba_str}

node_substations = substations[substations["ba_str"]
.isin(allowed_bas)]
node_substations = node_substations.reset_index(drop=True)
logger.debug('Working on {} substations in BA {}'
.format(len(node_substations), ba_str))
Expand All @@ -638,7 +651,8 @@ def run(cls, cost_fpath, features_fpath, network_nodes_fpath,
logger.info('{} paths were computed in {:.4f} hours'
.format(len(least_cost_paths), (time.time() - ts) / 3600))

return pd.concat(least_cost_paths, ignore_index=True)
costs = pd.concat(least_cost_paths, ignore_index=True)
return min_reinforcement_costs(costs)


def _rasterize_transmission(transmission_lines, xmission_config, cost_shape,
Expand Down Expand Up @@ -675,3 +689,26 @@ def _rasterize_transmission_layer(transmission_lines, cost_shape,
fill=0, transform=cost_transform)

return out


def min_reinforcement_costs(table):
"""Filter table down to cheapest reinforcement per substation.

Parameters
----------
table : pd.DataFrame | gpd.GeoDataFrame
Table containing costs for reinforced transmission. Must contain
a `gid` column identifying each substation with its own unique
ID and a `reinforcement_cost_per_mw` column with the
reinforcement costs to minimize.

Returns
-------
pd.DataFrame | gpd.GeoDataFrame
Table with a single entry for each `gid` with the least
`reinforcement_cost_per_mw`.
"""

grouped = table.groupby('gid')
table = table.loc[grouped["reinforcement_cost_per_mw"].idxmin()]
return table.reset_index(drop=True)
20 changes: 15 additions & 5 deletions reVX/least_cost_xmission/least_cost_paths_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def run_local(ctx, config):
start_index=0, step_index=1,
barrier_mult=config.barrier_mult,
max_workers=config.execution_control.max_workers,
state_connections=config.allow_connections_within_states,
save_paths=config.save_paths,
out_dir=config.dirout,
log_dir=config.log_directory,
Expand Down Expand Up @@ -163,6 +164,9 @@ def from_config(ctx, config, verbose):
show_default=True, default=None,
help=("Number of workers to use for processing, if 1 run in "
"serial, if None use all available cores"))
@click.option('--state_connections', '-acws', is_flag=True,
help='Flag to allow substations ot connect to any endpoints '
'within their state. Default is not verbose.')
@click.option('--save_paths', '-paths', is_flag=True,
help="Flag to save least cost path as a multi-line geometry")
@click.option('--out_dir', '-o', type=STR, default='./',
Expand All @@ -176,7 +180,8 @@ def from_config(ctx, config, verbose):
@click.pass_context
def local(ctx, cost_fpath, features_fpath, capacity_class, network_nodes_fpath,
transmission_lines_fpath, xmission_config, start_index, step_index,
barrier_mult, max_workers, save_paths, out_dir, log_dir, verbose):
barrier_mult, max_workers, state_connections, save_paths, out_dir,
log_dir, verbose):
"""
Run Least Cost Paths on local hardware
"""
Expand All @@ -197,14 +202,16 @@ def local(ctx, cost_fpath, features_fpath, capacity_class, network_nodes_fpath,
features = gpd.read_file(network_nodes_fpath)
features, *__ = LeastCostPaths._map_to_costs(cost_fpath, features)
indices = features.index[start_index::step_index]
kwargs = {"xmission_config": xmission_config,
"barrier_mult": barrier_mult,
"indices": indices,
"allow_connections_within_states": state_connections,
"save_paths": save_paths}
least_costs = ReinforcementPaths.run(cost_fpath, features_fpath,
network_nodes_fpath,
transmission_lines_fpath,
capacity_class,
xmission_config=xmission_config,
barrier_mult=barrier_mult,
indices=indices,
save_paths=save_paths)
**kwargs)
else:
features = gpd.read_file(features_fpath)
features, *__ = LeastCostPaths._map_to_costs(cost_fpath, features)
Expand Down Expand Up @@ -328,6 +335,9 @@ def get_node_cmd(config, start_index=0):
'-log {}'.format(SLURM.s(config.log_directory)),
]

if config.allow_connections_within_states:
args.append('-acws')

if config.save_paths:
args.append('-paths')

Expand Down
Loading
Loading