diff --git a/docs/architecture/drop_bus_branch.md b/docs/architecture/drop_bus_branch.md new file mode 100644 index 000000000..10376f83c --- /dev/null +++ b/docs/architecture/drop_bus_branch.md @@ -0,0 +1,13 @@ +# Drop Bus-branch support + +In the early days of ToOp, development happened on bus-branch grids (such as UCTE files). However, by now the support for bus/branch grids is mostly gone. Asset topologies require node/breaker information and also the export functionalities are built on node/breaker grids. + +Note that dropping bus/branch support will not remove the ability to optimize bus/branch grids alltogether. It only requires a preprocessing routine that amends the bus/branch grid using common assumptions to a node/breaker grid, e.g. by introducing asset bays manually. + +## Why we didn't do it + +A lot of tests build on bus/branch grids (ieee grids). It would be wise to introduce a general routine that performs bespoke node/breaker amendment for all test grids, so generally all tests run on node/breaker grids. Even though the jax-based tests might not need it everywhere, for comparing against the postprocess runners this would make sense. + +## How to deal with pandapower + +Pandapower tests are fully bus/branch because we didn't implement the asset topologies properly for node/breaker. We might end up in a situation where it would be much easier to either drop bus/branch AND pandapower support or to keep both in. If such a situation arises, it shall first be checked whether pulling in the asset topology support for pandapower might be feasible. If we end up in a stalemate situation, redecide as dropping pandapower completely is not intended at the moment. diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py index 799e3f2e8..1ff6da9ba 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/abstract_runner.py @@ -19,16 +19,15 @@ from copy import deepcopy from pathlib import Path -import pandera.typing as pat from beartype.typing import Optional, TypeAlias, Union from fsspec import AbstractFileSystem from toop_engine_interfaces.asset_topology import RealizedTopology from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Nminus1Definition -from toop_engine_interfaces.node_breaker_update import SwitchUpdateSchema +from toop_engine_interfaces.node_breaker_update import NodeBreakerUpdate from toop_engine_interfaces.stored_action_set import ActionSet -AdditionalActionInfo: TypeAlias = Union[pat.DataFrame[SwitchUpdateSchema], RealizedTopology] +AdditionalActionInfo: TypeAlias = Union[NodeBreakerUpdate, RealizedTopology] class AbstractLoadflowRunner(ABC): diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py index 51137ee8b..aead498e9 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/apply_asset_topo_powsybl.py @@ -600,6 +600,11 @@ def is_node_breaker_grid(net: Network, relevant_station: Optional[str] = None) - ) +def is_bus_branch_grid(net: Network, relevant_station: Optional[str] = None) -> bool: + """Check if the network is in bus-branch format.""" + return not is_node_breaker_grid(net=net, relevant_station=relevant_station) + + def apply_station(net: Network, station: Station) -> Union[pa.typing.DataFrame[SwitchUpdateSchema], RealizedStation]: """Apply a station topology to a powsybl model @@ -624,13 +629,14 @@ def apply_station(net: Network, station: Station) -> Union[pa.typing.DataFrame[S The realized station object which contains the input station plus a diff of switched couplers, reassignments and disconnections or a dataframe of switches that were updated. """ - if is_node_breaker_grid(net=net, relevant_station=station.grid_model_id): - return apply_node_breaker_topology( - net=net, - target_topology=Topology( - topology_id="this_id_will_be_ignored", - stations=[station], - timestamp=datetime.now(), - ), - ) - return apply_station_bus_branch(net=net, station=station) + if is_bus_branch_grid(net=net, relevant_station=station.grid_model_id): + return apply_station_bus_branch(net=net, station=station) + + return apply_node_breaker_topology( + net=net, + target_topology=Topology( + topology_id="this_id_will_be_ignored", + stations=[station], + timestamp=datetime.now(), + ), + ) diff --git a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py index 49a665995..78ec4317a 100644 --- a/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py +++ b/packages/dc_solver_pkg/src/toop_engine_dc_solver/postprocess/postprocess_powsybl.py @@ -34,10 +34,11 @@ fingerprint_current_limits_for_powsybl, get_current_branch_limits_for_powsybl, ) +from toop_engine_dc_solver.export.export import get_node_breaker_updates_from_action_set from toop_engine_dc_solver.postprocess.abstract_runner import AbstractLoadflowRunner, AdditionalActionInfo from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import ( - apply_node_breaker_topology, apply_topology_bus_branch, + is_bus_branch_grid, is_node_breaker_grid, ) from toop_engine_grid_helpers.powsybl.loadflow_parameters import DISTRIBUTED_SLACK @@ -49,6 +50,7 @@ from toop_engine_interfaces.asset_topology_helpers import electrical_components from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Contingency, Nminus1Definition +from toop_engine_interfaces.node_breaker_update import NodeBreakerUpdate from toop_engine_interfaces.stored_action_set import ActionSet logger = structlog.get_logger(__name__) @@ -112,8 +114,56 @@ def get_islanding_contingency_ids(net: Network, nminus1_definition: Nminus1Defin return islanding_contingency_ids -def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> AdditionalActionInfo | None: - """Apply actions to a powsybl network +def apply_node_breaker_updates(net: Network, node_breaker_updates: NodeBreakerUpdate) -> NodeBreakerUpdate: + """Apply raw node-breaker updates to a powsybl network. + + Parameters + ---------- + net : Network + The powsybl network to modify. Will be modified in-place. + node_breaker_updates : NodeBreakerUpdate + Low-level switch and PST updates to apply. + + Returns + ------- + NodeBreakerUpdate + The applied updates - this will always be exactly the input node_breaker_updates and is just added for convenience. + """ + if len(node_breaker_updates.switch_updates): + net.update_switches( + id=node_breaker_updates.switch_updates["grid_model_id"].tolist(), + open=node_breaker_updates.switch_updates["open"].tolist(), + ) + if len(node_breaker_updates.pst_updates): + net.update_phase_tap_changers( + id=node_breaker_updates.pst_updates["grid_model_id"].tolist(), + tap=node_breaker_updates.pst_updates["tap"].tolist(), + ) + return node_breaker_updates + + +def apply_node_breaker_action_set_updates( + net: Network, + actions: list[int], + action_set: ActionSet, + disconnections: Optional[list[int]] = None, + pst_setpoints: Optional[list[int]] = None, +) -> NodeBreakerUpdate | None: + """Translate action-set indices to raw node-breaker updates and apply them.""" + if not len(actions) and not len(disconnections or []) and not len(pst_setpoints or []): + return None + + node_breaker_updates = get_node_breaker_updates_from_action_set( + action_set=action_set, + actions=actions, + disconnections=disconnections, + pst_setpoints=pst_setpoints, + ) + return apply_node_breaker_updates(net, node_breaker_updates) + + +def apply_bus_branch_topology(net: Network, actions: list[int], action_set: ActionSet) -> AdditionalActionInfo | None: + """Apply topology actions to a bus-branch powsybl network. Parameters ---------- @@ -128,7 +178,7 @@ def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> A Returns ------- AdditionalActionInfo | None - Additional information about the action, either a DataFrame of switch updates or a RealizedTopology. + Realized topology information for the bus-branch application. Returns None if no actions are provided. """ if not len(actions): @@ -137,16 +187,27 @@ def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> A stations = [action_set.local_actions[action] for action in actions] changed_stations_topo = action_set.starting_topology.model_copy(update={"stations": stations}) - if is_node_breaker_grid(net, stations[0].grid_model_id): - additional_info = apply_node_breaker_topology(net, changed_stations_topo) - else: - additional_info = apply_topology_bus_branch(net, changed_stations_topo) + return apply_topology_bus_branch(net, changed_stations_topo) + + +def apply_topology(net: Network, actions: list[int], action_set: ActionSet) -> AdditionalActionInfo | None: + """Apply topology actions to a powsybl network. + + Node-breaker grids are translated to raw switch updates before application. The + legacy bus-branch path stays isolated in ``apply_bus_branch_topology``. + """ + if not len(actions): + return None + + relevant_station = action_set.local_actions[actions[0]].grid_model_id + if is_bus_branch_grid(net, relevant_station=relevant_station): + return apply_bus_branch_topology(net, actions, action_set) - return additional_info + return apply_node_breaker_action_set_updates(net, actions, action_set) -def apply_disconnections(net: Network, disconnections: list[int], action_set: ActionSet) -> None: - """Apply static disconnections to a powsybl network. +def apply_bus_branch_disconnections(net: Network, disconnections: list[int], action_set: ActionSet) -> None: + """Apply static disconnections to a bus-branch powsybl network. Works by removing the elements from the network. @@ -163,9 +224,7 @@ def apply_disconnections(net: Network, disconnections: list[int], action_set: Ac Raises ------ RuntimeError - If an element could not be disconnected - - + If an element could not be disconnected. """ for disconnection in disconnections: assert disconnection < len(action_set.disconnectable_branches), "Disconnection index out of range" @@ -174,8 +233,8 @@ def apply_disconnections(net: Network, disconnections: list[int], action_set: Ac logger.warning(f"Failed to disconnect element {elem.id} of type {elem.kind} at index {disconnection}") -def apply_pst_setpoints(net: Network, pst_setpoints: list[int], action_set: ActionSet) -> None: - """Apply phase shift tap setpoints to a powsybl network. +def apply_bus_branch_pst_setpoints(net: Network, pst_setpoints: list[int], action_set: ActionSet) -> None: + """Apply phase shift tap setpoints to a bus-branch powsybl network. Works by setting the tap position of the controllable PSTs in the network to the given setpoints. @@ -442,12 +501,26 @@ def _apply_requested_changes( None """ self.last_action_info = None - if len(actions): - self.last_action_info = apply_topology(net, actions, self.action_set) - if len(disconnections): - apply_disconnections(net, disconnections, self.action_set) - if pst_setpoints is not None and len(pst_setpoints): - apply_pst_setpoints(net, pst_setpoints, self.action_set) + if not len(actions) and not len(disconnections) and not (pst_setpoints is not None and len(pst_setpoints)): + return + + assert self.action_set is not None, "Action set must be stored before applying topology changes" + if is_bus_branch_grid(net, relevant_station=self._get_relevant_station_id()): + if len(actions): + self.last_action_info = apply_bus_branch_topology(net, actions, self.action_set) + if len(disconnections): + apply_bus_branch_disconnections(net, disconnections, self.action_set) + if pst_setpoints is not None and len(pst_setpoints): + apply_bus_branch_pst_setpoints(net, pst_setpoints, self.action_set) + return + + self.last_action_info = apply_node_breaker_action_set_updates( + net, + actions, + self.action_set, + disconnections, + pst_setpoints, + ) def build_topology_network( self, @@ -458,12 +531,20 @@ def build_topology_network( """Build a fresh network copy with the requested topology applied.""" assert self.net is not None, "Base grid must be loaded before building a topology network" net = deepcopy(self.net) - if len(actions): - _ = apply_topology(net, actions, self.action_set) - if len(disconnections): - apply_disconnections(net, disconnections, self.action_set) - if pst_setpoints is not None and len(pst_setpoints): - apply_pst_setpoints(net, pst_setpoints, self.action_set) + if not len(actions) and not len(disconnections) and not (pst_setpoints is not None and len(pst_setpoints)): + return net + + assert self.action_set is not None, "Action set must be stored before building a topology network" + if is_bus_branch_grid(net, relevant_station=self._get_relevant_station_id()): + if len(actions): + _ = apply_bus_branch_topology(net, actions, self.action_set) + if len(disconnections): + apply_bus_branch_disconnections(net, disconnections, self.action_set) + if pst_setpoints is not None and len(pst_setpoints): + apply_bus_branch_pst_setpoints(net, pst_setpoints, self.action_set) + return net + + _ = apply_node_breaker_action_set_updates(net, actions, self.action_set, disconnections, pst_setpoints) return net @overrides diff --git a/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py b/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py index 1450383ac..43273a42c 100644 --- a/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py +++ b/packages/dc_solver_pkg/tests/postprocessing/test_postprocess_powsybl.py @@ -35,7 +35,7 @@ from toop_engine_dc_solver.jax.types import ActionIndexComputations, NodalInjOptimResults, NodalInjStartOptions from toop_engine_dc_solver.postprocess.postprocess_powsybl import ( PowsyblRunner, - apply_disconnections, + apply_bus_branch_disconnections, apply_topology, compute_cross_coupler_flows, ) @@ -89,7 +89,7 @@ def test_apply_topology(preprocessed_powsybl_data_folder: Path) -> None: assert dc_res[0].status == pypowsybl.loadflow.ComponentStatus.CONVERGED -def test_apply_disconnections(preprocessed_powsybl_data_folder: Path) -> None: +def test_apply_bus_branch_disconnections(preprocessed_powsybl_data_folder: Path) -> None: net = pypowsybl.network.load(preprocessed_powsybl_data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"]) assert net.get_branches().connected1.iloc[0] @@ -101,7 +101,7 @@ def test_apply_disconnections(preprocessed_powsybl_data_folder: Path) -> None: disconnections = [0] - apply_disconnections(net, disconnections, action_set) + apply_bus_branch_disconnections(net, disconnections, action_set) assert not net.get_branches().connected1.iloc[0] assert not net.get_branches().connected2.iloc[0] @@ -423,8 +423,8 @@ def test_powsybl_runner_reuses_branch_limit_cache_for_contingency_only_updates( net = pypowsybl.network.load(preprocessed_powsybl_data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"]) monkeypatch.setattr( - "toop_engine_dc_solver.postprocess.postprocess_powsybl.is_node_breaker_grid", - lambda *_args, **_kwargs: False, + "toop_engine_dc_solver.postprocess.postprocess_powsybl.is_bus_branch_grid", + lambda *_args, **_kwargs: True, ) monkeypatch.setattr( "toop_engine_dc_solver.postprocess.postprocess_powsybl.get_current_branch_limits_for_powsybl", diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py index 1e25121f7..cf8f85148 100644 --- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py +++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/ac/scoring_functions.py @@ -12,7 +12,6 @@ from dataclasses import dataclass import numpy as np -import pandas as pd import polars as pl import structlog from beartype.typing import Collection, Optional @@ -26,6 +25,7 @@ from toop_engine_interfaces.loadflow_results import ConvergenceStatus from toop_engine_interfaces.loadflow_results_polars import LoadflowResultsPolars from toop_engine_interfaces.nminus1_definition import Nminus1Definition +from toop_engine_interfaces.node_breaker_update import NodeBreakerUpdate from toop_engine_topology_optimizer.ac.evolution_functions import INF_FITNESS, get_contingency_indices_from_ids from toop_engine_topology_optimizer.ac.storage import ACOptimTopology from toop_engine_topology_optimizer.ac.types import EarlyStoppingStageResult, RunnerGroup, TopologyScoringResult @@ -167,8 +167,8 @@ def extract_switching_distance(additional_info: AdditionalActionInfo) -> int: """ if isinstance(additional_info, RealizedTopology): return len(additional_info.reassignment_diff) - if isinstance(additional_info, pd.DataFrame): - return len(additional_info) + if isinstance(additional_info, NodeBreakerUpdate): + return len(additional_info.switch_updates) raise ValueError("Unknown format for additional info") diff --git a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py index 4948db29f..a43e775c8 100644 --- a/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py +++ b/packages/topology_optimizer_pkg/src/toop_engine_topology_optimizer/benchmark/benchmark_utils.py @@ -35,6 +35,7 @@ from toop_engine_dc_solver.export.export import get_node_breaker_updates_from_action_set from toop_engine_dc_solver.jax.types import StaticInformation from toop_engine_dc_solver.postprocess.abstract_runner import AbstractLoadflowRunner, AdditionalActionInfo +from toop_engine_dc_solver.postprocess.apply_asset_topo_powsybl import is_bus_branch_grid from toop_engine_dc_solver.postprocess.postprocess_pandapower import ( PandapowerRunner, ) @@ -46,12 +47,7 @@ ) from toop_engine_dc_solver.postprocess.postprocess_powsybl import ( PowsyblRunner, -) -from toop_engine_dc_solver.postprocess.postprocess_powsybl import ( - apply_disconnections as powsybl_apply_disconnections, -) -from toop_engine_dc_solver.postprocess.postprocess_powsybl import ( - apply_topology as powsybl_apply_topology, + apply_node_breaker_updates, ) from toop_engine_dc_solver.preprocess.convert_to_jax import load_grid from toop_engine_grid_helpers.powsybl.powsybl_helpers import load_lf_params_from_fs @@ -466,7 +462,12 @@ def apply_topology_and_save( is_pandapower_grid: bool = False, ) -> pypowsybl.network.Network: """ - Apply a set of topology actions and disconnections to the grid, then saves the modified network to a specified path. + Apply topology updates to a grid and save the modified network. + + Pandapower grids continue to use the legacy bus-branch helpers. Powsybl grids, + however, must be node-breaker grids and are updated through the canonical + ``NodeBreakerUpdate`` representation. Bus-branch powsybl grids are explicitly + unsupported in this method. Parameters ---------- @@ -481,28 +482,52 @@ def apply_topology_and_save( save_path : Path Path where the modified network will be saved. is_pandapower_grid : bool, optional - Whether the grid is a pandapower grid. Default is False (powsybl grid). + Whether the input grid is a pandapower grid. Returns ------- pypowsybl.network.Network - The modified network after applying the topology changes and disconnections. + The modified network. + + Raises + ------ + ValueError + If the loaded powsybl grid is in bus-branch format. """ - # Load grid - base_net = pypowsybl.network.load(grid_path) if not is_pandapower_grid else pandapower.from_json(grid_path) + filtered_actions = [action for action in actions if action is not None] + filtered_disconnections = [disconnection for disconnection in disconnections if disconnection is not None] - # Apply topology and disconnections if is_pandapower_grid: - # Pandapower version returns (net, realized_topology) - modified_net, _ = pandapower_apply_topology(net=base_net, actions=actions, action_set=action_set) - modified_net = pandapower_apply_disconnections(modified_net, disconnections=disconnections, action_set=action_set) - else: - # Powsybl version modifies in-place and returns AdditionalActionInfo - _ = powsybl_apply_topology(net=base_net, actions=actions, action_set=action_set) - powsybl_apply_disconnections(base_net, disconnections=disconnections, action_set=action_set) - modified_net = base_net + base_net = pandapower.from_json(grid_path) + modified_net, _ = pandapower_apply_topology(net=base_net, actions=filtered_actions, action_set=action_set) + modified_net = pandapower_apply_disconnections( + modified_net, + disconnections=filtered_disconnections, + action_set=action_set, + ) + modified_net.to_json(save_path) + logger.info(f"Saved modified network to {save_path}") + return modified_net + + base_net = pypowsybl.network.load(grid_path) + relevant_station = None + for topology in (action_set.starting_topology, action_set.simplified_starting_topology): + if topology is not None and len(topology.stations): + relevant_station = topology.stations[0].grid_model_id + break + + if is_bus_branch_grid(base_net, relevant_station=relevant_station): + raise ValueError("apply_topology_and_save is incompatible with bus-branch powsybl grids") + + node_breaker_updates = get_node_breaker_updates_from_action_set( + action_set=action_set, + actions=filtered_actions, + disconnections=filtered_disconnections, + ) + _ = apply_node_breaker_updates(base_net, node_breaker_updates) + modified_net = base_net - modified_net.save(save_path) if not is_pandapower_grid else modified_net.to_json(save_path) + modified_net.save(save_path) logger.info(f"Saved modified network to {save_path}") return modified_net diff --git a/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py b/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py index 4c81cf2fe..91cd4712d 100644 --- a/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py +++ b/packages/topology_optimizer_pkg/tests/benchmark/test_benchmark_utils.py @@ -9,15 +9,19 @@ import multiprocessing as mp from pathlib import Path +import pandapower import pytest from omegaconf import DictConfig +from toop_engine_interfaces.folder_structure import PREPROCESSING_PATHS from toop_engine_interfaces.messages.preprocess.preprocess_commands import ( CgmesImporterParameters, PreprocessParameters, UcteImporterParameters, ) +from toop_engine_interfaces.stored_action_set import load_action_set from toop_engine_topology_optimizer.benchmark.benchmark_utils import ( PipelineConfig, + apply_topology_and_save, get_paths, prepare_importer_parameters, run_pipeline, @@ -225,6 +229,43 @@ def _get_serialized_topology_fitness(topology: dict) -> float: assert ac_metrics["dc_info"]["disconnections"] == (expected_topology.get("disconnections") or []) +def test_apply_topology_and_save_rejects_bus_branch_powsybl_grids(grid_folder: Path, tmp_path: Path) -> None: + action_set = load_action_set( + grid_folder / "case57" / PREPROCESSING_PATHS["action_set_file_path"], + grid_folder / "case57" / PREPROCESSING_PATHS["action_set_diff_path"], + ) + + with pytest.raises(ValueError, match="bus-branch powsybl grids"): + apply_topology_and_save( + grid_path=grid_folder / "case57" / PREPROCESSING_PATHS["grid_file_path_powsybl"], + actions=[], + disconnections=[], + action_set=action_set, + save_path=tmp_path / "modified_network.xiidm", + ) + + +def test_apply_topology_and_save_supports_pandapower_grids(grid_folder: Path, tmp_path: Path) -> None: + action_set = load_action_set( + grid_folder / "case14" / PREPROCESSING_PATHS["action_set_file_path"], + grid_folder / "case14" / PREPROCESSING_PATHS["action_set_diff_path"], + ) + + save_path = tmp_path / "modified_network.json" + modified_net = apply_topology_and_save( + grid_path=grid_folder / "case14" / PREPROCESSING_PATHS["grid_file_path_pandapower"], + actions=[], + disconnections=[], + action_set=action_set, + save_path=save_path, + is_pandapower_grid=True, + ) + + assert save_path.exists() + reloaded_net = pandapower.from_json(save_path) + assert len(modified_net.bus) == len(reloaded_net.bus) + + def test_run_pipeline_no_optimization_stage(pipeline_and_configs, preprocessing_parameters): pipeline_cfg, dc_cfg, ac_cfg = pipeline_and_configs pipeline_cfg = PipelineConfig(**pipeline_cfg)