Skip to content

Topology Perturbations

This module provides classes for generating perturbed network topologies.

TopologyGenerator

Bases: ABC

Abstract base class for generating perturbed network topologies.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class TopologyGenerator(ABC):
    """Abstract base class for generating perturbed network topologies."""

    def __init__(self) -> None:
        """Initialize the topology generator."""
        pass

    @abstractmethod
    def generate(
        self,
        net: pp.pandapowerNet,
    ) -> Union[Generator[pp.pandapowerNet, None, None], List[pp.pandapowerNet]]:
        """Generate perturbed topologies.

        Args:
            net: The power network to perturb.

        Yields:
            A perturbed network topology.
        """
        pass
__init__()

Initialize the topology generator.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
15
16
17
def __init__(self) -> None:
    """Initialize the topology generator."""
    pass
generate(net) abstractmethod

Generate perturbed topologies.

Parameters:

Name Type Description Default
net pandapowerNet

The power network to perturb.

required

Yields:

Type Description
Union[Generator[pandapowerNet, None, None], List[pandapowerNet]]

A perturbed network topology.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@abstractmethod
def generate(
    self,
    net: pp.pandapowerNet,
) -> Union[Generator[pp.pandapowerNet, None, None], List[pp.pandapowerNet]]:
    """Generate perturbed topologies.

    Args:
        net: The power network to perturb.

    Yields:
        A perturbed network topology.
    """
    pass

NoPerturbationGenerator

Bases: TopologyGenerator

Generator that yields the original network without any perturbations.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class NoPerturbationGenerator(TopologyGenerator):
    """Generator that yields the original network without any perturbations."""

    def generate(
        self,
        net: pp.pandapowerNet,
    ) -> Generator[pp.pandapowerNet, None, None]:
        """Yield the original network without any perturbations.

        Args:
            net: The power network.

        Yields:
            The original power network.
        """
        yield net
generate(net)

Yield the original network without any perturbations.

Parameters:

Name Type Description Default
net pandapowerNet

The power network.

required

Yields:

Type Description
pandapowerNet

The original power network.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
38
39
40
41
42
43
44
45
46
47
48
49
50
def generate(
    self,
    net: pp.pandapowerNet,
) -> Generator[pp.pandapowerNet, None, None]:
    """Yield the original network without any perturbations.

    Args:
        net: The power network.

    Yields:
        The original power network.
    """
    yield net

NMinusKGenerator

Bases: TopologyGenerator

Generate perturbed topologies for N-k contingency analysis.

Only considers lines and transformers. Generates ALL possible topologies with at most k components set out of service (lines and transformers).

Only topologies that are feasible (= no unsupplied buses) are yielded.

Attributes:

Name Type Description
k

Maximum number of components to drop.

components_to_drop

List of tuples containing component indices and types.

component_combinations

List of all possible combinations of components to drop.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class NMinusKGenerator(TopologyGenerator):
    """Generate perturbed topologies for N-k contingency analysis.

    Only considers lines and transformers. Generates ALL possible topologies with at most k
    components set out of service (lines and transformers).

    Only topologies that are feasible (= no unsupplied buses) are yielded.

    Attributes:
        k: Maximum number of components to drop.
        components_to_drop: List of tuples containing component indices and types.
        component_combinations: List of all possible combinations of components to drop.
    """

    def __init__(self, k: int, base_net: pp.pandapowerNet) -> None:
        """Initialize the N-k generator.

        Args:
            k: Maximum number of components to drop.
            base_net: The base power network.

        Raises:
            ValueError: If k is 0.
            Warning: If k > 1, as this may result in slow data generation.
        """
        super().__init__()
        if k > 1:
            warnings.warn("k>1. This may result in slow data generation process.")
        if k == 0:
            raise ValueError(
                'k must be greater than 0. Use "none" as argument for the generator_type if you don\'t want to generate any perturbation',
            )
        self.k = k

        # Prepare the list of components to drop
        self.components_to_drop = [(index, "line") for index in base_net.line.index] + [
            (index, "trafo") for index in base_net.trafo.index
        ]

        # Generate all combinations of at most k components
        self.component_combinations = []
        for r in range(self.k + 1):
            self.component_combinations.extend(combinations(self.components_to_drop, r))

        print(
            f"Number of possible topologies with at most {self.k} dropped components: {len(self.component_combinations)}",
        )

    def generate(
        self,
        net: pp.pandapowerNet,
    ) -> Generator[pp.pandapowerNet, None, None]:
        """Generate perturbed topologies by dropping components.

        Args:
            net: The power network.

        Yields:
            A perturbed network topology with at most k components removed.
        """
        for selected_components in self.component_combinations:
            perturbed_topology = copy.deepcopy(net)

            # Separate lines and transformers
            lines_to_drop = [e[0] for e in selected_components if e[1] == "line"]
            trafos_to_drop = [e[0] for e in selected_components if e[1] == "trafo"]

            # Drop selected lines and transformers
            if lines_to_drop:
                perturbed_topology.line.loc[lines_to_drop, "in_service"] = False
            if trafos_to_drop:
                perturbed_topology.trafo.loc[trafos_to_drop, "in_service"] = False

            # Check network feasibility and yield the topology
            if not len(top.unsupplied_buses(perturbed_topology)):
                yield perturbed_topology
__init__(k, base_net)

Initialize the N-k generator.

Parameters:

Name Type Description Default
k int

Maximum number of components to drop.

required
base_net pandapowerNet

The base power network.

required

Raises:

Type Description
ValueError

If k is 0.

Warning

If k > 1, as this may result in slow data generation.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def __init__(self, k: int, base_net: pp.pandapowerNet) -> None:
    """Initialize the N-k generator.

    Args:
        k: Maximum number of components to drop.
        base_net: The base power network.

    Raises:
        ValueError: If k is 0.
        Warning: If k > 1, as this may result in slow data generation.
    """
    super().__init__()
    if k > 1:
        warnings.warn("k>1. This may result in slow data generation process.")
    if k == 0:
        raise ValueError(
            'k must be greater than 0. Use "none" as argument for the generator_type if you don\'t want to generate any perturbation',
        )
    self.k = k

    # Prepare the list of components to drop
    self.components_to_drop = [(index, "line") for index in base_net.line.index] + [
        (index, "trafo") for index in base_net.trafo.index
    ]

    # Generate all combinations of at most k components
    self.component_combinations = []
    for r in range(self.k + 1):
        self.component_combinations.extend(combinations(self.components_to_drop, r))

    print(
        f"Number of possible topologies with at most {self.k} dropped components: {len(self.component_combinations)}",
    )
generate(net)

Generate perturbed topologies by dropping components.

Parameters:

Name Type Description Default
net pandapowerNet

The power network.

required

Yields:

Type Description
pandapowerNet

A perturbed network topology with at most k components removed.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def generate(
    self,
    net: pp.pandapowerNet,
) -> Generator[pp.pandapowerNet, None, None]:
    """Generate perturbed topologies by dropping components.

    Args:
        net: The power network.

    Yields:
        A perturbed network topology with at most k components removed.
    """
    for selected_components in self.component_combinations:
        perturbed_topology = copy.deepcopy(net)

        # Separate lines and transformers
        lines_to_drop = [e[0] for e in selected_components if e[1] == "line"]
        trafos_to_drop = [e[0] for e in selected_components if e[1] == "trafo"]

        # Drop selected lines and transformers
        if lines_to_drop:
            perturbed_topology.line.loc[lines_to_drop, "in_service"] = False
        if trafos_to_drop:
            perturbed_topology.trafo.loc[trafos_to_drop, "in_service"] = False

        # Check network feasibility and yield the topology
        if not len(top.unsupplied_buses(perturbed_topology)):
            yield perturbed_topology

RandomComponentDropGenerator

Bases: TopologyGenerator

Generate perturbed topologies by randomly setting components out of service.

Generates perturbed topologies by randomly setting out of service at most k components among the selected element types. Only topologies that are feasible (= no unsupplied buses) are yielded.

Attributes:

Name Type Description
n_topology_variants

Number of topology variants to generate.

k

Maximum number of components to drop.

components_to_drop

List of tuples containing component indices and types.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
class RandomComponentDropGenerator(TopologyGenerator):
    """Generate perturbed topologies by randomly setting components out of service.

    Generates perturbed topologies by randomly setting out of service at most k components among the selected element types.
    Only topologies that are feasible (= no unsupplied buses) are yielded.

    Attributes:
        n_topology_variants: Number of topology variants to generate.
        k: Maximum number of components to drop.
        components_to_drop: List of tuples containing component indices and types.
    """

    def __init__(
        self,
        n_topology_variants: int,
        k: int,
        base_net: pp.pandapowerNet,
        elements: List[str] = ["line", "trafo", "gen", "sgen"],
    ) -> None:
        """Initialize the random component drop generator.

        Args:
            n_topology_variants: Number of topology variants to generate.
            k: Maximum number of components to drop.
            base_net: The base power network.
            elements: List of element types to consider for dropping.
        """
        super().__init__()
        self.n_topology_variants = n_topology_variants
        self.k = k

        # Create a list of all components that can be dropped
        self.components_to_drop = []
        for element in elements:
            self.components_to_drop.extend(
                [(index, element) for index in base_net[element].index],
            )

    def generate(
        self,
        net: pp.pandapowerNet,
    ) -> Generator[pp.pandapowerNet, None, None]:
        """Generate perturbed topologies by randomly setting components out of service.

        Args:
            net: The power network.

        Yields:
            A perturbed network topology.
        """
        n_generated_topologies = 0

        # Stop after we generated n_topology_variants
        while n_generated_topologies < self.n_topology_variants:
            perturbed_topology = copy.deepcopy(net)

            # draw the number of components to drop from a uniform distribution
            r = np.random.randint(
                1,
                self.k + 1,
            )  # TODO: decide if we want to be able to set 0 components out of service

            # Randomly select r<=k components to drop
            components = tuple(
                np.random.choice(range(len(self.components_to_drop)), r, replace=False),
            )

            # Convert indices back to actual components
            selected_components = tuple(
                self.components_to_drop[idx] for idx in components
            )

            # Separate lines, transformers, generators, and static generators
            lines_to_drop = [e[0] for e in selected_components if e[1] == "line"]
            trafos_to_drop = [e[0] for e in selected_components if e[1] == "trafo"]
            gens_to_turn_off = [e[0] for e in selected_components if e[1] == "gen"]
            sgens_to_turn_off = [e[0] for e in selected_components if e[1] == "sgen"]

            # Drop selected lines and transformers, turn off generators and static generators
            if lines_to_drop:
                perturbed_topology.line.loc[lines_to_drop, "in_service"] = False
            if trafos_to_drop:
                perturbed_topology.trafo.loc[trafos_to_drop, "in_service"] = False
            if gens_to_turn_off:
                perturbed_topology.gen.loc[gens_to_turn_off, "in_service"] = False
            if sgens_to_turn_off:
                perturbed_topology.sgen.loc[sgens_to_turn_off, "in_service"] = False

            # Check network feasibility and yield the topology
            if not len(top.unsupplied_buses(perturbed_topology)):
                yield perturbed_topology
                n_generated_topologies += 1
__init__(n_topology_variants, k, base_net, elements=['line', 'trafo', 'gen', 'sgen'])

Initialize the random component drop generator.

Parameters:

Name Type Description Default
n_topology_variants int

Number of topology variants to generate.

required
k int

Maximum number of components to drop.

required
base_net pandapowerNet

The base power network.

required
elements List[str]

List of element types to consider for dropping.

['line', 'trafo', 'gen', 'sgen']
Source code in gridfm_datakit/perturbations/topology_perturbation.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def __init__(
    self,
    n_topology_variants: int,
    k: int,
    base_net: pp.pandapowerNet,
    elements: List[str] = ["line", "trafo", "gen", "sgen"],
) -> None:
    """Initialize the random component drop generator.

    Args:
        n_topology_variants: Number of topology variants to generate.
        k: Maximum number of components to drop.
        base_net: The base power network.
        elements: List of element types to consider for dropping.
    """
    super().__init__()
    self.n_topology_variants = n_topology_variants
    self.k = k

    # Create a list of all components that can be dropped
    self.components_to_drop = []
    for element in elements:
        self.components_to_drop.extend(
            [(index, element) for index in base_net[element].index],
        )
generate(net)

Generate perturbed topologies by randomly setting components out of service.

Parameters:

Name Type Description Default
net pandapowerNet

The power network.

required

Yields:

Type Description
pandapowerNet

A perturbed network topology.

Source code in gridfm_datakit/perturbations/topology_perturbation.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def generate(
    self,
    net: pp.pandapowerNet,
) -> Generator[pp.pandapowerNet, None, None]:
    """Generate perturbed topologies by randomly setting components out of service.

    Args:
        net: The power network.

    Yields:
        A perturbed network topology.
    """
    n_generated_topologies = 0

    # Stop after we generated n_topology_variants
    while n_generated_topologies < self.n_topology_variants:
        perturbed_topology = copy.deepcopy(net)

        # draw the number of components to drop from a uniform distribution
        r = np.random.randint(
            1,
            self.k + 1,
        )  # TODO: decide if we want to be able to set 0 components out of service

        # Randomly select r<=k components to drop
        components = tuple(
            np.random.choice(range(len(self.components_to_drop)), r, replace=False),
        )

        # Convert indices back to actual components
        selected_components = tuple(
            self.components_to_drop[idx] for idx in components
        )

        # Separate lines, transformers, generators, and static generators
        lines_to_drop = [e[0] for e in selected_components if e[1] == "line"]
        trafos_to_drop = [e[0] for e in selected_components if e[1] == "trafo"]
        gens_to_turn_off = [e[0] for e in selected_components if e[1] == "gen"]
        sgens_to_turn_off = [e[0] for e in selected_components if e[1] == "sgen"]

        # Drop selected lines and transformers, turn off generators and static generators
        if lines_to_drop:
            perturbed_topology.line.loc[lines_to_drop, "in_service"] = False
        if trafos_to_drop:
            perturbed_topology.trafo.loc[trafos_to_drop, "in_service"] = False
        if gens_to_turn_off:
            perturbed_topology.gen.loc[gens_to_turn_off, "in_service"] = False
        if sgens_to_turn_off:
            perturbed_topology.sgen.loc[sgens_to_turn_off, "in_service"] = False

        # Check network feasibility and yield the topology
        if not len(top.unsupplied_buses(perturbed_topology)):
            yield perturbed_topology
            n_generated_topologies += 1