Skip to content

Commit

Permalink
Merge pull request #224 from ie3-institute/to/dev
Browse files Browse the repository at this point in the history
Add graph functionality
  • Loading branch information
t-ober authored Sep 10, 2024
2 parents 621a72f + 819c626 commit b926e9f
Show file tree
Hide file tree
Showing 14 changed files with 144 additions and 68 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- Added `Transformers2WResult` dictionary support [#217](https://github.com/ie3-institute/pypsdm/pull/217)
- `PrimaryData` time series are now protected, introduced `add_time_series` method [#218](https://github.com/ie3-institute/pypsdm/pull/218)
- Added method to create nodes and lines [#223](https://github.com/ie3-institute/pypsdm/pull/223)
- Added graph traversal functionality [#224](https://github.com/ie3-institute/pypsdm/pull/224)

### Changed

Expand Down
84 changes: 84 additions & 0 deletions pypsdm/graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import networkx as nx
from networkx import Graph

if TYPE_CHECKING:
from pypsdm import RawGridContainer


def find_branches(G: Graph, start_node):
visited = set()
visited.add(start_node)
branches = []

def dfs(node, path):
visited.add(node)
path.append(node)

for neighbor in G.neighbors(node):
if neighbor not in visited:
dfs(neighbor, path)

for neighbor in G.neighbors(start_node):
if neighbor not in visited:
path = [start_node]
dfs(neighbor, path)
branches.append(path)

return branches


def find_n_hop_closest_in_slack_direction(
uuid: str, n: int, raw_grid: RawGridContainer, candidates: set[str] | None = None
):
slack_ds = raw_grid.find_slack_downstream()
G = raw_grid.build_networkx_graph()
if candidates is None:
candidates = set(raw_grid.nodes.uuid)
# find djikstra shortest path between uuid and slck_ds
path = nx.shortest_path(G, uuid, slack_ds)
closest = find_n_hop_closest_candidates(n, G, uuid, candidates)
closest = [n for n in closest if n in path]
return closest


def find_n_hop_closest_candidates(n: int, G, uuid, candidates):
"""
Find all nodes within candidates that are n candidate hops away from the given node.
"""
closest = set([uuid])
for _ in range(n):
for node in closest:
closest = closest.union(find_closest_candidates(G, node, candidates))
return closest


def find_closest_candidates(G: nx.Graph, node: str, candidates: set[str]) -> list[str]:
"""
Starting from the node, find the closest nodes in all directions, that are in the
candidates set.
Args:
G: The graph to search in.
node: The node for which to find the closest candidates.
candidates: The set of nodes to search for.
"""
# NOTE: This might be added to pypsdm
res = []
visited = set()

def preorder_dfs(cur: str):
if cur in visited:
return
visited.add(cur)
if cur in candidates and cur != node:
res.append(cur)
return
for n in G.neighbors(cur):
preorder_dfs(n)

preorder_dfs(node)
return res
5 changes: 3 additions & 2 deletions pypsdm/models/gwr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Optional, Tuple, Union

import numpy as np
Expand Down Expand Up @@ -274,8 +275,8 @@ def to_csv(
@classmethod
def from_csv(
cls,
grid_path: str,
result_path: str,
grid_path: str | Path,
result_path: str | Path,
grid_delimiter: str | None = None,
result_delimiter: str | None = None,
primary_data_delimiter: Optional[str] = None,
Expand Down
5 changes: 3 additions & 2 deletions pypsdm/models/input/container/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Optional, Union

from pypsdm.models.enums import (
Expand Down Expand Up @@ -171,7 +172,7 @@ def plot_grid(

def to_csv(
self,
path: str,
path: str | Path,
include_primary_data: bool = True,
mkdirs: bool = False,
delimiter: str = ",",
Expand All @@ -185,7 +186,7 @@ def to_csv(
@classmethod
def from_csv(
cls,
path: str,
path: str | Path,
delimiter: str | None = None,
primary_data_delimiter: Optional[str] = None,
):
Expand Down
3 changes: 2 additions & 1 deletion pypsdm/models/input/container/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dataclasses import replace
from datetime import datetime
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Self, Tuple

from loguru import logger
Expand Down Expand Up @@ -112,7 +113,7 @@ def entity_keys(cls):
@classmethod
def entities_from_csv(
cls,
simulation_data_path: str,
simulation_data_path: str | Path,
simulation_end: datetime | None = None,
grid_container: GridContainer | None = None,
delimiter: str | None = None,
Expand Down
3 changes: 2 additions & 1 deletion pypsdm/models/input/container/participants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Union

import pandas as pd
Expand Down Expand Up @@ -151,7 +152,7 @@ def subset(self, uuids):

@staticmethod
def from_csv(
path: str, delimiter: str | None = None
path: str | Path, delimiter: str | None = None
) -> "SystemParticipantsContainer":
loads = Loads.from_csv(path, delimiter, must_exist=False)
fixed_feed_ins = FixedFeedIns.from_csv(path, delimiter, must_exist=False)
Expand Down
56 changes: 34 additions & 22 deletions pypsdm/models/input/container/raw_grid.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Union

from networkx import Graph

from pypsdm.graph import find_branches
from pypsdm.models.enums import RawGridElementsEnum
from pypsdm.models.input.connector.lines import Lines
from pypsdm.models.input.connector.switches import Switches
Expand Down Expand Up @@ -61,7 +63,7 @@ def get_branches(self, as_graphs=False) -> Union[list[list[str]], list[Graph]]:
raise ValueError("Did not find a slack node!")
slack_connected_node = slack_connected_node.pop()
graph = self.build_networkx_graph()
branches = self._find_branches(graph, slack_connected_node)
branches = find_branches(graph, slack_connected_node)
branches = [[slack_connected_node] + branch for branch in branches]
if as_graphs:
return [graph.subgraph(branch).copy() for branch in branches]
Expand Down Expand Up @@ -110,30 +112,40 @@ def admittance_matrix(self, uuid_to_idx: dict):
transformers_admittance = self.transformers_2_w.admittance_matrix(uuid_to_idx)
return lines_admittance + transformers_admittance

@staticmethod
def _find_branches(G: Graph, start_node):
visited = set()
visited.add(start_node)
branches = []

def dfs(node, path):
visited.add(node)
path.append(node)

for neighbor in G.neighbors(node):
if neighbor not in visited:
dfs(neighbor, path)

for neighbor in G.neighbors(start_node):
if neighbor not in visited:
path = []
dfs(neighbor, path)
branches.append(path)
def find_slack_downstream(self) -> str:
"""
Find the downstream node of the slack node, which is the node on the transformer's
lower voltage side.
"""
from pypsdm import Transformers2W

return branches
slack_node = self.nodes.get_slack_nodes()
if len(slack_node.data) != 1:
raise ValueError("Currently only implemented for singular slack nodes.")
transformers = self.transformers_2_w
slack_transformers = Transformers2W(
transformers.data[
(transformers.node_a.isin(slack_node.uuid.to_list()))
| (transformers.node_b.isin(slack_node.uuid.to_list()))
]
)
slack_connected_node = (
set(slack_transformers.node_a)
.union(slack_transformers.node_b)
.difference(slack_node.uuid)
)
if len(slack_connected_node) > 1:
raise ValueError(
"There are multiple nodes connected to the slack node via a transformer."
)
elif len(slack_connected_node) == 0:
raise ValueError("Did not find a slack node!")
return slack_connected_node.pop()

@classmethod
def from_csv(cls, path: str, delimiter: str | None = None) -> "RawGridContainer":
def from_csv(
cls, path: str | Path, delimiter: str | None = None
) -> "RawGridContainer":
nodes = Nodes.from_csv(path, delimiter)
lines = Lines.from_csv(path, delimiter, must_exist=False)
transformers_2_w = Transformers2W.from_csv(path, delimiter, must_exist=False)
Expand Down
6 changes: 4 additions & 2 deletions pypsdm/models/input/create/grid_elements.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pypsdm.models.input import Nodes, Lines
from pypsdm.models.input.create.utils import create_data
from uuid import uuid4

import pandas as pd

from pypsdm.models.input import Lines, Nodes
from pypsdm.models.input.create.utils import create_data


def create_nodes(data_dict):
return Nodes(create_data(data_dict, create_nodes_data))
Expand Down
4 changes: 2 additions & 2 deletions pypsdm/models/input/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def copy(
@classmethod
def from_csv(
cls: Type[Self],
path: str,
path: str | Path,
delimiter: str | None = None,
must_exist: bool = True,
) -> Self:
Expand All @@ -364,7 +364,7 @@ def from_csv(
@classmethod
def _from_csv(
cls: Type[Self],
path: str,
path: str | Path,
entity: EntitiesEnum,
delimiter: str | None = None,
must_exist: bool = True,
Expand Down
30 changes: 0 additions & 30 deletions pypsdm/models/input/utils.py

This file was deleted.

3 changes: 2 additions & 1 deletion pypsdm/models/result/container/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union

from pypsdm.io.utils import check_filter
Expand Down Expand Up @@ -133,7 +134,7 @@ def concat(self, other: "GridResultContainer", deep: bool = True, keep="last"):
@classmethod
def from_csv(
cls,
simulation_data_path: str,
simulation_data_path: str | Path,
delimiter: str | None = None,
simulation_end: Optional[datetime] = None,
grid_container: Optional[GridContainer] = None,
Expand Down
3 changes: 2 additions & 1 deletion pypsdm/models/result/container/participants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Iterable, Optional, Self, Tuple, Union

import pandas as pd
Expand Down Expand Up @@ -204,7 +205,7 @@ def to_csv(self, path: str, delimiter: str = ",", mkdirs=False):
@classmethod
def from_csv(
cls,
simulation_data_path: str,
simulation_data_path: str | Path,
simulation_end: Optional[datetime] = None,
grid_container: Optional[GridContainer] = None,
delimiter: Optional[str] = None,
Expand Down
3 changes: 2 additions & 1 deletion pypsdm/models/result/container/raw_grid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Optional, Self, Union

from pypsdm.models.enums import RawGridElementsEnum
Expand Down Expand Up @@ -112,7 +113,7 @@ def entity_keys(cls):
@classmethod
def from_csv(
cls,
simulation_data_path: str,
simulation_data_path: str | Path,
simulation_end: Optional[datetime] = None,
grid_container: Optional[GridContainer] = None,
delimiter: Optional[str] = None,
Expand Down
6 changes: 3 additions & 3 deletions pypsdm/processing/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@
def duration_weighted_series(series: Series):
series.sort_index(inplace=True)
values = series[:-1].reset_index(drop=True)
time = (
duration = (
(series.index[1::] - series.index[:-1])
.to_series()
.apply(lambda x: x.total_seconds() / 3600)
.reset_index(drop=True)
)
return pd.concat([values.rename("values"), time.rename("time")], axis=1)
return pd.concat([values.rename("values"), duration.rename("duration")], axis=1)


def weighted_series_sum(weighted_series: DataFrame) -> float:
if len(weighted_series) == 0:
return 0.0
return (weighted_series["values"] * weighted_series["time"]).sum()
return (weighted_series["values"] * weighted_series["duration"]).sum()


def duration_weighted_sum(series: Series) -> float:
Expand Down

0 comments on commit b926e9f

Please sign in to comment.