Skip to content

Importer#

Importer Worker#

toop_engine_importer.worker.worker #

Module contains functions for the kafka communication in the importer repo.

File: worker.py Author: Nico Westerbeck Created: 2024

logger module-attribute #

logger = Logger(__name__)

Args #

Bases: BaseModel

Holds arguments which must be provided at the launch of the worker.

Contains arguments that static for each preprocessing run.

kafka_broker class-attribute instance-attribute #

kafka_broker = 'localhost:9092'

The Kafka broker to connect to.

importer_command_topic class-attribute instance-attribute #

importer_command_topic = 'importer_commands'

The Kafka topic to listen for commands on.

importer_results_topic class-attribute instance-attribute #

importer_results_topic = 'importer_results'

The topic to push results to.

importer_heartbeat_topic class-attribute instance-attribute #

importer_heartbeat_topic = 'importer_heartbeat'

The topic to push heartbeats to.

heartbeat_interval_ms class-attribute instance-attribute #

heartbeat_interval_ms = 1000

The interval in milliseconds to send heartbeats.

idle_loop #

idle_loop(
    consumer, send_heartbeat_fn, heartbeat_interval_ms
)

Start the idle loop of the worker.

This will be running when the worker is currently not preprocessing This will wait until a StartPreprocessingCommand is received and return it. In case a ShutdownCommand is received, the worker will exit with the exit code provided in the command.

PARAMETER DESCRIPTION
consumer

The initialized Kafka consumer to listen for commands on.

TYPE: LongRunningKafkaConsumer

send_heartbeat_fn

A function to call when there were no messages received for a while.

TYPE: Callable

heartbeat_interval_ms

The time to wait for a new command in milliseconds. If no command has been received, a heartbeat will be sent and then the receiver will wait for commands again.

TYPE: int

RETURNS DESCRIPTION
StartOptimizationCommand

The start optimization command to start the optimization run with

Source code in packages/importer_pkg/src/toop_engine_importer/worker/worker.py
def idle_loop(
    consumer: LongRunningKafkaConsumer,
    send_heartbeat_fn: Callable[[], None],
    heartbeat_interval_ms: int,
) -> StartPreprocessingCommand:
    """Start the idle loop of the worker.

    This will be running when the worker is currently not preprocessing
    This will wait until a StartPreprocessingCommand is received and return it. In case a
    ShutdownCommand is received, the worker will exit with the exit code provided in the command.

    Parameters
    ----------
    consumer : LongRunningKafkaConsumer
        The initialized Kafka consumer to listen for commands on.
    send_heartbeat_fn : Callable
        A function to call when there were no messages received for a while.
    heartbeat_interval_ms : int
        The time to wait for a new command in milliseconds. If no command has been received, a
        heartbeat will be sent and then the receiver will wait for commands again.

    Returns
    -------
    StartOptimizationCommand
        The start optimization command to start the optimization run with
    """
    send_heartbeat_fn()
    logger.info("Entering idle loop")
    while True:
        message = consumer.poll(timeout=heartbeat_interval_ms / 1000)

        # Wait timeout exceeded
        if not message:
            send_heartbeat_fn()
            continue

        command = Command.model_validate_json(deserialize_message(message.value()))

        if isinstance(command.command, StartPreprocessingCommand):
            return command.command

        consumer.commit()
        if isinstance(command.command, ShutdownCommand):
            consumer.consumer.close()
            raise SystemExit(command.command.exit_code)

        # If we are here, we received a command that we do not know
        logger.warning(f"Received unknown command, dropping: {command}")

main #

main(
    args,
    producer,
    consumer,
    unprocessed_gridfile_fs,
    processed_gridfile_fs,
    loadflow_result_fs,
)

Start main function of the worker.

PARAMETER DESCRIPTION
args

The arguments to start the worker with.

TYPE: Args

unprocessed_gridfile_fs

A filesystem where the unprocessed gridfiles are stored. The concrete folder to use is determined by the start command, which contains an import location relative to the root of the unprocessed_gridfile_fs.

TYPE: AbstractFileSystem

processed_gridfile_fs

The target filesystem for the preprocessing worker. This contains all processed grid files. During the import job, a new folder import_results.data_folder was created which will be completed with the preprocess call to this function. Internally, only the data folder is passed around as a dirfs. Note that the unprocessed_gridfile_fs is not needed here anymore, as all preprocessing steps that need the unprocessed gridfiles were already done.

TYPE: AbstractFileSystem

loadflow_result_fs

A filesystem where the loadflow results are stored. Loadflows will be stored here using the uuid generation process and passed as a StoredLoadflowReference which contains the subfolder in this filesystem.

TYPE: AbstractFileSystem

producer

The Kafka producer to send results and heartbeats with.

TYPE: Producer

consumer

The Kafka consumer to receive commands with.

TYPE: LongRunningKafkaConsumer

Source code in packages/importer_pkg/src/toop_engine_importer/worker/worker.py
def main(
    args: Args,
    producer: Producer,
    consumer: LongRunningKafkaConsumer,
    unprocessed_gridfile_fs: AbstractFileSystem,
    processed_gridfile_fs: AbstractFileSystem,
    loadflow_result_fs: AbstractFileSystem,
) -> None:
    """Start main function of the worker.

    Parameters
    ----------
    args: Args
        The arguments to start the worker with.
    unprocessed_gridfile_fs: AbstractFileSystem
        A filesystem where the unprocessed gridfiles are stored. The concrete folder to use is determined by the start
        command, which contains an import location relative to the root of the unprocessed_gridfile_fs.
    processed_gridfile_fs: AbstractFileSystem
        The target filesystem for the preprocessing worker. This contains all processed grid files.
        During the import job,  a new folder import_results.data_folder was created
        which will be completed with the preprocess call to this function.
        Internally, only the data folder is passed around as a dirfs.
        Note that the unprocessed_gridfile_fs is not needed here anymore, as all preprocessing steps that need the
        unprocessed gridfiles were already done.
    loadflow_result_fs: AbstractFileSystem
        A filesystem where the loadflow results are stored. Loadflows will be stored here using the uuid generation process
        and passed as a StoredLoadflowReference which contains the subfolder in this filesystem.
    producer: Producer
        The Kafka producer to send results and heartbeats with.
    consumer: LongRunningKafkaConsumer
        The Kafka consumer to receive commands with.
    """
    instance_id = str(uuid4())
    logger.info(f"Starting importer instance {instance_id} with arguments {args}")
    jax.config.update("jax_enable_x64", True)
    jax.config.update("jax_logging_level", "INFO")

    def heartbeat_idle() -> None:
        producer.produce(
            args.importer_heartbeat_topic,
            value=serialize_message(
                PreprocessHeartbeat(
                    idle=True,
                    status_info=None,
                    instance_id=instance_id,
                ).model_dump_json()
            ),
            key=instance_id.encode("utf-8"),
        )
        producer.flush()

    def heartbeat_working(
        stage: PreprocessStage,
        message: Optional[str],
        preprocess_id: str,
        start_time: float,
    ) -> None:
        logger.info(f"Preprocessing stage {stage} for job {preprocess_id} after {time.time() - start_time}s: {message}")
        producer.produce(
            args.importer_heartbeat_topic,
            value=serialize_message(
                PreprocessHeartbeat(
                    idle=False,
                    status_info=PreprocessStatusInfo(
                        preprocess_id=preprocess_id,
                        runtime=time.time() - start_time,
                        stage=stage,
                        message=message,
                    ),
                    instance_id=instance_id,
                ).model_dump_json()
            ),
            key=preprocess_id.encode("utf-8"),
        )
        producer.flush()
        # Ping the command consumer to show we are still alive
        consumer.heartbeat()

    while True:
        command = idle_loop(
            consumer=consumer,
            send_heartbeat_fn=heartbeat_idle,
            heartbeat_interval_ms=args.heartbeat_interval_ms,
        )
        consumer.start_processing()

        start_time = time.time()
        heartbeat_fn = partial(
            heartbeat_working,
            preprocess_id=command.preprocess_id,
            start_time=start_time,
        )
        producer.produce(
            args.importer_results_topic,
            value=serialize_message(
                Result(
                    preprocess_id=command.preprocess_id,
                    runtime=0,
                    result=PreprocessingStartedResult(),
                ).model_dump_json()
            ),
            key=command.preprocess_id.encode(),
        )
        producer.flush()
        heartbeat_fn("start", "Preprocessing run started")

        try:
            importer_results = import_grid_model(
                start_command=command,
                status_update_fn=heartbeat_fn,
                unprocessed_gridfile_fs=unprocessed_gridfile_fs,
                processed_gridfile_fs=processed_gridfile_fs,
            )

            result = preprocess(
                start_command=command,
                import_results=importer_results,
                status_update_fn=heartbeat_fn,
                loadflow_result_fs=loadflow_result_fs,
                processed_gridfile_fs=processed_gridfile_fs,
            )

            heartbeat_fn("end", "Preprocessing run done")

            producer.produce(
                topic=args.importer_results_topic,
                value=serialize_message(
                    Result(
                        preprocess_id=command.preprocess_id,
                        runtime=time.time() - start_time,
                        result=result,
                    ).model_dump_json()
                ),
                key=command.preprocess_id.encode(),
            )
        except Exception as e:
            logger.error(f"Error while processing {command.preprocess_id}", e)
            logger.error(f"Traceback: {traceback.format_exc()}")
            producer.produce(
                topic=args.importer_results_topic,
                value=serialize_message(
                    Result(
                        preprocess_id=command.preprocess_id,
                        runtime=time.time() - start_time,
                        result=ErrorResult(error=str(e)),
                    ).model_dump_json()
                ),
                key=command.preprocess_id.encode(),
            )
        producer.flush()
        consumer.stop_processing()

Contingency from PowerFactory#

toop_engine_importer.contingency_from_power_factory #

Import contingency from PowerFactory.

__all__ module-attribute #

__all__ = [
    "AllGridElementsSchema",
    "ContingencyImportSchemaPowerFactory",
    "ContingencyMatchSchema",
    "get_contingencies_from_file",
    "match_contingencies",
    "power_factory_data_class",
]

AllGridElementsSchema #

Bases: DataFrameModel

A AllGridElementsSchema is a DataFrameModel for all grid elements in the grid model.

The grid model is loaded from the CGMES file in either PyPowsybl or Pandapower.

element_type class-attribute instance-attribute #

element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The grid model type of the contingency. e.g. LINE, SWITCH, BUS, etc.

grid_model_id class-attribute instance-attribute #

grid_model_id = Field(coerce=True, nullable=True)

The grid model id of the contingency. e.g. a CGMES id (cryptic number)

grid_model_name class-attribute instance-attribute #

grid_model_name = Field(coerce=True, nullable=True)

The grid model name of the contingency. e.g. a CGMES name (human readable name)

ContingencyImportSchemaPowerFactory #

Bases: DataFrameModel

A ContingencyImportSchemaPowerFactory is a DataFrameModel defining the expected data of the contingency import.

From PowerFactory: You may find the list of contingencies in the PowerFactory GUI under "Calculation > Contingency Analysis > Show Contingencies...".

index class-attribute instance-attribute #

index = Field(coerce=True, nullable=False)

The unique index of the DataFrame. This index is used as a unique id for the dataframe.

contingency_name class-attribute instance-attribute #

contingency_name = Field(coerce=True, nullable=False)

The id of contingency found in the contingency table. Attribute: "loc_name" of contingency table May be a multi index to group the contingencies together.

contingency_id class-attribute instance-attribute #

contingency_id = Field(coerce=True, nullable=False)

A id for the contingency. This id is used to group the contingencies together. Attribute: "number" of contingency table.

power_factory_grid_model_name class-attribute instance-attribute #

power_factory_grid_model_name = Field(
    coerce=True, nullable=False
)

The name of the grid model element Attribute: "loc_name" of grid model element

power_factory_grid_model_fid class-attribute instance-attribute #

power_factory_grid_model_fid = Field(
    coerce=True, nullable=True
)

The foreign Key of the grid model element Attribute: "for_name" of grid model element Note: True spacing of FID must be kept in the string.

power_factory_grid_model_rdf_id class-attribute instance-attribute #

power_factory_grid_model_rdf_id = Field(
    coerce=True, nullable=True
)

The rdf id (CIM) of the grid model element Attribute: "cimRdfId" of grid model element

comment class-attribute instance-attribute #

comment = Field(nullable=True)

May contain information about the contingency. Leave empty if not needed. Fill if comments or descriptions exist in the contingency table.

power_factory_element_type class-attribute instance-attribute #

power_factory_element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The type of the contingency based on the PowerFactory type. Gives a hint where to look for the contingency.

ContingencyMatchSchema #

Bases: ContingencyImportSchemaPowerFactory, AllGridElementsSchema

A ContingencyMatchSchema is a DataFrameModel for matching the ContingencyImportSchema with the grid model.

ContingencyMatchSchema is a merge of: ContingencyImportSchema.merge( AllGridElementsSchema, how="left", left_on="power_factory_grid_model_rdf_id", right_on="grid_model_id" ) Note: the power_factory_grid_model_rdf_id has a leading underscore and may need modification.

element_type class-attribute instance-attribute #

element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The grid model type of the contingency. e.g. LINE, SWITCH, BUS, etc.

grid_model_id class-attribute instance-attribute #

grid_model_id = Field(coerce=True, nullable=True)

The grid model id of the contingency. e.g. a CGMES id (cryptic number)

grid_model_name class-attribute instance-attribute #

grid_model_name = Field(coerce=True, nullable=True)

The grid model name of the contingency. e.g. a CGMES name (human readable name)

index class-attribute instance-attribute #

index = Field(coerce=True, nullable=False)

The unique index of the DataFrame. This index is used as a unique id for the dataframe.

contingency_name class-attribute instance-attribute #

contingency_name = Field(coerce=True, nullable=False)

The id of contingency found in the contingency table. Attribute: "loc_name" of contingency table May be a multi index to group the contingencies together.

contingency_id class-attribute instance-attribute #

contingency_id = Field(coerce=True, nullable=False)

A id for the contingency. This id is used to group the contingencies together. Attribute: "number" of contingency table.

power_factory_grid_model_name class-attribute instance-attribute #

power_factory_grid_model_name = Field(
    coerce=True, nullable=False
)

The name of the grid model element Attribute: "loc_name" of grid model element

power_factory_grid_model_fid class-attribute instance-attribute #

power_factory_grid_model_fid = Field(
    coerce=True, nullable=True
)

The foreign Key of the grid model element Attribute: "for_name" of grid model element Note: True spacing of FID must be kept in the string.

power_factory_grid_model_rdf_id class-attribute instance-attribute #

power_factory_grid_model_rdf_id = Field(
    coerce=True, nullable=True
)

The rdf id (CIM) of the grid model element Attribute: "cimRdfId" of grid model element

comment class-attribute instance-attribute #

comment = Field(nullable=True)

May contain information about the contingency. Leave empty if not needed. Fill if comments or descriptions exist in the contingency table.

power_factory_element_type class-attribute instance-attribute #

power_factory_element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The type of the contingency based on the PowerFactory type. Gives a hint where to look for the contingency.

get_contingencies_from_file #

get_contingencies_from_file(
    n1_file, delimiter=";", filesystem=None
)

Get the contingencies from the file.

This function reads the contingencies from the file and returns a DataFrame in the ContingencyImportSchema format.

PARAMETER DESCRIPTION
n1_file

The path to the file.

TYPE: Path

delimiter

The delimiter of the file. Default is ";".

TYPE: str DEFAULT: ';'

filesystem

The filesystem to use to read the file. If None, the local filesystem is used.

TYPE: AbstractFileSystem | None DEFAULT: None

RETURNS DESCRIPTION
ContingencyImportSchema

A DataFrame containing the contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def get_contingencies_from_file(
    n1_file: Path, delimiter: str = ";", filesystem: AbstractFileSystem | None = None
) -> ContingencyImportSchemaPowerFactory:
    """Get the contingencies from the file.

    This function reads the contingencies from the file and returns a DataFrame in the
    ContingencyImportSchema format.

    Parameters
    ----------
    n1_file : Path
        The path to the file.
    delimiter : str
        The delimiter of the file. Default is ";".
    filesystem : AbstractFileSystem | None
        The filesystem to use to read the file. If None, the local filesystem is used.

    Returns
    -------
    ContingencyImportSchema
        A DataFrame containing the contingencies.
    """
    if filesystem is None:
        filesystem = LocalFileSystem()
    with filesystem.open(str(n1_file), "r") as f:
        n1_definition = pd.read_csv(f, delimiter=delimiter)
    cond = n1_definition["power_factory_grid_model_name"].isna()
    n1_definition.loc[cond, "power_factory_grid_model_name"] = n1_definition.loc[cond, "contingency_name"]
    n1_definition["contingency_id"] = n1_definition["contingency_id"].astype(int)
    n1_definition = ContingencyImportSchemaPowerFactory.validate(n1_definition)
    return n1_definition

match_contingencies #

match_contingencies(
    n1_definition, all_element_names, match_by_name=True
)

Match the contingencies from the file with the elements in the grid model.

This function matches the contingencies from the file with the elements in the grid model. It first tries to match by index, then by name.

PARAMETER DESCRIPTION
n1_definition

The contingencies from the file.

TYPE: ContingencyImportSchema

all_element_names

The elements in the grid model.

TYPE: AllGridElementsSchema

match_by_name

If True, match by name. Default is True. If False, only match by index.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ContingencyMatchSchema

A DataFrame containing the matched contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def match_contingencies(
    n1_definition: ContingencyImportSchemaPowerFactory,
    all_element_names: AllGridElementsSchema,
    match_by_name: bool = True,
) -> ContingencyMatchSchema:
    """Match the contingencies from the file with the elements in the grid model.

    This function matches the contingencies from the file with the elements in the grid model.
    It first tries to match by index, then by name.

    Parameters
    ----------
    n1_definition : ContingencyImportSchema
        The contingencies from the file.
    all_element_names : AllGridElementsSchema
        The elements in the grid model.
    match_by_name : bool
        If True, match by name. Default is True.
        If False, only match by index.

    Returns
    -------
    ContingencyMatchSchema
        A DataFrame containing the matched contingencies.
    """
    processed_n1_definition = match_contingencies_by_index(n1_definition, all_element_names)
    if match_by_name:
        processed_n1_definition = match_contingencies_by_name(processed_n1_definition, all_element_names)
    return processed_n1_definition

toop_engine_importer.contingency_from_power_factory.power_factory_data_class #

Classes for the contingency list import from PowerFactory.

This importing has the focus on CIM bases grid models. UCTE has not been tested.

Author: Benjamin Petrick Created: 2025-05-13

GridModelTypePowerFactory module-attribute #

GridModelTypePowerFactory = Literal[
    "ElmTr2",
    "ElmLne",
    "ElmGenstat",
    "ElmLod",
    "ElmSym",
    "ElmNec",
    "ElmZpu",
    "ElmTr3",
    "ElmSind",
    "ElmTerm",
    "ElmShnt",
    "ElmVac",
]

GridElementType module-attribute #

GridElementType = Literal[
    "BUS",
    "BUSBAR_SECTION",
    "LINE",
    "SWITCH",
    "TWO_WINDINGS_TRANSFORMER",
    "THREE_WINDINGS_TRANSFORMER",
    "GENERATOR",
    "LOAD",
    "SHUNT_COMPENSATOR",
    "DANGLING_LINE",
    "TIE_LINE",
]

ContingencyImportSchemaPowerFactory #

Bases: DataFrameModel

A ContingencyImportSchemaPowerFactory is a DataFrameModel defining the expected data of the contingency import.

From PowerFactory: You may find the list of contingencies in the PowerFactory GUI under "Calculation > Contingency Analysis > Show Contingencies...".

index class-attribute instance-attribute #

index = Field(coerce=True, nullable=False)

The unique index of the DataFrame. This index is used as a unique id for the dataframe.

contingency_name class-attribute instance-attribute #

contingency_name = Field(coerce=True, nullable=False)

The id of contingency found in the contingency table. Attribute: "loc_name" of contingency table May be a multi index to group the contingencies together.

contingency_id class-attribute instance-attribute #

contingency_id = Field(coerce=True, nullable=False)

A id for the contingency. This id is used to group the contingencies together. Attribute: "number" of contingency table.

power_factory_grid_model_name class-attribute instance-attribute #

power_factory_grid_model_name = Field(
    coerce=True, nullable=False
)

The name of the grid model element Attribute: "loc_name" of grid model element

power_factory_grid_model_fid class-attribute instance-attribute #

power_factory_grid_model_fid = Field(
    coerce=True, nullable=True
)

The foreign Key of the grid model element Attribute: "for_name" of grid model element Note: True spacing of FID must be kept in the string.

power_factory_grid_model_rdf_id class-attribute instance-attribute #

power_factory_grid_model_rdf_id = Field(
    coerce=True, nullable=True
)

The rdf id (CIM) of the grid model element Attribute: "cimRdfId" of grid model element

comment class-attribute instance-attribute #

comment = Field(nullable=True)

May contain information about the contingency. Leave empty if not needed. Fill if comments or descriptions exist in the contingency table.

power_factory_element_type class-attribute instance-attribute #

power_factory_element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The type of the contingency based on the PowerFactory type. Gives a hint where to look for the contingency.

AllGridElementsSchema #

Bases: DataFrameModel

A AllGridElementsSchema is a DataFrameModel for all grid elements in the grid model.

The grid model is loaded from the CGMES file in either PyPowsybl or Pandapower.

element_type class-attribute instance-attribute #

element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The grid model type of the contingency. e.g. LINE, SWITCH, BUS, etc.

grid_model_id class-attribute instance-attribute #

grid_model_id = Field(coerce=True, nullable=True)

The grid model id of the contingency. e.g. a CGMES id (cryptic number)

grid_model_name class-attribute instance-attribute #

grid_model_name = Field(coerce=True, nullable=True)

The grid model name of the contingency. e.g. a CGMES name (human readable name)

ContingencyMatchSchema #

Bases: ContingencyImportSchemaPowerFactory, AllGridElementsSchema

A ContingencyMatchSchema is a DataFrameModel for matching the ContingencyImportSchema with the grid model.

ContingencyMatchSchema is a merge of: ContingencyImportSchema.merge( AllGridElementsSchema, how="left", left_on="power_factory_grid_model_rdf_id", right_on="grid_model_id" ) Note: the power_factory_grid_model_rdf_id has a leading underscore and may need modification.

element_type class-attribute instance-attribute #

element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The grid model type of the contingency. e.g. LINE, SWITCH, BUS, etc.

grid_model_id class-attribute instance-attribute #

grid_model_id = Field(coerce=True, nullable=True)

The grid model id of the contingency. e.g. a CGMES id (cryptic number)

grid_model_name class-attribute instance-attribute #

grid_model_name = Field(coerce=True, nullable=True)

The grid model name of the contingency. e.g. a CGMES name (human readable name)

index class-attribute instance-attribute #

index = Field(coerce=True, nullable=False)

The unique index of the DataFrame. This index is used as a unique id for the dataframe.

contingency_name class-attribute instance-attribute #

contingency_name = Field(coerce=True, nullable=False)

The id of contingency found in the contingency table. Attribute: "loc_name" of contingency table May be a multi index to group the contingencies together.

contingency_id class-attribute instance-attribute #

contingency_id = Field(coerce=True, nullable=False)

A id for the contingency. This id is used to group the contingencies together. Attribute: "number" of contingency table.

power_factory_grid_model_name class-attribute instance-attribute #

power_factory_grid_model_name = Field(
    coerce=True, nullable=False
)

The name of the grid model element Attribute: "loc_name" of grid model element

power_factory_grid_model_fid class-attribute instance-attribute #

power_factory_grid_model_fid = Field(
    coerce=True, nullable=True
)

The foreign Key of the grid model element Attribute: "for_name" of grid model element Note: True spacing of FID must be kept in the string.

power_factory_grid_model_rdf_id class-attribute instance-attribute #

power_factory_grid_model_rdf_id = Field(
    coerce=True, nullable=True
)

The rdf id (CIM) of the grid model element Attribute: "cimRdfId" of grid model element

comment class-attribute instance-attribute #

comment = Field(nullable=True)

May contain information about the contingency. Leave empty if not needed. Fill if comments or descriptions exist in the contingency table.

power_factory_element_type class-attribute instance-attribute #

power_factory_element_type = Field(
    coerce=True, nullable=True, isin=__args__
)

The type of the contingency based on the PowerFactory type. Gives a hint where to look for the contingency.

toop_engine_importer.contingency_from_power_factory.contingency_from_file #

Import contingencies from a file.

This module contains functions to import contingencies from a file and match them with the grid model.

Author: Benjamin Petrick Created: 2025-05-13

logger module-attribute #

logger = Logger(__name__)

get_contingencies_from_file #

get_contingencies_from_file(
    n1_file, delimiter=";", filesystem=None
)

Get the contingencies from the file.

This function reads the contingencies from the file and returns a DataFrame in the ContingencyImportSchema format.

PARAMETER DESCRIPTION
n1_file

The path to the file.

TYPE: Path

delimiter

The delimiter of the file. Default is ";".

TYPE: str DEFAULT: ';'

filesystem

The filesystem to use to read the file. If None, the local filesystem is used.

TYPE: AbstractFileSystem | None DEFAULT: None

RETURNS DESCRIPTION
ContingencyImportSchema

A DataFrame containing the contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def get_contingencies_from_file(
    n1_file: Path, delimiter: str = ";", filesystem: AbstractFileSystem | None = None
) -> ContingencyImportSchemaPowerFactory:
    """Get the contingencies from the file.

    This function reads the contingencies from the file and returns a DataFrame in the
    ContingencyImportSchema format.

    Parameters
    ----------
    n1_file : Path
        The path to the file.
    delimiter : str
        The delimiter of the file. Default is ";".
    filesystem : AbstractFileSystem | None
        The filesystem to use to read the file. If None, the local filesystem is used.

    Returns
    -------
    ContingencyImportSchema
        A DataFrame containing the contingencies.
    """
    if filesystem is None:
        filesystem = LocalFileSystem()
    with filesystem.open(str(n1_file), "r") as f:
        n1_definition = pd.read_csv(f, delimiter=delimiter)
    cond = n1_definition["power_factory_grid_model_name"].isna()
    n1_definition.loc[cond, "power_factory_grid_model_name"] = n1_definition.loc[cond, "contingency_name"]
    n1_definition["contingency_id"] = n1_definition["contingency_id"].astype(int)
    n1_definition = ContingencyImportSchemaPowerFactory.validate(n1_definition)
    return n1_definition

match_contingencies #

match_contingencies(
    n1_definition, all_element_names, match_by_name=True
)

Match the contingencies from the file with the elements in the grid model.

This function matches the contingencies from the file with the elements in the grid model. It first tries to match by index, then by name.

PARAMETER DESCRIPTION
n1_definition

The contingencies from the file.

TYPE: ContingencyImportSchema

all_element_names

The elements in the grid model.

TYPE: AllGridElementsSchema

match_by_name

If True, match by name. Default is True. If False, only match by index.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
ContingencyMatchSchema

A DataFrame containing the matched contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def match_contingencies(
    n1_definition: ContingencyImportSchemaPowerFactory,
    all_element_names: AllGridElementsSchema,
    match_by_name: bool = True,
) -> ContingencyMatchSchema:
    """Match the contingencies from the file with the elements in the grid model.

    This function matches the contingencies from the file with the elements in the grid model.
    It first tries to match by index, then by name.

    Parameters
    ----------
    n1_definition : ContingencyImportSchema
        The contingencies from the file.
    all_element_names : AllGridElementsSchema
        The elements in the grid model.
    match_by_name : bool
        If True, match by name. Default is True.
        If False, only match by index.

    Returns
    -------
    ContingencyMatchSchema
        A DataFrame containing the matched contingencies.
    """
    processed_n1_definition = match_contingencies_by_index(n1_definition, all_element_names)
    if match_by_name:
        processed_n1_definition = match_contingencies_by_name(processed_n1_definition, all_element_names)
    return processed_n1_definition

match_contingencies_by_index #

match_contingencies_by_index(
    n1_definition, all_element_names
)

Match the contingencies from the file with the elements in the grid model.

PARAMETER DESCRIPTION
n1_definition

The contingencies from the file.

TYPE: ContingencyImportSchema

all_element_names

The elements in the grid model.

TYPE: AllGridElementsSchema

RETURNS DESCRIPTION
ContingencyMatchSchema

A DataFrame containing the matched contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def match_contingencies_by_index(
    n1_definition: ContingencyImportSchemaPowerFactory, all_element_names: AllGridElementsSchema
) -> ContingencyMatchSchema:
    """Match the contingencies from the file with the elements in the grid model.

    Parameters
    ----------
    n1_definition : ContingencyImportSchema
        The contingencies from the file.
    all_element_names : AllGridElementsSchema
        The elements in the grid model.

    Returns
    -------
    ContingencyMatchSchema
        A DataFrame containing the matched contingencies.
    """
    # match grid_model_ids directly
    processed_n1_definition = n1_definition.merge(
        all_element_names, how="left", left_on="power_factory_grid_model_rdf_id", right_on="grid_model_id"
    )
    if (~processed_n1_definition["grid_model_name"].isna()).sum() == 0:
        # no elements found, try to remove underscore from rdf_id
        n1_definition["power_factory_grid_model_rdf_id"] = n1_definition["power_factory_grid_model_rdf_id"].str[1:]
        processed_n1_definition = n1_definition.merge(
            all_element_names, how="left", left_on="power_factory_grid_model_rdf_id", right_on="grid_model_id"
        )
    if (~processed_n1_definition["grid_model_name"].isna()).sum() == 0:
        logger.warning("No elements found in the grid model via CIM id. Check the grid model and the contingency file.")

    processed_n1_definition = ContingencyMatchSchema.validate(processed_n1_definition)
    return processed_n1_definition

match_contingencies_by_name #

match_contingencies_by_name(
    processed_n1_definition, all_element_names
)

Match the contingencies from the file with the elements in the grid model by name.

Matches by name and replaces the grid_model_name, element_type and grid_model_id. First tries to match 100% of the name. Second tries to match by removing spaces and replacing "+" with "##_##".

PARAMETER DESCRIPTION
processed_n1_definition

The contingencies from the file.

TYPE: ContingencyMatchSchema

all_element_names

The elements in the grid model.

TYPE: AllGridElementsSchema

RETURNS DESCRIPTION
ContingencyMatchSchema

A DataFrame containing the matched contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def match_contingencies_by_name(
    processed_n1_definition: ContingencyMatchSchema,
    all_element_names: AllGridElementsSchema,
) -> ContingencyMatchSchema:
    """Match the contingencies from the file with the elements in the grid model by name.

    Matches by name and replaces the grid_model_name, element_type and grid_model_id.
    First tries to match 100% of the name.
    Second tries to match by removing spaces and replacing "+" with "##_##".

    Parameters
    ----------
    processed_n1_definition : ContingencyMatchSchema
        The contingencies from the file.
    all_element_names : AllGridElementsSchema
        The elements in the grid model.

    Returns
    -------
    ContingencyMatchSchema
        A DataFrame containing the matched contingencies.
    """
    processed_n1_definition = match_contingencies_column(
        processed_n1_definition=processed_n1_definition,
        all_element_names=all_element_names,
        n1_column="power_factory_grid_model_name",
        element_column="grid_model_name",
    )

    # a "+" sometimes makes problems in the name matching
    all_element_names["grid_model_name_no_space"] = (
        all_element_names["grid_model_name"].str.replace(" ", "").str.replace("+", "##_##")
    )
    processed_n1_definition["power_factory_grid_model_name_no_space"] = (
        processed_n1_definition["power_factory_grid_model_name"].str.replace(" ", "").str.replace("+", "##_##")
    )
    processed_n1_definition = match_contingencies_column(
        processed_n1_definition=processed_n1_definition,
        all_element_names=all_element_names,
        n1_column="power_factory_grid_model_name_no_space",
        element_column="grid_model_name_no_space",
    )

    processed_n1_definition.drop(
        columns=[
            "power_factory_grid_model_name_no_space",
        ],
        inplace=True,
    )
    all_element_names.drop(columns=["grid_model_name_no_space"], inplace=True)

    return processed_n1_definition

match_contingencies_with_suffix #

match_contingencies_with_suffix(
    processed_n1_definition,
    all_element_names,
    grid_model_suffix,
)

Match the contingencies from the file with the elements in the grid model by name.

Matches by name and replaces the grid_model_name with power_factory_grid_model_name. Removes suffix from the grid_model_name.

PARAMETER DESCRIPTION
processed_n1_definition

The contingencies from the file.

TYPE: ContingencyMatchSchema

all_element_names

The elements in the grid model.

TYPE: AllGridElementsSchema

grid_model_suffix

The suffixes to match the grid model names.

TYPE: list[str]

RETURNS DESCRIPTION
ContingencyMatchSchema

A DataFrame containing the matched contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def match_contingencies_with_suffix(
    processed_n1_definition: ContingencyMatchSchema,
    all_element_names: AllGridElementsSchema,
    grid_model_suffix: list[str],
) -> ContingencyMatchSchema:
    """Match the contingencies from the file with the elements in the grid model by name.

    Matches by name and replaces the grid_model_name with power_factory_grid_model_name.
    Removes suffix from the grid_model_name.

    Parameters
    ----------
    processed_n1_definition : ContingencyMatchSchema
        The contingencies from the file.
    all_element_names : AllGridElementsSchema
        The elements in the grid model.
    grid_model_suffix : list[str]
        The suffixes to match the grid model names.

    Returns
    -------
    ContingencyMatchSchema
        A DataFrame containing the matched contingencies.
    """
    all_element_names["grid_model_name_suffix"] = all_element_names["grid_model_name"]
    for suffix in grid_model_suffix:
        cond_suffix = all_element_names["grid_model_name_suffix"].str.endswith(suffix)
        all_element_names.loc[cond_suffix, "grid_model_name_suffix"] = all_element_names.loc[
            cond_suffix, "grid_model_name_suffix"
        ].str[: -len(suffix)]

    processed_n1_definition = match_contingencies_column(
        processed_n1_definition=processed_n1_definition,
        all_element_names=all_element_names,
        n1_column="power_factory_grid_model_name",
        element_column="grid_model_name_suffix",
    )

    all_element_names.drop(columns=["grid_model_name_suffix"], inplace=True)

    return processed_n1_definition

match_contingencies_column #

match_contingencies_column(
    processed_n1_definition,
    all_element_names,
    n1_column,
    element_column,
)

Match a column processed_n1_definition with a column from all_element_names.

This functions matches based on 100% name match and replaces the grid_model_name, element_type and grid_model_id

PARAMETER DESCRIPTION
processed_n1_definition

The contingencies from the file.

TYPE: ContingencyMatchSchema

all_element_names

The elements in the grid model.

TYPE: AllGridElementsSchema

n1_column

The column name in processed_n1_definition.

TYPE: str

element_column

The column name in all_element_names.

TYPE: str

RETURNS DESCRIPTION
ContingencyMatchSchema

A DataFrame containing the matched contingencies.

Source code in packages/importer_pkg/src/toop_engine_importer/contingency_from_power_factory/contingency_from_file.py
def match_contingencies_column(
    processed_n1_definition: ContingencyMatchSchema,
    all_element_names: AllGridElementsSchema,
    n1_column: str,
    element_column: str,
) -> ContingencyMatchSchema:
    """Match a column processed_n1_definition with a column from all_element_names.

    This functions matches based on 100% name match and replaces the grid_model_name, element_type and grid_model_id

    Parameters
    ----------
    processed_n1_definition : ContingencyMatchSchema
        The contingencies from the file.
    all_element_names : AllGridElementsSchema
        The elements in the grid model.
    n1_column : str
        The column name in processed_n1_definition.
    element_column : str
        The column name in all_element_names.

    Returns
    -------
    ContingencyMatchSchema
        A DataFrame containing the matched contingencies.
    """
    # merge the n1_definition with all_element_names
    processed_n1_definition = processed_n1_definition.merge(
        all_element_names,
        how="left",
        left_on=n1_column,
        right_on=element_column,
        suffixes=("", "_2"),
    )
    # get new matched elements
    cond_not_matched_elements = processed_n1_definition["grid_model_name"].isna()
    cond_name_found = ~processed_n1_definition["grid_model_name_2"].isna()
    cond_replace = cond_not_matched_elements & cond_name_found
    # replace new matched elements
    processed_n1_definition.loc[cond_replace, "grid_model_name"] = processed_n1_definition.loc[
        cond_replace, "grid_model_name_2"
    ]
    processed_n1_definition.loc[cond_replace, "element_type"] = processed_n1_definition.loc[cond_replace, "element_type_2"]
    processed_n1_definition.loc[cond_replace, "grid_model_id"] = processed_n1_definition.loc[cond_replace, "grid_model_id_2"]
    processed_n1_definition.drop(columns=["grid_model_name_2", "element_type_2", "grid_model_id_2"], inplace=True)
    processed_n1_definition = ContingencyMatchSchema.validate(processed_n1_definition)
    return processed_n1_definition

UCTE Toolset#

toop_engine_importer.ucte_toolset #

A collection of tools to work with UCTE data.

  • ucte_toolset.py: Functions load, manipulate, and save UCTE data using pd.DataFrame.

Importer Pandapower#

toop_engine_importer.pandapower_import #

Contains functions to import data from pandapower networks to the Topology Optimizer.

__all__ module-attribute #

__all__ = [
    "add_substation_column_to_bus",
    "create_virtual_slack",
    "drop_elements_connected_to_one_bus",
    "drop_unsupplied_buses",
    "fuse_closed_switches_by_bus_ids",
    "fuse_closed_switches_fast",
    "get_all_switches_from_bus_ids",
    "get_asset_topology_from_network",
    "get_closed_switch",
    "get_coupler_types_of_substation",
    "get_indirect_connected_switch",
    "get_relevant_subs",
    "get_station_from_id",
    "get_station_id_list",
    "get_substation_buses_from_bus_id",
    "get_type_b_nodes",
    "make_pp_masks",
    "move_elements_based_on_labels",
    "preprocess_net_step1",
    "preprocess_net_step2",
    "remove_out_of_service",
    "replace_zero_branches",
    "select_connected_subnet",
    "validate_asset_topology",
]

create_virtual_slack #

create_virtual_slack(net)

Create a virtual slack bus for all ext_grids in the network.

PARAMETER DESCRIPTION
net

The pandapower network to create a virtual slack for, will be modified in-place. Note: network is modified in-place.

TYPE: pandapowerNet

RETURNS DESCRIPTION
pandapowerNet

The network with a virtual slack.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def create_virtual_slack(net: pp.pandapowerNet) -> None:
    """Create a virtual slack bus for all ext_grids in the network.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to create a virtual slack for, will be modified in-place.
        Note: network is modified in-place.

    Returns
    -------
    pp.pandapowerNet
        The network with a virtual slack.
    """
    if net.gen.slack.sum() <= 1:
        return
    # Create a virtual slack where all ext_grids are connected to
    virtual_slack_bus = pp.create_bus(net, vn_kv=380, in_service=True, name="virtual_slack")

    for generator in net.gen[net.gen.slack].index:
        cur_bus = net.gen.loc[generator].bus
        # Connect each gen through a trafo to the virtual slack
        pp.create_transformer_from_parameters(
            net,
            hv_bus=virtual_slack_bus,
            lv_bus=cur_bus,
            name="con_" + str(net.gen.loc[generator].name),
            sn_mva=9999,
            vn_hv_kv=net.bus.vn_kv[cur_bus],
            vn_lv_kv=net.bus.vn_kv[cur_bus],
            # shift_degree=net.ext_grid.loc[generator].va_degree,
            shift_degree=0,
            pfe_kw=1,
            i0_percent=0.1,
            vk_percent=1,
            vkr_percent=0.1,
            xn_ohm=10,
        )

    net.gen.drop(net.gen[net.gen.slack].index, inplace=True)

    pp.create_ext_grid(
        net,
        virtual_slack_bus,
        vm_pu=1,
        va_degree=0,
        in_service=True,
        name="virtual_slack",
    )

drop_elements_connected_to_one_bus #

drop_elements_connected_to_one_bus(net, branch_types=None)

Drop elements connected to one bus.

  • impedance -> Capacitor will end up on the same bus
  • trafo3w -> edgecase: trafo3w that goes from one hv to the same level but two different busbars will end up on the same bus
PARAMETER DESCRIPTION
net

pandapower network Note: the network is modified in place

TYPE: pandapowerNet

branch_types

list of branch types to drop elements connected to one bus

TYPE: list[str] DEFAULT: None

RETURNS DESCRIPTION
None
Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def drop_elements_connected_to_one_bus(net: pp.pandapowerNet, branch_types: Optional[list[str]] = None) -> None:
    """Drop elements connected to one bus.

    - impedance -> Capacitor will end up on the same bus
    - trafo3w -> edgecase: trafo3w that goes from one hv to the same level but two
                 different busbars will end up on the same bus

    Parameters
    ----------
    net : pp.pandapowerNet
        pandapower network
        Note: the network is modified in place
    branch_types : list[str]
        list of branch types to drop elements connected to one bus

    Returns
    -------
    None

    """
    if branch_types is None:
        branch_types = ["line", "trafo", "trafo3w", "impedance", "switch"]

    for branch_type in branch_types:
        handle_elements_connected_to_one_bus(net, branch_type)

drop_unsupplied_buses #

drop_unsupplied_buses(net)

Drop all unsupplied buses from the network.

PARAMETER DESCRIPTION
net

The pandapower network to drop unsupplied buses from, will be modified in-place.

TYPE: pandapowerNet

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def drop_unsupplied_buses(net: pp.pandapowerNet) -> None:
    """Drop all unsupplied buses from the network.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to drop unsupplied buses from, will be modified in-place.
    """
    pp.drop_buses(net, pp.topology.unsupplied_buses(net))
    assert len(pp.topology.unsupplied_buses(net)) == 0

fuse_closed_switches_fast #

fuse_closed_switches_fast(net, switch_ids=None)

Fuse closed switches in the network by merging busbars.

This routine uses an algorithm to number each busbar and then find the lowest connected busbar iteratively. If a busbar is connected to a lower-numbered busbar, it will be re-labeled to the lower-numbered busbar. This algorithm needs as many iterations as the maximum number of hops between the lowest and highest busbar in any of the substations.

PARAMETER DESCRIPTION
net

The pandapower network to fuse closed switches in, will be modified in-place.

TYPE: pandapowerNet

switch_ids

The switch ids to fuse. If None, all closed switches are fused.

TYPE: Optional[list[int]] DEFAULT: None

RETURNS DESCRIPTION
DataFrame

The closed switches that were fused.

DataFrame

The buses that were dropped because they were relabeled to a lower-numbered busbar.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def fuse_closed_switches_fast(
    net: pp.pandapowerNet,
    switch_ids: Optional[list[int]] = None,
) -> tuple[pd.DataFrame, pd.DataFrame]:
    """Fuse closed switches in the network by merging busbars.

    This routine uses an algorithm to number each busbar and then find the lowest connected busbar
    iteratively. If a busbar is connected to a lower-numbered busbar, it will be re-labeled to the
    lower-numbered busbar. This algorithm needs as many iterations as the maximum number of hops
    between the lowest and highest busbar in any of the substations.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to fuse closed switches in, will be modified in-place.
    switch_ids: list[int]
        The switch ids to fuse. If None, all closed switches are fused.

    Returns
    -------
    pd.DataFrame
        The closed switches that were fused.
    pd.DataFrame
        The buses that were dropped because they were relabeled to a lower-numbered busbar.
    """
    # Label the busbars, find the lowest index that every busbar is coupled to
    labels = np.arange(np.max(net.bus.index) + 1)
    closed_switches = net.switch[net.switch.closed & (net.switch.et == "b") & (net.switch.bus != net.switch.element)]
    if switch_ids is not None:
        closed_switches = closed_switches[closed_switches.index.isin(switch_ids)]
    while not np.array_equal(labels[closed_switches.bus.values], labels[closed_switches.element.values]):
        bus_smaller = labels[closed_switches.bus.values] < labels[closed_switches.element.values]
        element_smaller = labels[closed_switches.bus.values] > labels[closed_switches.element.values]

        # Where the element is smaller, set the bus labels to the element labels
        _, change_idx = np.unique(closed_switches.bus.values[element_smaller], return_index=True)
        labels[closed_switches.bus.values[element_smaller][change_idx]] = labels[
            closed_switches.element.values[element_smaller][change_idx]
        ]

        # Where the bus is smaller (and where the element was not already touched), set the element labels to the bus labels
        was_touched = np.isin(
            closed_switches.element.values,
            closed_switches.bus.values[element_smaller][change_idx],
        )
        cond = bus_smaller & ~was_touched
        _, change_idx = np.unique(closed_switches.element.values[cond], return_index=True)
        labels[closed_switches.element.values[cond][change_idx]] = labels[closed_switches.bus.values[cond][change_idx]]

    # Move all elements over to the lowest index busbar
    move_elements_based_on_labels(net, labels)
    # Drop all busbars that were re-labeled because they were connected to a lower-labeled bus
    buses_to_drop = net.bus[~np.isin(net.bus.index, labels)]
    switch_cond = (net.switch.et == "b") & (net.switch.bus == net.switch.element)
    switch_to_drop = net.switch[switch_cond]
    pp.toolbox.drop_elements(net, "switch", switch_to_drop.index)
    pp.drop_buses(net, buses_to_drop.index)
    return closed_switches, buses_to_drop

move_elements_based_on_labels #

move_elements_based_on_labels(net, labels)

Move all elements in the network to the lowest labeled busbar.

PARAMETER DESCRIPTION
net

The pandapower network to move elements in, will be modified in-place.

TYPE: pandapowerNet

labels

The labels of the busbars to move the elements to.

TYPE: ndarray

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def move_elements_based_on_labels(
    net: pp.pandapowerNet,
    labels: np.ndarray,
) -> None:
    """Move all elements in the network to the lowest labeled busbar.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to move elements in, will be modified in-place.
    labels: np.ndarray
        The labels of the busbars to move the elements to.
    """
    for element, column in pp.element_bus_tuples():
        if element == "switch":
            net[element][column] = labels[net[element][column]]
            switch_cond = net[element].et == "b"
            net[element].loc[switch_cond, "element"] = labels[net[element].loc[switch_cond, "element"]]
            net[element].loc[net[element].index, "bus"] = labels[net[element].loc[net[element].index, "bus"]]
        else:
            net[element][column] = labels[net[element][column]]

remove_out_of_service #

remove_out_of_service(net)

Remove all out-of-service elements from the network.

PARAMETER DESCRIPTION
net

The pandapower network to remove out-of-service elements from, will be modified in-place.

TYPE: pandapowerNet

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def remove_out_of_service(net: pp.pandapowerNet) -> None:
    """Remove all out-of-service elements from the network.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to remove out-of-service elements from, will be modified in-place.
    """
    for element in pp.pp_elements():
        if "bus" == element and "in_service" in net[element]:
            pp.drop_buses(net, net[element][~net[element]["in_service"]].index)
        elif "in_service" in net[element]:
            net[element] = net[element][net[element]["in_service"]]

replace_zero_branches #

replace_zero_branches(net)

Replace zero-impedance branches with switches in the network.

Some leftover lines and xwards will be bumped to a higher impedance to avoid numerical issues.

PARAMETER DESCRIPTION
net

The pandapower network to replace zero branches in, will be modified in-place.

TYPE: pandapowerNet

RETURNS DESCRIPTION
pandapowerNet

The network with zero branches replaced.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def replace_zero_branches(net: pp.pandapowerNet) -> None:
    """Replace zero-impedance branches with switches in the network.

    Some leftover lines and xwards will be bumped to a higher impedance to avoid numerical issues.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to replace zero branches in, will be modified in-place.

    Returns
    -------
    pp.pandapowerNet
        The network with zero branches replaced.
    """
    pp.toolbox.replace_zero_branches_with_switches(
        net,
        min_length_km=0.0,
        min_r_ohm_per_km=0.002,
        min_x_ohm_per_km=0.002,
        min_c_nf_per_km=0,
        min_rft_pu=0,
        min_xft_pu=0,
    )
    threshold_x_ohm = 0.001
    # net.xward.x_ohm[net.xward.x_ohm == 1e-6] = 1e-2
    net.xward.loc[net.xward.x_ohm < threshold_x_ohm, "x_ohm"] = 0.01
    zero_lines = (net.line.x_ohm_per_km * net.line.length_km) < threshold_x_ohm
    net.line.loc[zero_lines, "x_ohm_per_km"] = 0.01
    net.line.loc[zero_lines, "length_km"] = 1.0

select_connected_subnet #

select_connected_subnet(net)

Select the connected subnet of the grid that has a slack and return it.

PARAMETER DESCRIPTION
net

The pandapower network to select the connected subnet from.

TYPE: pandapowerNet

RETURNS DESCRIPTION
pandapowerNet

The connected subnet of the grid that has a slack.

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/pandapower/pandapower_import_helpers.py
def select_connected_subnet(net: pp.pandapowerNet) -> pp.pandapowerNet:
    """Select the connected subnet of the grid that has a slack and return it.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to select the connected subnet from.

    Returns
    -------
    pp.pandapowerNet
        The connected subnet of the grid that has a slack.
    """
    name = net.name
    mg = pp.topology.create_nxgraph(net, respect_switches=True)

    slack_bus = net.ext_grid[net.ext_grid.in_service].bus
    if len(slack_bus) == 0:
        slack_bus = net.gen[net.gen.slack & net.gen.in_service].bus
        if len(slack_bus) == 0:
            raise ValueError("No slack bus found in the network.")
    slack_bus = slack_bus.iloc[0]

    cc = pp.topology.connected_component(mg, slack_bus)

    next_grid_buses = set(cc)
    net_new = pp.select_subnet(
        net,
        next_grid_buses,
        include_switch_buses=True,
        include_results=False,
        keep_everything_else=True,
    )
    net_new.name = name
    return net_new

get_asset_topology_from_network #

get_asset_topology_from_network(
    network,
    topology_id,
    grid_model_file,
    station_id_list,
    foreign_key="equipment",
)

Get the asset topology from the network.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

topology_id

Id of the topology.

TYPE: str

grid_model_file

Name of the grid model file.

TYPE: str

station_id_list

List of station ids for which the stations should be retrieved. Station ids are a list -> a list of busbars associated with the station.

TYPE: List[List[int]]

foreign_key

Defines the column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
asset_topology

Topology class of the network.

TYPE: Topology

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_asset_topology_from_network(
    network: pp.pandapowerNet,
    topology_id: str,
    grid_model_file: str,
    station_id_list: List[List[int]],
    foreign_key: str = "equipment",
) -> Topology:
    """Get the asset topology from the network.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    topology_id: str
        Id of the topology.
    grid_model_file: str
        Name of the grid model file.
    station_id_list: List[List[int]]
        List of station ids for which the stations should be retrieved.
        Station ids are a list -> a list of busbars associated with the station.
    foreign_key: str
        Defines the column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    asset_topology: Topology
        Topology class of the network.
    """
    asset_topology = get_list_of_stations_ids(network=network, station_list=station_id_list, foreign_key=foreign_key)
    timestamp = datetime.datetime.now()
    return Topology(
        topology_id=topology_id,
        grid_model_file=grid_model_file,
        stations=asset_topology,
        timestamp=timestamp,
    )

get_station_from_id #

get_station_from_id(
    network, station_id_list, foreign_key="equipment"
)

Get the busses from a station_id.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_id_list

List of station ids for which the stations should be retrieved.

TYPE: list[int]

foreign_key

Defines the column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
station

Station object.

TYPE: Station

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_station_from_id(
    network: pp.pandapowerNet,
    station_id_list: list[int],
    foreign_key: str = "equipment",
) -> Station:
    """Get the busses from a station_id.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_id_list: list[int]
        List of station ids for which the stations should be retrieved.
    foreign_key: str
        Defines the column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    station: Station
        Station object.
    """
    station_buses = get_busses_from_station(network, station_bus_index=station_id_list)
    coupler_elements = get_coupler_from_station(network, station_buses)
    (
        station_branches,
        switching_matrix,
        asset_connection_path,
    ) = get_branches_from_station(network, station_buses, foreign_key=foreign_key)

    # get the lists of the pydantic model objects
    busbar_list = get_list_of_busbars_from_df(station_buses[station_buses["type"] == "b"])
    coupler_list = get_list_of_coupler_from_df(coupler_elements)
    switchable_assets_list = get_list_of_switchable_assets_from_df(
        station_branches=station_branches, asset_bay_list=asset_connection_path
    )

    voltage_level_float = get_parameter_from_station(network=network, station_bus_index=station_id_list, parameter="vn_kv")
    # region = get_parameter_from_station(network, station_name, "zone")

    # get the station_name from the station_id
    # in pandapower a station is a bus -> only one entry in the DataFrame
    station_buses.sort_index(inplace=True)
    station_name = station_buses["name"].values[0]
    grid_model_id = station_buses["grid_model_id"].values[0]

    return Station(
        grid_model_id=grid_model_id,
        name=station_name,
        # region=region,
        voltage_level=voltage_level_float,
        busbars=busbar_list,
        couplers=coupler_list,
        assets=switchable_assets_list,
        asset_switching_table=switching_matrix,
    )

add_substation_column_to_bus #

add_substation_column_to_bus(
    network,
    substation_col="substat",
    get_name_col="name",
    only_closed_switches=False,
)

Add a substation column to the bus DataFrame.

This function will go through all busbars of type 'b' and add the substation name to all buses connected to the busbar.

PARAMETER DESCRIPTION
network

The pandapower network to add the substation column to. Note: the network will be modified in-place.

TYPE: pandapowerNet

substation_col

The name of the new substation column where the value from the get_name_col is added.

TYPE: Optional[str] DEFAULT: 'substat'

get_name_col

The name of the column to get the substation name from.

TYPE: Optional[str] DEFAULT: 'name'

only_closed_switches

If True, only closed switches are considered. The result will lead substation naming after the the electrical voltage level.

TYPE: bool DEFAULT: False

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def add_substation_column_to_bus(
    network: pp.pandapowerNet,
    substation_col: Optional[str] = "substat",
    get_name_col: Optional[str] = "name",
    only_closed_switches: bool = False,
) -> None:
    """Add a substation column to the bus DataFrame.

    This function will go through all busbars of type 'b' and add the substation name to all buses connected to the busbar.

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network to add the substation column to.
        Note: the network will be modified in-place.
    substation_col: Optional[str]
        The name of the new substation column where the value from the get_name_col is added.
    get_name_col: Optional[str]
        The name of the column to get the substation name from.
    only_closed_switches: bool
        If True, only closed switches are considered.
        The result will lead substation naming after the the electrical voltage level.
    """
    bus_type_b = get_type_b_nodes(network).index
    network.bus[substation_col] = ""
    found_list = []
    name_list = []
    for bus_id in bus_type_b:
        if bus_id in found_list:
            continue
        station_buses = list(get_substation_buses_from_bus_id(network, bus_id, only_closed_switches=only_closed_switches))
        station_name = str(network.bus.loc[bus_id, get_name_col])
        counter = 0
        while station_name in name_list:
            station_name = str(network.bus.loc[bus_id, get_name_col]) + f"_{counter}"
            counter += 1
        network.bus.loc[station_buses, substation_col] = station_name
        found_list.extend(station_buses)
        name_list.append(station_name)

fuse_closed_switches_by_bus_ids #

fuse_closed_switches_by_bus_ids(network, switch_bus_ids)

Fuse a series of closed switches in the network by merging busbars (type b).

Note: this function expects that there are only switches between the busbars.
Warning: this function will break the model if gaps are between the buses or other elements in between.
This function will not work if you try to fuse multiple busbars,
that are not directly connected by the the switch_bus_ids.
e.g.
----busbar1----switch1----switch2---switch3----busbar2----
will result in:
----busbar1---

This will not work:
(no connection between busbar1 and busbar3 / busbar2 and 4)
----busbar1----switch1----switch2---switch3----busbar2----
----busbar3----switch4----switch5---switch6----busbar4----
call this function twice to fuse busbar2 into busbar1 and busbar4 into busbar3
PARAMETER DESCRIPTION
network

The pandapower network to fuse closed switches in, will be modified in-place.

TYPE: pandapowerNet

switch_bus_ids

The bus ids of the switches to fuse. Note: this must include the bus_id that is expected to be the final busbar.

TYPE: list[int]

RETURNS DESCRIPTION
bus_labels

An with the length of the highest bus id in the network representing the busbar index. At the index of the array(old busbar index), the new busbar index is stored.

TYPE: array

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def fuse_closed_switches_by_bus_ids(network: pp.pandapowerNet, switch_bus_ids: list[int]) -> np.array:
    """Fuse a series of closed switches in the network by merging busbars (type b).

    ```
    Note: this function expects that there are only switches between the busbars.
    Warning: this function will break the model if gaps are between the buses or other elements in between.
    This function will not work if you try to fuse multiple busbars,
    that are not directly connected by the the switch_bus_ids.
    e.g.
    ----busbar1----switch1----switch2---switch3----busbar2----
    will result in:
    ----busbar1---

    This will not work:
    (no connection between busbar1 and busbar3 / busbar2 and 4)
    ----busbar1----switch1----switch2---switch3----busbar2----
    ----busbar3----switch4----switch5---switch6----busbar4----
    call this function twice to fuse busbar2 into busbar1 and busbar4 into busbar3
    ```

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network to fuse closed switches in, will be modified in-place.
    switch_bus_ids: list[int]
        The bus ids of the switches to fuse.
        Note: this must include the bus_id that is expected to be the final busbar.

    Returns
    -------
    bus_labels: np.array
        An with the length of the highest bus id in the network representing the busbar index.
        At the index of the array(old busbar index), the new busbar index is stored.

    """
    # get a label dict for the buses
    # make sure that missing/deleted buses do not break the algorithm
    bus_labels = np.arange(np.max(network.bus.index) + 1)
    # remove duplicate bus ids -> can happen if cross coupler have only one DV switch
    switch_bus_ids_pruned = list(set(switch_bus_ids))
    switch_buses = network.bus.loc[switch_bus_ids_pruned]
    switch_buses_type_b = switch_buses[switch_buses["type"] == "b"]
    if len(switch_buses_type_b) == 0:
        raise ValueError(f"No busbars found in the switch_bus_ids list {switch_bus_ids}")
    # select the first busbar of type 'b' to be the reference busbar
    for bus_id in switch_bus_ids_pruned:
        # set all busbars to the first busbar
        bus_labels[bus_id] = switch_buses_type_b.index[0]
        # Move all elements over to the lowest index busbar

    move_elements_based_on_labels(network, bus_labels)
    # Drop all busbars that were re-labeled because they were connected to a lower-labeled bus
    buses_to_drop = network.bus[~np.isin(network.bus.index, bus_labels)]
    # drop switches that are connected to one bus -> have been fused
    network["switch"] = network["switch"][network["switch"]["bus"] != network["switch"]["element"]]
    pp.drop_buses(network, buses_to_drop.index)

    return bus_labels

get_all_switches_from_bus_ids #

get_all_switches_from_bus_ids(
    network, bus_ids, only_closed_switches=True
)

Get all switches connected to a list of buses.

PARAMETER DESCRIPTION
network

The pandapower network to get the switches from.

TYPE: pandapowerNet

bus_ids

The buses to get the switches from.

TYPE: list[int]

only_closed_switches

If True, only closed switches are considered.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
DataFrame

A DataFrame with all switches connected to the buses in bus_ids.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_all_switches_from_bus_ids(
    network: pp.pandapowerNet, bus_ids: list[int], only_closed_switches: bool = True
) -> pd.DataFrame:
    """Get all switches connected to a list of buses.

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network to get the switches from.
    bus_ids: list[int]
        The buses to get the switches from.
    only_closed_switches: bool
        If True, only closed switches are considered.

    Returns
    -------
    pd.DataFrame
        A DataFrame with all switches connected to the buses in bus_ids.
    """
    connected = pp.toolbox.get_connected_elements_dict(
        network,
        bus_ids,
        respect_switches=only_closed_switches,
        include_empty_lists=True,
    )
    station_switches = network.switch[network.switch.index.isin(connected["switch"])]
    return station_switches

get_closed_switch #

get_closed_switch(switches, column, column_ids)

Get the closed switch based on the column and column_ids.

PARAMETER DESCRIPTION
switches

The switches df to filter the closed switch from.

TYPE: DataFrame

column

The column to filter the column_ids. e.g. foreign_id

TYPE: str

column_ids

The column ids to filter the closed switch from.

TYPE: list[Union[str, int, float]]

RETURNS DESCRIPTION
DataFrame

The closed switch filtered by the column_ids.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_closed_switch(switches: pd.DataFrame, column: str, column_ids: list[Union[str, int, float]]) -> pd.DataFrame:
    """Get the closed switch based on the column and column_ids.

    Parameters
    ----------
    switches: pd.DataFrame
        The switches df to filter the closed switch from.
    column: str
        The column to filter the column_ids. e.g. foreign_id
    column_ids: list[Union[str, int, float]]
        The column ids to filter the closed switch from.

    Returns
    -------
    pd.DataFrame
        The closed switch filtered by the column_ids.
    """
    closed_switch = switches[(switches[column].isin(column_ids)) & (switches.closed)]
    return closed_switch

get_coupler_types_of_substation #

get_coupler_types_of_substation(
    network, substation_bus_list, only_closed_switches=True
)

Get the cross coupler (German: Querkuppler), busbar coupler and a cross connector of a substation.

A busbar coupler is a connection between two busbars, where assets can be connected to both busbars. A cross coupler is a connection between two busbars B1 and B2, where assets A1 can not be connected to both busbars directly. Asset A1 can only be connected directly to B1 and is connected indirectly to B2 by the cross coupler. A coupler is always a disconnector (DS), a power switch (CB) and a DS in series. In unique cases, there can be two CB switches in series. A cross connector is a single disconnector between two busbars.

PARAMETER DESCRIPTION
network

The pandapower network to get the Cross coupler/quercoupler from.

TYPE: pandapowerNet

substation_bus_list

The bus list of the substation. All buses in the list represent a substation.

TYPE: list[int]

only_closed_switches

If True, only closed switches are considered.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
coupler

a dictionary with 4 keys: - 1. key: "busbar_coupler_bus_ids" - 2. key: "cross_coupler_bus_ids" - 3. key: "busbar_coupler_switch_ids" - 4. key: "cross_coupler_switch_ids" bus_ids: list of bus ids representing the busbar coupler and cross coupler switch_ids: list of switch ids representing the busbar coupler and cross coupler switch_ids = [CB, DS1, DS2] Note: the switches are not filtered by open/closed. Note: if there is only one switch or two switches: switch_ids_1sw = [CB, CB, CB] switch_ids_2sw = [CB, CB, DS2]

TYPE: dict[str, list[list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_coupler_types_of_substation(
    network: pp.pandapowerNet,
    substation_bus_list: list[int],
    only_closed_switches: bool = True,
) -> dict[str, list[list[int]]]:
    """Get the cross coupler (German: Querkuppler),  busbar coupler and a cross connector of a substation.

    A busbar coupler is a connection between two busbars, where assets can be connected to both busbars.
    A cross coupler is a connection between two busbars B1 and B2,
    where assets A1 can not be connected to both busbars directly.
    Asset A1 can only be connected directly to B1 and is connected indirectly to B2 by the cross coupler.
    A coupler is always a disconnector (DS), a power switch (CB) and a DS in series. In unique cases, there can be
    two CB switches in series.
    A cross connector is a single disconnector between two busbars.


    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network to get the Cross coupler/quercoupler from.
    substation_bus_list: list[int]
        The bus list of the substation.
        All buses in the list represent a substation.
    only_closed_switches: bool
        If True, only closed switches are considered.

    Returns
    -------
    coupler : dict[str, list[list[int]]]
        a dictionary with 4 keys:
        - 1. key: "busbar_coupler_bus_ids"
        - 2. key: "cross_coupler_bus_ids"
        - 3. key: "busbar_coupler_switch_ids"
        - 4. key: "cross_coupler_switch_ids"
        bus_ids: list of bus ids representing the busbar coupler and cross coupler
        switch_ids: list of switch ids representing the busbar coupler and cross coupler
            switch_ids = [CB, DS1, DS2]
        Note: the switches are not filtered by open/closed.
        Note: if there is only one switch or two switches:
            switch_ids_1sw = [CB, CB, CB]
            switch_ids_2sw = [CB, CB, DS2]
    """
    coupler = {
        "busbar_coupler_bus_ids": [],
        "cross_coupler_bus_ids": [],
        "busbar_coupler_switch_ids": [],
        "cross_coupler_switch_ids": [],
    }  # type: dict[str, list[list[int]]]
    bus_type_b = get_type_b_nodes(network, substation_bus_list)
    if len(bus_type_b) == 0 or len(bus_type_b) == 1:
        # no coupled busbars
        return coupler
    vertical_busbars = get_vertical_connected_busbars(network, substation_bus_list)
    busbar_combinations = [
        (int(bus_1), int(bus_2)) for i, bus_1 in enumerate(bus_type_b.index) for bus_2 in bus_type_b.index[i + 1 :]
    ]
    # sort by busbar coupler and cross coupler
    for bus_1, bus_2 in busbar_combinations:
        # get connection between busbars
        switches, _connection = get_connection_between_busbars(
            network=network,
            bus_1=bus_1,
            bus_2=bus_2,
            exlcude_ids=bus_type_b.index,
            only_closed_switches=only_closed_switches,
        )
        if len(switches) != 0:
            # check for parallel switches
            for cb_switch_id in switches:
                # if not consider_three_buses:
                #     cb_switch_id_list = [cb_switch_id]
                # else:
                #     cb_switch_id_list = cb_switch_id
                cb_switch_id_list = [cb_switch_id]
                power_switch = network.switch.loc[cb_switch_id_list]
                switch_buses = np.append(power_switch.element.values, power_switch.bus.values)
                ds_switch_1 = pp.toolbox.get_connecting_branches(
                    network,
                    [bus_1],
                    switch_buses,
                )
                ds_switch_2 = pp.toolbox.get_connecting_branches(
                    network,
                    [bus_2],
                    switch_buses,
                )
                if bus_1 in vertical_busbars and bus_2 in vertical_busbars[bus_1]:
                    bus_key = "busbar_coupler_bus_ids"
                    switch_key = "busbar_coupler_switch_ids"
                else:
                    bus_key = "cross_coupler_bus_ids"
                    switch_key = "cross_coupler_switch_ids"

                # handle cases with two buses
                bus_res = [
                    bus_1,
                    bus_2,
                    power_switch.element.values[0],
                    power_switch.bus.values[0],
                ]
                switch_res = [
                    cb_switch_id_list[0],
                    int(list(ds_switch_1["switch"])[0]),  # noqa: RUF015
                    int(list(ds_switch_2["switch"])[0]),  # noqa: RUF015
                ]
                # # handle cases with three buses
                # # if consider_three_buses:
                # if len(cb_switch_id_list) > 1:
                #     switch_res.append(cb_switch_id_list[1])
                #     node_list = list(
                #         set(
                #             np.append(
                #                 power_switch.element.values, power_switch.bus.values
                #             )
                #         )
                #     )
                #     bus_res = [bus_1, bus_2] + node_list
                coupler[bus_key].append(bus_res)
                coupler[switch_key].append(switch_res)

    return coupler

get_indirect_connected_switch #

get_indirect_connected_switch(
    net,
    bus_1,
    bus_2,
    only_closed_switches=True,
    consider_three_buses=False,
    exclude_buses=None,
)

Get a switch, that is indirectly connected by two buses and only by two buses.

This function will only return the indirect connection between two buses. e.g. switchB or any switch that is parallel to switchB.

busA---switchA---busB---switchB---busC---switchC---busD
Note: this function will also return an empty dict for bus1 and bus3. Note: this function will return an empty dict if e.g. switch1 & bus2 are missing.

PARAMETER DESCRIPTION
net

The pandapower network to get the indirect connections from.

TYPE: pandapowerNet

bus_1

The bus to get the indirect connections from.

TYPE: int

bus_2

The bus to get the indirect connections to.

TYPE: int

only_closed_switches

If True, only closed switches are considered.

TYPE: bool DEFAULT: True

consider_three_buses

If True, the function will also consider three buses in between. Bus1---switch1---bus2---switch2---bus3---switch3---bus4---switch4---bus5

TYPE: bool DEFAULT: False

exclude_buses

The buses to exclude from the indirect connection. e.g. give all other busbars (type b) in the substation.

TYPE: Optional[list[int]] DEFAULT: None

RETURNS DESCRIPTION
dict[str, list[int]]

A dictionary with the indirect connections from bus_1 to bus_2

RAISES DESCRIPTION
ValueError

If the indirect connection contains more than one switch. e.g. a parallel line to the switch.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_indirect_connected_switch(
    net: pp.pandapowerNet,
    bus_1: int,
    bus_2: int,
    only_closed_switches: bool = True,
    consider_three_buses: bool = False,
    exclude_buses: Optional[list[int]] = None,
) -> dict[str, list[int]]:
    """Get a switch, that is indirectly connected by two buses and only by two buses.

    This function will only return the indirect connection between two buses.
    e.g. switchB or any switch that is parallel to switchB.
    ```
    busA---switchA---busB---switchB---busC---switchC---busD
    ```
    Note: this function will also return an empty dict for bus1 and bus3.
    Note: this function will return an empty dict if e.g. switch1 & bus2 are missing.

    Parameters
    ----------
    net: pp.pandapowerNet
        The pandapower network to get the indirect connections from.
    bus_1: int
        The bus to get the indirect connections from.
    bus_2: int
        The bus to get the indirect connections to.
    only_closed_switches: bool
        If True, only closed switches are considered.
    consider_three_buses: bool
        If True, the function will also consider three buses in between.
        Bus1---switch1---bus2---switch2---bus3---switch3---bus4---switch4---bus5
    exclude_buses: Optional[list[int]]
        The buses to exclude from the indirect connection.
        e.g. give all other busbars (type b) in the substation.

    Returns
    -------
    dict[str, list[int]]
        A dictionary with the indirect connections from bus_1 to bus_2

    Raises
    ------
    ValueError
        If the indirect connection contains more than one switch.
        e.g. a parallel line to the switch.
    """
    if exclude_buses is None:
        exclude_buses = [bus_1, bus_2]
    bus_1_connected = list(pp.toolbox.get_connected_buses(net, [bus_1], respect_switches=only_closed_switches, consider="s"))
    bus_1_connected = [el for el in bus_1_connected if el not in exclude_buses]
    bus_2_connected = list(pp.toolbox.get_connected_buses(net, [bus_2], respect_switches=only_closed_switches, consider="s"))
    bus_2_connected = [el for el in bus_2_connected if el not in exclude_buses]

    indirect_connection = pp.toolbox.get_connecting_branches(net, bus_1_connected, bus_2_connected)
    if consider_three_buses:
        indirect_connection_3 = get_indirect_connected_switches_three_buses(
            net,
            bus_1,
            bus_2,
            bus_1_connected,
            bus_2_connected,
            only_closed_switches,
            exclude_buses,
        )
        if "switch" in indirect_connection:
            indirect_connection["switch"] = indirect_connection["switch"] | set(indirect_connection_3["switch"])
        else:
            indirect_connection["switch"] = set(indirect_connection_3["switch"])

    indirect_connection = {
        key: list(indirect_connection[key])
        for key in indirect_connection
        if len(indirect_connection[key]) > 0 or key == "switch"
    }
    # filter only closed switches in the indirect connection
    closed_switches = []
    if "switch" in indirect_connection and only_closed_switches:
        for switch_id in indirect_connection["switch"]:
            if net.switch.loc[switch_id].closed:
                closed_switches.append(switch_id)
        indirect_connection["switch"] = closed_switches
        if len(indirect_connection["switch"]) == 0:
            del indirect_connection["switch"]
    if ("switch" in indirect_connection and len(indirect_connection) != 1) or (
        "switch" not in indirect_connection and len(indirect_connection) > 0
    ):
        error_value = [f"{key!s}:{value!s}" for key, values in indirect_connection.items() for value in values]
        raise ValueError(
            f"Indirect connection between bus {bus_1} and {bus_2} must contain only switches {' '.join(error_value)}"
        )
    return indirect_connection

get_station_id_list #

get_station_id_list(bus_df, substation_col='substat')

Get all station ids from the network.

This function will return all unique station ids from the network.

PARAMETER DESCRIPTION
bus_df

The bus DataFrame to get the station ids from. e.g. pre filtered bus DataFrame with only busbars of type 'b'.

TYPE: DataFrame

substation_col

The column name of the substation

TYPE: str DEFAULT: 'substat'

RETURNS DESCRIPTION
list[int]

A list of station ids in the order of the stations in the substation_col.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_station_id_list(bus_df: pd.DataFrame, substation_col: str = "substat") -> list[int]:
    """Get all station ids from the network.

    This function will return all unique station ids from the network.

    Parameters
    ----------
    bus_df: pd.DataFrame
        The bus DataFrame to get the station ids from.
        e.g. pre filtered bus DataFrame with only busbars of type 'b'.
    substation_col: str
        The column name of the substation

    Returns
    -------
    list[int]
        A list of station ids in the order of the stations in the substation_col.
    """
    substation_names = bus_df[substation_col].unique()
    return [bus_df[bus_df[substation_col] == substation_name].index for substation_name in substation_names]

get_substation_buses_from_bus_id #

get_substation_buses_from_bus_id(
    network, start_bus_id, only_closed_switches=False
)

Get all buses of a substation from a start bus id.

This function will return all buses that are connected to the start bus id via switches. Note: The input expects a bus ids only containing the busbars you want to get the connection for. See diagram for references. e.g:: input [BB1, BB2] -> get BC½ input [BB1, BB2, BB3, BB4] -> get BC½, BC¾, CC⅓, CC2/4

PARAMETER DESCRIPTION
network

The pandapower network to get the substation buses from.

TYPE: pandapowerNet

start_bus_id

The bus id to start the search from.

TYPE: int

only_closed_switches

If True, only closed switches are considered.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
set[int]

A set of bus ids that are connected to the start bus id.

RAISES DESCRIPTION
RuntimeError

If the function detects an infinite loop.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_substation_buses_from_bus_id(
    network: pp.pandapowerNet, start_bus_id: int, only_closed_switches: bool = False
) -> set[int]:
    """Get all buses of a substation from a start bus id.

    This function will return all buses that are connected to the start bus id via switches.
    Note: The input expects a bus ids only containing the busbars you want to get the connection for.
    See diagram for references. e.g::
    input [BB1, BB2] -> get BC1/2
    input [BB1, BB2, BB3, BB4] -> get BC1/2, BC3/4, CC1/3, CC2/4

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network to get the substation buses from.
    start_bus_id: int
        The bus id to start the search from.
    only_closed_switches: bool
        If True, only closed switches are considered.

    Returns
    -------
    set[int]
        A set of bus ids that are connected to the start bus id.

    Raises
    ------
    RuntimeError
        If the function detects an infinite loop.
    """
    station_buses = {start_bus_id}
    len_station = len(station_buses)
    len_update = 0
    break_counter = 0
    max_loop_count = 25
    while len_station != len_update:
        len_station = len(station_buses)
        update_bus = pp.toolbox.get_connected_buses(
            network, station_buses, consider="s", respect_switches=only_closed_switches
        )
        station_buses.update(update_bus)
        len_update = len(station_buses)
        break_counter += 1
        # maximum hops is 7 for a standard substation as drawn the module header if you start at a branch
        if break_counter > max_loop_count:
            raise RuntimeError(
                "Infinite loop detected, please check the network model. "
                + f"Substation: {network.bus.loc[start_bus_id, 'name']}, with bus_id: {start_bus_id}"
            )
    return station_buses

get_type_b_nodes #

get_type_b_nodes(network, substation_bus_list=None)

Get all nodes of type 'b' (busbar) in a network or substation.

PARAMETER DESCRIPTION
network

The pandapower network to get the busbars from.

TYPE: pandapowerNet

substation_bus_list

The bus ids of the substation.

TYPE: Optional[list[int]] DEFAULT: None

RETURNS DESCRIPTION
DataFrame

A DataFrame with all busbars of type 'b' in the substation.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pandapower_toolset_node_breaker.py
def get_type_b_nodes(network: pp.pandapowerNet, substation_bus_list: Optional[list[int]] = None) -> pd.DataFrame:
    """Get all nodes of type 'b' (busbar) in a network or substation.

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network to get the busbars from.
    substation_bus_list: Optional[list[int]]
        The bus ids of the substation.

    Returns
    -------
    pd.DataFrame
        A DataFrame with all busbars of type 'b' in the substation.
    """
    if substation_bus_list is None:
        substation_bus_list = network.bus.index
    substation_buses = network.bus.loc[substation_bus_list]
    bus_type_b = substation_buses[substation_buses.type == "b"]
    return bus_type_b

get_relevant_subs #

get_relevant_subs(
    network,
    region="",
    voltage_level=150,
    min_branches_per_station=4,
    exclude_stations=None,
    substation_column="substat",
    min_busbars_per_substation=2,
    min_busbar_coupler_per_station=1,
)

Create the network masks for the pandapower network.

PARAMETER DESCRIPTION
network

The pandapower network.

TYPE: pandapowerNet

region

The region of the network.

TYPE: str DEFAULT: ''

voltage_level

The voltage level of the network to be considered.

TYPE: float DEFAULT: 150

min_branches_per_station

The minimum number of busbars per station to be relevant.

TYPE: int DEFAULT: 4

exclude_stations

The substations to exclude.

TYPE: Optional[list[str]] DEFAULT: None

substation_column

The column containing the substation.

TYPE: str DEFAULT: 'substat'

min_busbars_per_substation

The minimum number of busbars per substation to be relevant. Note: you need a substation column in the bus dataframe. Note: if you merged the busbars, you need to set this to 1. Note: set to 0 and ignore busbars and branches

TYPE: int DEFAULT: 2

min_busbar_coupler_per_station

The minimum number of busbar coupler per station to be relevant. Note: you need a substation column in the bus dataframe. Note: if you merged the busbars, you need to set this to 0.

TYPE: int DEFAULT: 1

RETURNS DESCRIPTION
ndarray

The relevant substations.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pp_masks.py
def get_relevant_subs(
    network: pp.pandapowerNet,
    region: str = "",
    voltage_level: float = 150,
    min_branches_per_station: int = 4,
    exclude_stations: Optional[list[str]] = None,
    substation_column: str = "substat",
    min_busbars_per_substation: int = 2,
    min_busbar_coupler_per_station: int = 1,
) -> NetworkMasks:
    """Create the network masks for the pandapower network.

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network.
    region: str
        The region of the network.
    voltage_level: float
        The voltage level of the network to be considered.
    min_branches_per_station: int
        The minimum number of busbars per station to be relevant.
    exclude_stations: Optional[list[str]]
        The substations to exclude.
    substation_column: str
        The column containing the substation.
    min_busbars_per_substation: int
        The minimum number of busbars per substation to be relevant.
        Note: you need a substation column in the bus dataframe.
        Note: if you merged the busbars, you need to set this to 1.
        Note: set to 0 and ignore busbars and branches
    min_busbar_coupler_per_station: int
        The minimum number of busbar coupler per station to be relevant.
        Note: you need a substation column in the bus dataframe.
        Note: if you merged the busbars, you need to set this to 0.

    Returns
    -------
    np.ndarray
        The relevant substations.
    """
    region_busses = network.bus.zone == region
    hv_grid_busses = network.bus.vn_kv >= voltage_level

    if substation_column in network.bus.columns:
        if min_busbars_per_substation > 0:
            mask_multiple_busbars = mask_min_busbar_per_station(network, min_busbars_per_substation, substation_column)
        else:
            mask_multiple_busbars = np.ones(len(network.bus), dtype=bool)

        if min_branches_per_station > 0:
            mask_min_branches = mask_min_branches_per_station(network, substation_column, min_branches_per_station)
        else:
            mask_min_branches = np.ones(len(network.bus), dtype=bool)

        if min_busbar_coupler_per_station > 0:
            mask_multiple_busbar_coupler = mask_min_busbar_coupler(
                network, min_busbar_coupler_per_station, substation_column
            )
        else:
            mask_multiple_busbar_coupler = np.ones(len(network.bus), dtype=bool)
    else:
        mask_multiple_busbars = np.ones(len(network.bus), dtype=bool)
        mask_min_branches = count_branches_at_buses(network, network.bus.index) >= min_branches_per_station
        mask_multiple_busbar_coupler = np.ones(len(network.bus), dtype=bool)

    if exclude_stations is not None and substation_column in network.bus.columns:
        exclude_mask = ~network.bus[substation_column].isin(exclude_stations)
    else:
        exclude_mask = np.ones(len(network.bus), dtype=bool)

    filtered_busbars = network.bus[
        hv_grid_busses
        & region_busses
        & exclude_mask
        & mask_multiple_busbars
        & mask_multiple_busbar_coupler
        & mask_min_branches
    ]
    relevant_subs = np.isin(network.bus.index, filtered_busbars.index)

    return relevant_subs

make_pp_masks #

make_pp_masks(
    network,
    region="",
    voltage_level=150,
    min_power=100.0,
    trafo_weight=1.2,
    cross_border_weight=1.2,
    min_branches_per_station=4,
    foreign_id_column="equipment",
    exclude_stations=None,
    substation_column="substat",
    min_busbars_per_substation=2,
    min_busbar_coupler_per_station=1,
)

Create the network masks for the pandapower network.

PARAMETER DESCRIPTION
network

The pandapower network.

TYPE: pandapowerNet

region

The region of the network.

TYPE: str DEFAULT: ''

voltage_level

The voltage level of the network to be considered.

TYPE: float DEFAULT: 150

min_power

The minimum power of generators, loads and static generators to be considered, for the n-1 analysis and reassignable.

TYPE: float DEFAULT: 100.0

trafo_weight

The weight of the transformers for the overload.

TYPE: float DEFAULT: 1.2

cross_border_weight

The weight of the cross border lines for the overload.

TYPE: float DEFAULT: 1.2

min_branches_per_station

The minimum number of busbars per station to be relevant.

TYPE: int DEFAULT: 4

foreign_id_column

The column containing the foreign id.

TYPE: str DEFAULT: 'equipment'

exclude_stations

The substations to exclude.

TYPE: Optional[list[str]] DEFAULT: None

substation_column

The column containing the substation.

TYPE: str DEFAULT: 'substat'

min_busbars_per_substation

The minimum number of busbars per substation to be relevant. Note: you need a substation column in the bus dataframe. Note: if you merged the busbars, you need to set this to 1.

TYPE: int DEFAULT: 2

min_busbar_coupler_per_station

The minimum number of busbar coupler per station to be relevant. Note: you need a substation column in the bus dataframe. Note: if you merged the busbars, you need to set this to 0.

TYPE: int DEFAULT: 1

RETURNS DESCRIPTION
NetworkMasks

The network masks.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/pp_masks.py
def make_pp_masks(
    network: pp.pandapowerNet,
    region: str = "",
    voltage_level: float = 150,
    min_power: float = 100.0,
    trafo_weight: float = 1.2,
    cross_border_weight: float = 1.2,
    min_branches_per_station: int = 4,
    foreign_id_column: str = "equipment",
    exclude_stations: Optional[list[str]] = None,
    substation_column: str = "substat",
    min_busbars_per_substation: int = 2,
    min_busbar_coupler_per_station: int = 1,
) -> NetworkMasks:
    """Create the network masks for the pandapower network.

    Parameters
    ----------
    network: pp.pandapowerNet
        The pandapower network.
    region: str
        The region of the network.
    voltage_level: float
        The voltage level of the network to be considered.
    min_power: float
        The minimum power of generators, loads and static generators to be considered,
        for the n-1 analysis and reassignable.
    trafo_weight: float
        The weight of the transformers for the overload.
    cross_border_weight: float
        The weight of the cross border lines for the overload.
    min_branches_per_station: int
        The minimum number of busbars per station to be relevant.
    foreign_id_column: str
        The column containing the foreign id.
    exclude_stations: Optional[list[str]]
        The substations to exclude.
    substation_column: str
        The column containing the substation.
    min_busbars_per_substation: int
        The minimum number of busbars per substation to be relevant.
        Note: you need a substation column in the bus dataframe.
        Note: if you merged the busbars, you need to set this to 1.
    min_busbar_coupler_per_station: int
        The minimum number of busbar coupler per station to be relevant.
        Note: you need a substation column in the bus dataframe.
        Note: if you merged the busbars, you need to set this to 0.


    Returns
    -------
    NetworkMasks
        The network masks.
    """
    region_busses = network.bus.zone == region
    hv_grid_busses = network.bus.vn_kv >= voltage_level
    region_hv_bus_indices = network.bus.index[region_busses & hv_grid_busses]

    line_for_reward = (
        # Only high voltage lines
        (network.line.from_bus.isin(region_hv_bus_indices)) | (network.line.to_bus.isin(region_hv_bus_indices))
    )

    if foreign_id_column in network.line.columns:
        line_for_reward &= (
            # Only lines with FID
            network.line[foreign_id_column].notna()
        )
    line_for_nminus1 = line_for_reward
    if substation_column in network.bus.columns:
        line_for_reward &= (
            # Only lines that connect different substations
            network.bus.loc[network.line.from_bus.values].substat.values
            != network.bus.loc[network.line.to_bus.values].substat.values
        )

    trafo_for_reward = network.trafo.hv_bus.isin(
        # Only trafo with high voltage the specified region
        region_hv_bus_indices
    ) | network.trafo.lv_bus.isin(region_hv_bus_indices)
    if foreign_id_column in network.trafo.columns:
        trafo_for_reward &= (
            # Only trafo with FID
            network.trafo[foreign_id_column].notna()
        )
    trafo_for_nminus1 = trafo_for_reward
    trafo3w_for_reward = (
        network.trafo3w.hv_bus.isin(region_hv_bus_indices)
        | network.trafo3w.mv_bus.isin(region_hv_bus_indices)
        | network.trafo3w.lv_bus.isin(region_hv_bus_indices)
    )
    if foreign_id_column in network.trafo3w.columns:
        trafo3w_for_reward &= (
            # Only trafo3w with FID
            network.trafo3w[foreign_id_column].notna()
        )
    trafo3w_for_nminus1 = trafo3w_for_reward
    load_for_nminus1 = network.load.bus.isin(region_hv_bus_indices) & (network.load.p_mw >= min_power)
    if foreign_id_column in network.load.columns:
        load_for_nminus1 &= (
            # Only loads with FID
            network.load[foreign_id_column].notna()
        )

    gen_for_nminus1 = network.gen.bus.isin(region_hv_bus_indices) & (network.gen.p_mw >= min_power)
    if foreign_id_column in network.gen.columns:
        gen_for_nminus1 &= (
            # Only generators with FID
            network.gen[foreign_id_column].notna()
        )

    sgen_for_nminus1 = network.sgen.bus.isin(region_hv_bus_indices) & (network.sgen.p_mw >= min_power)
    if foreign_id_column in network.sgen.columns:
        sgen_for_nminus1 &= (
            # Only static generators with FID
            network.sgen[foreign_id_column].notna()
        )

    dso_trafos = network.trafo.hv_bus.isin(region_hv_bus_indices) & ~network.trafo.lv_bus.isin(region_hv_bus_indices)
    if foreign_id_column in network.trafo.columns:
        dso_trafos &= (
            # Only trafo with FID
            network.trafo[foreign_id_column].notna()
        )

    trafo_overload_weight = np.ones(len(network.trafo))
    trafo_overload_weight[dso_trafos] *= trafo_weight
    dso_trafo3ws = network.trafo3w.hv_bus.isin(region_hv_bus_indices) & (
        ~network.trafo3w.lv_bus.isin(region_hv_bus_indices) | ~network.trafo3w.lv_bus.isin(region_hv_bus_indices)
    )
    if foreign_id_column in network.trafo3w.columns:
        dso_trafo3ws &= (
            # Only trafo3w with FID
            network.trafo3w[foreign_id_column].notna()
        )

    trafo3w_overload_weight = np.ones(len(network.trafo3w))
    trafo3w_overload_weight[dso_trafo3ws] *= trafo_weight
    cross_border_lines = (
        network.line.from_bus.isin(region_hv_bus_indices) & ~network.line.to_bus.isin(region_hv_bus_indices)
    ) | (network.line.to_bus.isin(region_hv_bus_indices) & ~network.line.from_bus.isin(region_hv_bus_indices))
    line_overload_weight = np.ones(len(network.line))
    line_overload_weight[cross_border_lines] *= cross_border_weight

    relevant_subs = get_relevant_subs(
        network=network,
        region=region,
        voltage_level=voltage_level,
        min_branches_per_station=min_branches_per_station,
        exclude_stations=exclude_stations,
        substation_column=substation_column,
        min_busbars_per_substation=min_busbars_per_substation,
        min_busbar_coupler_per_station=min_busbar_coupler_per_station,
    )

    masks = NetworkMasks(
        relevant_subs=relevant_subs,
        line_for_nminus1=line_for_nminus1.values,
        line_for_reward=line_for_reward.values,
        line_overload_weight=line_overload_weight,
        line_disconnectable=line_for_reward.values,
        trafo_for_nminus1=trafo_for_nminus1.values,
        trafo_for_reward=trafo_for_reward.values,
        trafo_overload_weight=trafo_overload_weight,
        trafo_disconnectable=trafo_for_reward.values,
        trafo3w_for_nminus1=trafo3w_for_nminus1.values,
        trafo3w_for_reward=trafo3w_for_reward.values,
        trafo3w_overload_weight=trafo3w_overload_weight,
        trafo3w_disconnectable=trafo3w_for_reward.values,
        generator_for_nminus1=gen_for_nminus1.values,
        sgen_for_nminus1=sgen_for_nminus1.values,
        load_for_nminus1=load_for_nminus1.values,
    )
    return masks

preprocess_net_step1 #

preprocess_net_step1(net)

General preprocessing - e.g. a PowerFactory network may converge in AC -> change elements.

Step 1: General preprocessing - select connected subnet - Remove zero branches - remove out of service elements - handle_constant_z_load - drop elements connected to one bus - replace xward by internal elements - replace ward by internal elements - drop controler

PARAMETER DESCRIPTION
net

pandapower network Note: the network is modified in place

TYPE: pandapowerNet

RETURNS DESCRIPTION
net

modified pandapower network

TYPE: pandapowerNet

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py
def preprocess_net_step1(net: pp.pandapowerNet) -> pp.pandapowerNet:
    """General preprocessing - e.g. a PowerFactory network may converge in AC -> change elements.

    Step 1: General preprocessing
        - select connected subnet
        - Remove zero branches
        - remove out of service elements
        - handle_constant_z_load
        - drop elements connected to one bus
        - replace xward by internal elements
        - replace ward by internal elements
        - drop controler

    Parameters
    ----------
    net : pp.pandapowerNet
        pandapower network
        Note: the network is modified in place

    Returns
    -------
    net : pp.pandapowerNet
        modified pandapower network


    """
    # preprocessing: remove zero branches, fuse closed switches, remove out of service elements
    net = select_connected_subnet(net)
    replace_zero_branches(net)
    remove_out_of_service(net)
    # sometimes if the load is 100% constan z it will not converge -> investigate, cosinder setting to 99%
    modify_constan_z_load(net)
    drop_elements_connected_to_one_bus(net)
    pp.replace_xward_by_internal_elements(net)
    pp.replace_ward_by_internal_elements(net)
    validate_trafo_model(net)
    if "controler" in net:
        del net["controler"]

    return net

preprocess_net_step2 #

preprocess_net_step2(network, topology_model)

Step 2: after creation of the asset topology.

1
2
3
4
5
6
- fuse buses with closed switches
- remove switches
- delete bus_geodata as it is not up to date after fusing buses
- create continuous bus index (asset topology index and bus index do not match)
- update station ids in asset topology
-> conversion to bus-brach model completed
PARAMETER DESCRIPTION
network

pandapower network Note: the network is modified in place

TYPE: pandapowerNet

topology_model

asset topology model

TYPE: Topology

RETURNS DESCRIPTION
topology_model

modified asset topology model

TYPE: Topology

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py
def preprocess_net_step2(network: pp.pandapowerNet, topology_model: Topology) -> Topology:
    """Step 2: after creation of the asset topology.

        - fuse buses with closed switches
        - remove switches
        - delete bus_geodata as it is not up to date after fusing buses
        - create continuous bus index (asset topology index and bus index do not match)
        - update station ids in asset topology
        -> conversion to bus-brach model completed

    Parameters
    ----------
    network : pp.pandapowerNet
        pandapower network
        Note: the network is modified in place
    topology_model : Topology
        asset topology model

    Returns
    -------
    topology_model : Topology
        modified asset topology model

    """
    handle_switches(network)
    drop_elements_connected_to_one_bus(network)
    if "bus_geodata" in network:
        del network["bus_geodata"]
    old_index = pp.toolbox.create_continuous_bus_index(network, start=0, store_old_index=True)
    for station in topology_model.stations:
        station_id = int(station.grid_model_id.split(SEPARATOR)[0])
        new_id = old_index[station_id]
        station.grid_model_id = f"{new_id}{SEPARATOR}{station.grid_model_id.split(SEPARATOR)[1]}"
    return topology_model

validate_asset_topology #

validate_asset_topology(net, topology_model)

Validate the asset topology with the network.

PARAMETER DESCRIPTION
net

pandapower network

TYPE: pandapowerNet

topology_model

asset topology model

TYPE: Topology

RETURNS DESCRIPTION
None
RAISES DESCRIPTION
ValueError

if the number of connections in the network does not match the number of assets in the station

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/preprocessing.py
def validate_asset_topology(net: pp.pandapowerNet, topology_model: Topology) -> None:
    """Validate the asset topology with the network.

    Parameters
    ----------
    net : pp.pandapowerNet
        pandapower network
    topology_model : Topology
        asset topology model

    Returns
    -------
    None

    Raises
    ------
    ValueError
        if the number of connections in the network does not match the number of assets in the station
    """
    for station in topology_model.stations:
        s_id = int(station.grid_model_id.split(r"%%")[0])
        connection_dict = pp.toolbox.get_connected_elements_dict(net, [s_id])
        del connection_dict["bus"]
        len_connection = len([element for key in connection_dict for element in connection_dict[key]])
        if len_connection != len(station.assets):
            logger.warning(
                f"Station {station.grid_model_id} has {len(station.assets)} assets but only "
                + f"{len_connection} connections in the network"
            )
            logger.warning(connection_dict)
            for asset in station.assets:
                logger.warning(asset)
            raise ValueError(
                f"Station {station.grid_model_id} has {len(station.assets)} assets but only "
                + f"{len_connection} connections in the network"
            )

toop_engine_importer.pandapower_import.asset_topology #

Module contains functions to translate the pandapower model to the asset topology model.

File: asset_topology.py Author: Benjamin Petrick Created: 2024-10-01

logger module-attribute #

logger = Logger(__name__)

get_busses_from_station #

get_busses_from_station(
    network,
    station_name=None,
    station_col="substat",
    station_bus_index=None,
    foreign_key="equipment",
)

Get the busses from a station_name.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_name

Station id for which the busses should be retrieved.

TYPE: Optional[Union[str, int, float]] DEFAULT: None

station_col

Column name in the bus DataFrame that contains the station_name.

TYPE: str DEFAULT: 'substat'

station_bus_index

List of bus indices for which the busses should be retrieved.

TYPE: Optional[Union[list[int], int]] DEFAULT: None

foreign_key

Defines the column name that is used as the foreign_key.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
station_busses

DataFrame with the busses of the station_name. Note: The DataFrame columns are the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus. Note: the function does not check for types e.g. 'n' (Node) or 'b' (Busbar).

TYPE: DataFrame

RAISES DESCRIPTION
ValueError:

If station_name and station_bus_index are None.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_busses_from_station(
    network: pp.pandapowerNet,
    station_name: Optional[Union[str, int, float]] = None,
    station_col: str = "substat",
    station_bus_index: Optional[Union[list[int], int]] = None,
    foreign_key: str = "equipment",
) -> pd.DataFrame:
    """Get the busses from a station_name.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_name: Optional[Union[str, int, float]]
        Station id for which the busses should be retrieved.
    station_col: str
        Column name in the bus DataFrame that contains the station_name.
    station_bus_index: Optional[Union[list[int], int]]
        List of bus indices for which the busses should be retrieved.
    foreign_key: str
        Defines the column name that is used as the foreign_key.

    Returns
    -------
    station_busses: pd.DataFrame
        DataFrame with the busses of the station_name.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.
        Note: the function does not check for types e.g. 'n' (Node) or 'b' (Busbar).

    Raises
    ------
    ValueError:
        If station_name and station_bus_index are None.

    """
    bus_df = get_station_bus_df(
        network=network,
        station_name=station_name,
        station_col=station_col,
        station_bus_index=station_bus_index,
    )
    bus_df["grid_model_id"] = bus_df.index.astype(str) + SEPARATOR + "bus"
    bus_df["int_id"] = bus_df.index
    # equipment col is the foreign key (unique) in powerfactory
    if foreign_key in bus_df.columns:
        bus_df["name"] = bus_df[foreign_key]
    station_busses = bus_df[["grid_model_id", "type", "name", "int_id", "in_service"]]

    if foreign_key in station_busses.columns:
        # force behavior: the index of the DataFrame is the internal id of the bus
        assert all(network.bus.loc[station_busses.index, foreign_key] == station_busses["name"])
    return station_busses

get_coupler_from_station #

get_coupler_from_station(
    network, station_buses, foreign_key="equipment"
)

Get the coupler elements from a station_name.

This function expects couplers between all busbars of type 'b' (Busbar). Merge cross couplers before using this function.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_buses

DataFrame with the busses of the station_name. Note: The DataFrame columns are the same as in the pydantic model. Note: The index of the DataFrame is the internal id of the bus. Note: The function expects all nodes of the station, inlc. type 'n' (Node) and 'b' (Busbar).

TYPE: DataFrame

foreign_key

Defines the column name that is used as the foreign_key.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
station_switches_CB

DataFrame with the coupler elements of the station_name. Note: The DataFrame columns are the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus.

TYPE: DataFrame

RAISES DESCRIPTION
ValueError:

If no busbar coupler is found between the busbars. If the station_busses are not of type 'b' (Busbar). If the coupler elements are not of type 'CB' (Circuit Breaker).

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_coupler_from_station(
    network: pp.pandapowerNet,
    station_buses: pd.DataFrame,
    foreign_key: str = "equipment",
) -> pd.DataFrame:
    """Get the coupler elements from a station_name.

    This function expects couplers between all busbars of type 'b' (Busbar).
    Merge cross couplers before using this function.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_buses: pd.DataFrame
        DataFrame with the busses of the station_name.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: The index of the DataFrame is the internal id of the bus.
        Note: The function expects all nodes of the station, inlc. type 'n' (Node) and 'b' (Busbar).
    foreign_key: str
        Defines the column name that is used as the foreign_key.

    Returns
    -------
    station_switches_CB: pd.DataFrame
        DataFrame with the coupler elements of the station_name.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.

    Raises
    ------
    ValueError:
        If no busbar coupler is found between the busbars.
        If the station_busses are not of type 'b' (Busbar).
        If the coupler elements are not of type 'CB' (Circuit Breaker).
    """
    station_switches = get_all_switches_from_bus_ids(network=network, bus_ids=station_buses.index)
    # get all busbars of type 'b'
    bus_type_b = station_buses[station_buses["type"] == "b"]
    # create a list of all possible busbar combinations
    busbar_combinations = [(bus_1, bus_2) for i, bus_1 in enumerate(bus_type_b.index) for bus_2 in bus_type_b.index[i + 1 :]]
    switch_ids = []
    switch_bus = []
    # iterate over all busbar combinations and check if they are connected by a switch
    for bus_1, bus_2 in busbar_combinations:
        # check if the busbars are directly connected by a switch
        direct_connection = pp.toolbox.get_connecting_branches(network, [bus_1], [bus_2])
        if list(direct_connection.keys()) == ["switch"]:
            switch_ids.append(next(iter(direct_connection["switch"])))
            switch_bus.append((bus_1, bus_2))
        else:
            # check if the busbars are indirectly connected by a switch
            indirect_connection = get_indirect_connected_switch(network, bus_1, bus_2, consider_three_buses=True)
            if list(indirect_connection.keys()) == ["switch"]:
                for switch_id in indirect_connection["switch"]:
                    switch_ids.append(switch_id)
                    switch_bus.append((bus_1, bus_2))
            else:
                raise ValueError(
                    f"Busbars {bus_1} and {bus_2} are not or not only connected by a switch. "
                    + f"Element: {indirect_connection}. Busbar:{bus_type_b.iloc[0].to_dict()}"
                )

    # verify that the switches are of type CB
    station_switches_cb = station_switches[station_switches.index.isin(switch_ids)]
    if len(switch_ids) != len(station_switches_cb):
        raise ValueError(f"switch_id {switch_ids} does not match station_switches_CB {station_switches_cb.index}")
    if not all(station_switches_cb["type"] == "CB"):
        raise ValueError(
            f"switches {station_switches_cb.index} are not of type CB, but {station_switches_cb['type'].unique()}"
        )

    # modify the station_switches_CB DataFrame to match the pydantic model
    # -> rename bus ids to match the busbars of the station
    for index, switch_id in enumerate(switch_ids):
        bus_1, bus_2 = switch_bus[index]
        station_switches_cb.at[switch_id, "element"] = bus_1
        station_switches_cb.at[switch_id, "bus"] = bus_2

    # translate closed to open convention
    station_switches_cb["closed"] = ~station_switches_cb["closed"]
    station_switches_cb.rename(columns={"closed": "open"}, inplace=True)
    # rename columns to pydantic model
    station_switches_cb = station_switches_cb.rename(columns={"bus": "busbar_from_id", "element": "busbar_to_id"})
    station_switches_cb["grid_model_id"] = station_switches_cb.index.astype(str) + SEPARATOR + "switch"
    if "in_service" not in station_switches_cb.columns:
        station_switches_cb["in_service"] = True
    # equipment col is the foreign key (unique) in powerfactory
    if foreign_key in station_switches_cb.columns:
        station_switches_cb["name"] = station_switches_cb[foreign_key]
    station_switches_cb = station_switches_cb[
        [
            "grid_model_id",
            "type",
            "name",
            "busbar_from_id",
            "busbar_to_id",
            "open",
            "in_service",
        ]
    ]
    return station_switches_cb

get_branches_from_station #

get_branches_from_station(
    network,
    station_buses,
    branch_types=None,
    bus_types=None,
    foreign_key="equipment",
)

Get the branches from a station_buses index.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_buses

DataFrame with one or multiple busses to get the bus id from. Note: The DataFrame columns are the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus.

TYPE: DataFrame

branch_types

List of branch types that should be retrieved. It needs to be an attribute of the pandapower network.

TYPE: Optional[List[str]] DEFAULT: None

bus_types

List of the tuple(name_of_column, pydantic_type, postfix_gridmodel_id). This list is used to identify the bus columns in the branch types.

TYPE: Optional[List[Tuple[str, Optional[str], str]]] DEFAULT: None

foreign_key

Defines the column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
station_branches

DataFrame with the branches of the station_name. Note: The DataFrame columns are the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus.

TYPE: DataFrame

switching_matrix

DataFrame with the switching matrix of the station_name. Note: The DataFrame columns are the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus.

TYPE: ndarray

RAISES DESCRIPTION
ValueError:

If the branch type is not found in the pp.pandapowerNet. Note: trying to call an attribute with an empty DataFrame will not raise an error. e.g. pp.networks.case4gs()["impedance"].empty == True will not raise an error. But calling an "not_existing_attribute" will raise an error. If any bus name from bus_from_to_names is not found in the branch type.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_branches_from_station(  # noqa: PLR0912, C901
    network: pp.pandapowerNet,
    station_buses: pd.DataFrame,
    branch_types: Optional[List[str]] = None,
    bus_types: Optional[List[Tuple[str, Optional[str], str]]] = None,
    foreign_key: str = "equipment",
) -> Tuple[pd.DataFrame, np.ndarray, List[AssetBay]]:
    """Get the branches from a station_buses index.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_buses: pd.DataFrame
        DataFrame with one or multiple busses to get the bus id from.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.
    branch_types: List[str]
        List of branch types that should be retrieved.
        It needs to be an attribute of the pandapower network.
    bus_types: List[Tuple[str, Optional[str], str]]
        List of the tuple(name_of_column, pydantic_type, postfix_gridmodel_id).
        This list is used to identify the bus columns in the branch types.
    foreign_key: str
        Defines the column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    station_branches: pd.DataFrame
        DataFrame with the branches of the station_name.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.
    switching_matrix: np.ndarray
        DataFrame with the switching matrix of the station_name.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.

    Raises
    ------
    ValueError:
        If the branch type is not found in the pp.pandapowerNet.
            Note: trying to call an attribute with an empty DataFrame will not
            raise an error. e.g. pp.networks.case4gs()["impedance"].empty == True
            will not raise an error. But calling an "not_existing_attribute" will raise an error.
        If any bus name from bus_from_to_names is not found in the branch type.
    """
    if branch_types is None:
        branch_types = [
            "line",
            "trafo",
            "trafo3w",
            "load",
            "gen",
            "sgen",
            "impedance",
            "shunt",
        ]
    if bus_types is None:
        bus_types = [
            ("bus", None, ""),
            ("from_bus", "from", ""),
            ("to_bus", "to", ""),
            ("hv_bus", "hv", "_hv"),
            ("lv_bus", "lv", "_lv"),
            ("mv_bus", "mv", "_mv"),
        ]

    bus_ids = station_buses.index
    bus_type_b = station_buses[station_buses["type"] == "b"]
    branch_data = []
    asset_connection_list = []
    # get all branches
    for branch_type in branch_types:
        if not hasattr(network, branch_type):
            raise ValueError(f"Branch type {branch_type} not found in pandapower network")

        branch_df = getattr(network, branch_type)
        branch_df_all_busses = get_branch_from_bus_ids(
            branch_df=branch_df,
            branch_type=branch_type,
            bus_ids=bus_ids,
            bus_types=bus_types,
        )

        # get connection path to busbars
        for index, branch in branch_df_all_busses.iterrows():
            if branch["bus_int_id"] in bus_ids:
                asset_bus = branch["bus_int_id"]
            else:
                raise ValueError(f"Branch {index} is not connected to the station busses {bus_ids}")

            if branch["bus_int_id"] not in bus_type_b.index:
                # asset is not directly connected to a busbar -> get connection path
                asset_connection = get_asset_connection_path_to_busbars(
                    network=network,
                    asset_bus=asset_bus,
                    station_buses=station_buses,
                    save_col_name=foreign_key,
                )

                # change bus_int_id to the final busbar
                final_bus_dict = asset_connection.sr_switch_grid_model_id
                closed_sr_switches = get_closed_switch(
                    network.switch,
                    column=foreign_key,
                    column_ids=final_bus_dict.values(),
                )
                closed_dv_switches = get_closed_switch(
                    network.switch,
                    column=foreign_key,
                    column_ids=[asset_connection.dv_switch_grid_model_id],
                )
                closed_sl_switches = get_closed_switch(
                    network.switch,
                    column=foreign_key,
                    column_ids=[asset_connection.sl_switch_grid_model_id],
                )
                if (
                    (len(closed_sr_switches) == 0)
                    or (len(closed_dv_switches) == 0)
                    or ((len(closed_sl_switches) == 0) and (asset_connection.sl_switch_grid_model_id is not None))
                ):
                    logger.warning(
                        "No closed switch found (Element is disconnected and will be dropped) for "
                        + f"element_type:{branch_type} element: {branch.to_dict()}."
                    )
                    # if asset is not connected -> -1
                    branch_df_all_busses.loc[index, "bus_int_id"] = -1
                    # do not append asset_connection -> asset is disconnected and will be dropped
                else:
                    if len(closed_sr_switches) > 1:
                        logger.warning(
                            f"Expected one closed switch for element_type:{branch_type} element: {branch.to_dict()}, "
                            + f"got {len(closed_sr_switches)} switches: {closed_sr_switches.to_dict()}. Using the first one."
                        )
                        closed_sr_switches = closed_sr_switches.iloc[[0]]
                    final_bus = [  # noqa: RUF015
                        i for i in final_bus_dict if final_bus_dict[i] == closed_sr_switches[foreign_key].values[0]
                    ][0]
                    branch_df_all_busses.loc[index, "bus_int_id"] = int(final_bus.split(SEPARATOR)[0])
                    asset_connection_list.append(asset_connection)
            else:
                # asset is directly connected to a busbar
                # -> bus_int_id is already the final busbar
                asset_connection_list.append(None)
        # drop all assets that are not connected to the busbars
        branch_df_all_busses = branch_df_all_busses[branch_df_all_busses["bus_int_id"] != -1]
        if "in_service" not in branch_df_all_busses.columns:
            branch_df_all_busses["in_service"] = True

        # get columns for pydantic model
        branch_df_all_busses = branch_df_all_busses[
            ["grid_model_id", "type", "name", "bus_int_id", "branch_end", "in_service"]
        ]
        branch_data.append(branch_df_all_busses)

    station_branches = pd.concat(branch_data)
    # create switching matrix
    switching_matrix = get_asset_switching_table(station_buses=bus_type_b, station_elements=station_branches)
    # keep only relevant columns
    station_branches = station_branches[["grid_model_id", "type", "name", "branch_end", "in_service"]]
    return station_branches, switching_matrix, asset_connection_list

get_parameter_from_station #

get_parameter_from_station(
    network,
    station_name=None,
    station_col="substat",
    station_bus_index=None,
    parameter="vn_kv",
)

Get the voltage level from a station_name.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_name

Station id for which the busses should be retrieved.

TYPE: Optional[Union[str, int, float]] DEFAULT: None

station_col

Column name in the bus DataFrame that contains the station_name.

TYPE: str DEFAULT: 'substat'

station_bus_index

List of bus indices for which the busses should be retrieved.

TYPE: Optional[Union[list[int], int]] DEFAULT: None

parameter

Parameter that should be retrieved.

TYPE: Literal['vn_kv', 'zone'] DEFAULT: 'vn_kv'

RETURNS DESCRIPTION
parameter

Parameter value.

TYPE: Union[float, int, str]

RAISES DESCRIPTION
ValueError:

If station_name and station_bus_index are None. If the parameter is not found in the bus_df. If the voltage level is not unique for the station_name.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_parameter_from_station(
    network: pp.pandapowerNet,
    station_name: Optional[Union[str, int, float]] = None,
    station_col: str = "substat",
    station_bus_index: Optional[Union[list[int], int]] = None,
    parameter: Literal["vn_kv", "zone"] = "vn_kv",
) -> Union[float, int, str]:
    """Get the voltage level from a station_name.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_name: Optional[Union[str, int, float]]
        Station id for which the busses should be retrieved.
    station_col: str
        Column name in the bus DataFrame that contains the station_name.
    station_bus_index: Optional[Union[list[int], int]]
        List of bus indices for which the busses should be retrieved.
    parameter: Literal["vn_kn", "zone"]
        Parameter that should be retrieved.

    Returns
    -------
    parameter: Union[float, int, str]
        Parameter value.

    Raises
    ------
    ValueError:
        If station_name and station_bus_index are None.
        If the parameter is not found in the bus_df.
        If the voltage level is not unique for the station_name.
    """
    bus_df = get_station_bus_df(
        network=network,
        station_name=station_name,
        station_col=station_col,
        station_bus_index=station_bus_index,
    )
    if parameter not in bus_df.columns:
        raise ValueError(f"parameter '{parameter}' not found in bus_df with columns {bus_df.columns}")
    if len(bus_df[parameter].unique()) != 1:
        raise ValueError(f"parameter '{parameter}' is not unique for station {station_name}: {bus_df[parameter].unique()}")
    parameter = bus_df[parameter].unique()[0]
    return parameter

get_station_from_id #

get_station_from_id(
    network, station_id_list, foreign_key="equipment"
)

Get the busses from a station_id.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_id_list

List of station ids for which the stations should be retrieved.

TYPE: list[int]

foreign_key

Defines the column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
station

Station object.

TYPE: Station

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_station_from_id(
    network: pp.pandapowerNet,
    station_id_list: list[int],
    foreign_key: str = "equipment",
) -> Station:
    """Get the busses from a station_id.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_id_list: list[int]
        List of station ids for which the stations should be retrieved.
    foreign_key: str
        Defines the column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    station: Station
        Station object.
    """
    station_buses = get_busses_from_station(network, station_bus_index=station_id_list)
    coupler_elements = get_coupler_from_station(network, station_buses)
    (
        station_branches,
        switching_matrix,
        asset_connection_path,
    ) = get_branches_from_station(network, station_buses, foreign_key=foreign_key)

    # get the lists of the pydantic model objects
    busbar_list = get_list_of_busbars_from_df(station_buses[station_buses["type"] == "b"])
    coupler_list = get_list_of_coupler_from_df(coupler_elements)
    switchable_assets_list = get_list_of_switchable_assets_from_df(
        station_branches=station_branches, asset_bay_list=asset_connection_path
    )

    voltage_level_float = get_parameter_from_station(network=network, station_bus_index=station_id_list, parameter="vn_kv")
    # region = get_parameter_from_station(network, station_name, "zone")

    # get the station_name from the station_id
    # in pandapower a station is a bus -> only one entry in the DataFrame
    station_buses.sort_index(inplace=True)
    station_name = station_buses["name"].values[0]
    grid_model_id = station_buses["grid_model_id"].values[0]

    return Station(
        grid_model_id=grid_model_id,
        name=station_name,
        # region=region,
        voltage_level=voltage_level_float,
        busbars=busbar_list,
        couplers=coupler_list,
        assets=switchable_assets_list,
        asset_switching_table=switching_matrix,
    )

get_list_of_stations_ids #

get_list_of_stations_ids(
    network, station_list, foreign_key="equipment"
)

Get the list of stations from the network.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_list

List of station ids for which the stations should be retrieved. Station ids are a list -> a list of busbars associated with the station.

TYPE: List[List[int]]

foreign_key

Defines the column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
station_list

List of station objects.

TYPE: list[Station]

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_list_of_stations_ids(
    network: pp.pandapowerNet,
    station_list: List[List[int]],
    foreign_key: str = "equipment",
) -> List[Station]:
    """Get the list of stations from the network.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_list: List[List[int]]
        List of station ids for which the stations should be retrieved.
        Station ids are a list -> a list of busbars associated with the station.
    foreign_key: str
        Defines the column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    station_list: list[Station]
        List of station objects.
    """
    station_list = [
        get_station_from_id(network=network, station_id_list=station_id, foreign_key=foreign_key)
        for station_id in station_list
    ]

    return station_list

get_asset_topology_from_network #

get_asset_topology_from_network(
    network,
    topology_id,
    grid_model_file,
    station_id_list,
    foreign_key="equipment",
)

Get the asset topology from the network.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

topology_id

Id of the topology.

TYPE: str

grid_model_file

Name of the grid model file.

TYPE: str

station_id_list

List of station ids for which the stations should be retrieved. Station ids are a list -> a list of busbars associated with the station.

TYPE: List[List[int]]

foreign_key

Defines the column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
asset_topology

Topology class of the network.

TYPE: Topology

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_asset_topology_from_network(
    network: pp.pandapowerNet,
    topology_id: str,
    grid_model_file: str,
    station_id_list: List[List[int]],
    foreign_key: str = "equipment",
) -> Topology:
    """Get the asset topology from the network.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    topology_id: str
        Id of the topology.
    grid_model_file: str
        Name of the grid model file.
    station_id_list: List[List[int]]
        List of station ids for which the stations should be retrieved.
        Station ids are a list -> a list of busbars associated with the station.
    foreign_key: str
        Defines the column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    asset_topology: Topology
        Topology class of the network.
    """
    asset_topology = get_list_of_stations_ids(network=network, station_list=station_id_list, foreign_key=foreign_key)
    timestamp = datetime.datetime.now()
    return Topology(
        topology_id=topology_id,
        grid_model_file=grid_model_file,
        stations=asset_topology,
        timestamp=timestamp,
    )

get_station_bus_df #

get_station_bus_df(
    network,
    station_name=None,
    station_col="substat",
    station_bus_index=None,
)

Get the bus df by either station_name or station_bus_index.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

station_name

Station id for which the busses should be retrieved.

TYPE: Optional[Union[str, int, float]] DEFAULT: None

station_col

Column name in the bus DataFrame that contains the station_name. Note: Pandapower does not have a station column, so this is a custom column.

TYPE: str DEFAULT: 'substat'

station_bus_index

List of bus indices for which the busses should be retrieved.

TYPE: Optional[Union[list[int], int]] DEFAULT: None

RETURNS DESCRIPTION
bus_df

TYPE: DataFrame

RAISES DESCRIPTION
ValueError:

If station_name and station_bus_index are None.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_station_bus_df(
    network: pp.pandapowerNet,
    station_name: Optional[Union[str, int, float]] = None,
    station_col: str = "substat",
    station_bus_index: Optional[Union[list[int], int]] = None,
) -> pd.DataFrame:
    """Get the bus df by either station_name or station_bus_index.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    station_name: Optional[Union[str, int, float]]
        Station id for which the busses should be retrieved.
    station_col: str
        Column name in the bus DataFrame that contains the station_name.
        Note: Pandapower does not have a station column, so this is a custom column.
    station_bus_index: Optional[Union[list[int], int]]
        List of bus indices for which the busses should be retrieved.

    Returns
    -------
    bus_df: pd.DataFrame

    Raises
    ------
    ValueError:
        If station_name and station_bus_index are None.
    """
    bus_df = network.bus
    if station_name is not None and station_bus_index is None:
        bus_df = bus_df[bus_df[station_col] == station_name]
    elif station_bus_index is not None and station_name is None:
        if isinstance(station_bus_index, int):
            station_bus_index_list = [station_bus_index]
        else:
            station_bus_index_list = station_bus_index
        bus_df = bus_df.loc[station_bus_index_list]
    else:
        raise ValueError("Either station_name or station_bus_index needs to be set.")

    return bus_df

get_asset_connection_path_to_busbars #

get_asset_connection_path_to_busbars(
    network,
    asset_bus,
    station_buses,
    save_col_name="equipment",
)

Get the asset connection path to busbars.

PARAMETER DESCRIPTION
network

pandapower network object

TYPE: pandapowerNet

asset_bus

Asset bus id for which the connection path should be retrieved.

TYPE: int

station_buses

DataFrame with the busses of the station_name. Note: The DataFrame columns are the same as in the pydantic model.

TYPE: DataFrame

save_col_name

Column name that is used as the foreign_key/unique identifier.

TYPE: str DEFAULT: 'equipment'

RETURNS DESCRIPTION
asset_connection

AssetConnectionPath object.

TYPE: AssetConnectionPath

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_asset_connection_path_to_busbars(  # noqa: PLR0915
    network: pp.pandapowerNet,
    asset_bus: int,
    station_buses: pd.DataFrame,
    save_col_name: str = "equipment",
) -> AssetBay:
    """Get the asset connection path to busbars.

    Parameters
    ----------
    network: pp.pandapowerNet
        pandapower network object
    asset_bus: int
        Asset bus id for which the connection path should be retrieved.
    station_buses: pd.DataFrame
        DataFrame with the busses of the station_name.
        Note: The DataFrame columns are the same as in the pydantic model.
    save_col_name: str
        Column name that is used as the foreign_key/unique identifier.

    Returns
    -------
    asset_connection: AssetConnectionPath
        AssetConnectionPath object.

    """
    station_switches = get_all_switches_from_bus_ids(
        network=network, bus_ids=station_buses.index, only_closed_switches=False
    )
    station_switches = station_switches[(station_switches["et"] == "b")]
    # ---------- bus 1 - asset bus
    # entry point for search
    bus_1_element = station_buses[station_buses.index == asset_bus]
    bus_1 = asset_bus
    # check if bus exists and if a bus of type 'n' (Node)
    assert len(bus_1_element) == 1, f"Expected one bus with index {asset_bus}, got {len(bus_1_element)}"
    assert bus_1_element.type.iloc[0] == "n", f"Expected bus.type 'n', got {bus_1_element.type.iloc[0]}"

    # get all switches connected to the bus
    sl_disconnector = station_switches[(station_switches.bus == bus_1) | (station_switches.element == bus_1)]
    assert len(sl_disconnector) == 1, f"Expected one switch for SL connected to bus {bus_1}, got {len(sl_disconnector)}"
    assert sl_disconnector.et.iloc[0] == "b", f"Expected bus-bus switch, got {sl_disconnector.et.iloc[0]}"

    # check if switch is a disconnector or a circuit breaker
    # bus 1 / SL Switch is optional, so it can be a disconnector or a circuit breaker
    if sl_disconnector.type.iloc[0] == "CB":
        sl_disconnector = None
        bus_2 = bus_1
        condition_not_bus_1 = np.ones(len(station_switches), dtype=bool)
    else:
        assert sl_disconnector.type.iloc[0] == "DS", f"Expected switch type DS, got {sl_disconnector.type.iloc[0]}"
        bus_2 = sl_disconnector.element.iloc[0]
        if bus_2 == bus_1:
            bus_2 = sl_disconnector.bus.iloc[0]
        condition_not_bus_1 = (station_switches.bus != bus_1) & (station_switches.element != bus_1)

    # ---------- bus 2 - circuit breaker bus

    # get circuit breaker connected to the bus

    condition_bus_2 = (station_switches.bus == bus_2) | (station_switches.element == bus_2)
    circuit_breaker = station_switches[condition_not_bus_1 & condition_bus_2]
    assert len(circuit_breaker) == 1, (
        f"Expected one circuit breaker connected to bus {bus_2}, got {len(circuit_breaker)}, CB: {circuit_breaker.to_dict()}"
    )
    assert circuit_breaker.et.iloc[0] == "b", (
        f"Expected bus-bus switch, got {circuit_breaker.et.iloc[0]}, CB: {circuit_breaker.to_dict()}"
    )
    assert circuit_breaker.type.iloc[0] == "CB", (
        f"Expected switch type CB, got {circuit_breaker.type.iloc[0]}, CB: {circuit_breaker.to_dict()}"
    )
    bus_2_element = station_buses[station_buses.index == bus_2]
    assert len(bus_2_element) == 1, (
        f"Expected one bus with index {bus_2}, got {len(bus_2_element)}, CB: {circuit_breaker.to_dict()}"
    )
    assert bus_2_element.type.iloc[0] == "n", (
        f"Expected bus.type 'n', got {bus_2_element.type.iloc[0]}, CB: {circuit_breaker.to_dict()}"
    )

    # ---------- bus 3 - busbar section bus
    bus_3 = circuit_breaker.element.iloc[0]
    if bus_3 == bus_2:
        bus_3 = circuit_breaker.bus.iloc[0]

    # get sr disconnector to the final busbar
    condition_not_bus_2 = (station_switches.bus != bus_2) & (station_switches.element != bus_2)
    condition_bus_3 = (station_switches.bus == bus_3) | (station_switches.element == bus_3)
    sr_disconnectors = station_switches[condition_not_bus_2 & condition_bus_3]

    assert len(sr_disconnectors) != 0, (
        f"Expected one ore more switches for DS connected to the bus {bus_3}, got {len(sr_disconnectors)}"
    )
    assert all(sr_disconnectors.et == "b"), f"Expected bus-bus switch, got {sr_disconnectors.et.to_list()}"
    assert all(sr_disconnectors.type == "DS"), f"Expected switch type DS, got {sr_disconnectors.type.to_list()}"
    bus_3_element = station_buses[station_buses.index == bus_3]
    assert len(bus_3_element) == 1, f"Expected one bus with index {bus_3}, got {len(bus_3_element)}"
    assert bus_3_element.type.iloc[0] == "n", f"Expected bus.type 'n', got {bus_3_element.type.iloc[0]}"

    # get final busbars by iterating over all sr disconnectors
    final_buses = {}
    for _, sr_disconnector in sr_disconnectors.iterrows():
        final_bus = sr_disconnector.element
        if final_bus == bus_3:
            final_bus = sr_disconnector.bus
        final_bus_element = station_buses[station_buses.index == final_bus]
        assert len(final_bus_element) != 0, f"Expected one bus with index {final_bus}, got {len(final_bus_element)}"
        assert final_bus_element.type.iloc[0] == "b", f"Expected bus.type 'b', got {final_bus_element.type.iloc[0]}"
        final_buses[f"{final_bus}{SEPARATOR}bus"] = sr_disconnector[save_col_name]

    if sl_disconnector is not None:
        sl_switch_grid_model_id = sl_disconnector[save_col_name].iloc[0]
    else:
        sl_switch_grid_model_id = None

    asset_connection = AssetBay(
        sl_switch_grid_model_id=sl_switch_grid_model_id,
        dv_switch_grid_model_id=circuit_breaker[save_col_name].iloc[0],
        sr_switch_grid_model_id=final_buses,
    )

    return asset_connection

get_branch_from_bus_ids #

get_branch_from_bus_ids(
    branch_df, branch_type, bus_ids, bus_types
)

Get the branches based on branch_type and bus_ids.

PARAMETER DESCRIPTION
branch_df

DataFrame with one or multiple busses to get the bus id from. Note: The DataFrame columns are the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus.

TYPE: DataFrame

branch_type

Branch type that should be retrieved. It needs to be an attribute of the pandapower network.

TYPE: str

bus_ids

List of bus indices for which the busses should be retrieved.

TYPE: List[int]

bus_types

List of the tuple(name_of_column, pydantic_type, postfix_gridmodel_id). This list is used to identify the bus columns in the branch types.

TYPE: List[Tuple[str, Optional[str], str]]

RETURNS DESCRIPTION
branch_df_all_busses

DataFrame with the branches of the station_name. Note: The DataFrame columns NOT yet the same as in the pydantic model. Note: the index of the DataFrame is the internal id of the bus.

TYPE: DataFrame

RAISES DESCRIPTION
ValueError:

If branch_type is not found in the pp.pandapowerNet.

Source code in packages/importer_pkg/src/toop_engine_importer/pandapower_import/asset_topology.py
def get_branch_from_bus_ids(
    branch_df: pd.DataFrame,
    branch_type: str,
    bus_ids: List[int],
    bus_types: List[Tuple[str, Optional[str], str]],
) -> pd.DataFrame:
    """Get the branches based on branch_type and bus_ids.

    Parameters
    ----------
    branch_df: pd.DataFrame
        DataFrame with one or multiple busses to get the bus id from.
        Note: The DataFrame columns are the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.
    branch_type: str
        Branch type that should be retrieved.
        It needs to be an attribute of the pandapower network.
    bus_ids: List[int]
        List of bus indices for which the busses should be retrieved.
    bus_types: List[Tuple[str, Optional[str], str]]
        List of the tuple(name_of_column, pydantic_type, postfix_gridmodel_id).
        This list is used to identify the bus columns in the branch types.

    Returns
    -------
    branch_df_all_busses: pd.DataFrame
        DataFrame with the branches of the station_name.
        Note: The DataFrame columns NOT yet the same as in the pydantic model.
        Note: the index of the DataFrame is the internal id of the bus.

    Raises
    ------
    ValueError:
        If branch_type is not found in the pp.pandapowerNet.

    """
    branch_df["branch_end"] = None
    # rename bus columns to bus_int_id
    # get all elements from bus columns
    branch_df_col_list = []
    for bus_col_name, pydantic_type, postfix_gridmodel_id in bus_types:
        if bus_col_name in branch_df.columns:
            branch_df_col = branch_df[branch_df[bus_col_name].isin(bus_ids)].copy()
            branch_df_col.loc[branch_df_col[bus_col_name].isin(bus_ids), "branch_end"] = pydantic_type
            branch_df_col.rename(columns={bus_col_name: "bus_int_id"}, inplace=True)
            # get grid_model_id from index and branch_type
            branch_df_col["grid_model_id"] = branch_df_col.index.astype(str) + SEPARATOR + branch_type

            if branch_type != "trafo":
                branch_df_col["grid_model_id"] = branch_df_col["grid_model_id"] + postfix_gridmodel_id
                branch_df_col["type"] = branch_type + postfix_gridmodel_id
            else:
                branch_df_col["type"] = branch_type

            branch_df_col_list.append(branch_df_col)
    if len(branch_df_col_list) == 0:
        raise ValueError(
            f"bus column not found for branch_type: '{branch_type}', "
            + f"using bus_type: '{bus_types}' in columns: '{branch_df.columns}'"
        )
    branch_df_all_busses = pd.concat(branch_df_col_list)
    return branch_df_all_busses

Importer Pypowsybl#

toop_engine_importer.pypowsybl_import #

Import data from PyPowSyBl networks to the Topology Optimizer.

__all__ module-attribute #

__all__ = [
    "NetworkMasks",
    "PowsyblSecurityAnalysisParam",
    "PreProcessingStatistics",
    "apply_cb_lists",
    "apply_preprocessing_changes_to_network",
    "apply_white_list_to_operational_limits",
    "assign_element_id_to_cb_df",
    "convert_file",
    "convert_low_impedance_lines",
    "create_default_network_masks",
    "get_branches_df_with_element_name",
    "get_list_of_stations",
    "get_topology",
    "load_preprocessing_statistics_filesystem",
    "make_masks",
    "remove_branches_across_switch",
    "save_masks_to_files",
    "save_preprocessing_statistics_filesystem",
    "validate_network_masks",
]

PowsyblSecurityAnalysisParam #

Bases: BaseModel

Contains all the parameter for a Security Analysis with pypowsybl.

single_element_contingencies_ids instance-attribute #

single_element_contingencies_ids

The ids of the single element contingencies for the different element types.

The keys are the element types and the values are the ids of the elements. keys example: "dangling", "generator", "line", "switch", "tie", "transformer", "load", "custom"

current_limit_factor instance-attribute #

current_limit_factor

The factor to reduce the current limit on the lines.

This factor needs to be applied before the security analysis in for current limit and after in the violation dataframe.

monitored_branches instance-attribute #

monitored_branches

The branches that are monitored during the security analysis.

monitored_buses instance-attribute #

monitored_buses

The buses that are monitored during the security analysis.

ac_run class-attribute instance-attribute #

ac_run = True

Define load flow type.

True: run AC N-1 Analysis. False: run DC N-1 Analysis.

PreProcessingStatistics #

Bases: BaseModel

Contains all the statistics of the postprocessing.

id_lists class-attribute instance-attribute #

id_lists = Field(default_factory=dict)

Contains the ids of the N-1 analysis, border line currents and CB lists. keys: relevant_subs, line_for_nminus1, trafo_for_nminus1, tie_line_for_nminus1, dangling_line_for_nminus1, generator_for_nminus1, load_for_nminus1, switches_for_nminus1 white_list, black_list

import_result instance-attribute #

import_result

Statistics and results from an import process.

border_current class-attribute instance-attribute #

border_current = Field(default_factory=dict)

Contains the statistics of the current limit for the lines that leave the tso area.

network_changes class-attribute instance-attribute #

network_changes = Field(default_factory=dict)

Contains the statistics of the changes made to the network. keys: black_list, white_list, low_impedance_lines, branches_across_switch

import_parameter class-attribute instance-attribute #

import_parameter = None

Contains the statistics of the post processing.

NetworkMasks dataclass #

NetworkMasks(
    relevant_subs,
    line_for_nminus1,
    line_for_reward,
    line_overload_weight,
    line_disconnectable,
    line_blacklisted,
    line_tso_border,
    trafo_for_nminus1,
    trafo_for_reward,
    trafo_overload_weight,
    trafo_disconnectable,
    trafo_blacklisted,
    trafo_n0_n1_max_diff_factor,
    trafo_dso_border,
    trafo_pst_controllable,
    tie_line_for_reward,
    tie_line_for_nminus1,
    tie_line_overload_weight,
    tie_line_disconnectable,
    tie_line_tso_border,
    dangling_line_for_nminus1,
    generator_for_nminus1,
    load_for_nminus1,
    switch_for_nminus1,
    switch_for_reward,
    busbar_for_nminus1,
)

Class to hold the network masks.

See class PowsyblBackend(BackendInterface) in DCLoadflowsolver for more information.

relevant_subs instance-attribute #

relevant_subs

relevant_subs.npy (a boolean mask of relevant nodes).

line_for_nminus1 instance-attribute #

line_for_nminus1

line_for_nminus1.npy (a boolean mask of lines that are relevant for n-1).

line_for_reward instance-attribute #

line_for_reward

line_for_reward.npy (a boolean mask of lines that are relevant for the reward).

line_overload_weight instance-attribute #

line_overload_weight

line_overload_weight.npy (a float mask of weights for the overload).

line_disconnectable instance-attribute #

line_disconnectable

line_disconnectable.npy (a boolean mask of lines that can be disconnected)

line_blacklisted instance-attribute #

line_blacklisted

line_blacklisted.npy (a boolean mask of lines that are blacklisted) Currently only used during importing and not part of the PowsyblBackend

line_tso_border instance-attribute #

line_tso_border

line_tso_border.npy (a boolean mask of lines that are leading to TSOs outside the reward area) Currently only used during importing and not part of the PowsyblBackend

trafo_for_nminus1 instance-attribute #

trafo_for_nminus1

trafo_for_nminus1.npy (a boolean mask of transformers that are relevant for n-1).

trafo_for_reward instance-attribute #

trafo_for_reward

trafo_for_reward.npy (a boolean mask of transformers that are relevant for the reward).

trafo_overload_weight instance-attribute #

trafo_overload_weight

trafo_overload_weight.npy (a float mask of weights for the overload).

trafo_disconnectable instance-attribute #

trafo_disconnectable

trafo_disconnectable.npy (a boolean mask of transformers that can be disconnected)

trafo_blacklisted instance-attribute #

trafo_blacklisted

trafo_blacklisted.npy (a boolean mask of transformers that are blacklisted) Currently only used during importing and not part of the PowsyblBackend

trafo_n0_n1_max_diff_factor instance-attribute #

trafo_n0_n1_max_diff_factor

trafo_n0_n1_max_diff_factor.npy (if a trafo shall be limited in its N-0 to N-1 difference and by how much)

trafo_dso_border instance-attribute #

trafo_dso_border

trafo_dso_border.npy (a boolean mask of transformers that border the DSO control area) Currently only used during importing and not part of the PowsyblBackend

trafo_pst_controllable instance-attribute #

trafo_pst_controllable

Trafos which are a PST and can be controlled

tie_line_for_reward instance-attribute #

tie_line_for_reward

tie_line_for_reward.npy (a boolean mask of tie lines that are relevant for the reward).

tie_line_for_nminus1 instance-attribute #

tie_line_for_nminus1

tie_line_for_nminus1.npy (a boolean mask of tie lines that are relevant for n-1).

tie_line_overload_weight instance-attribute #

tie_line_overload_weight

tie_line_overload_weight.npy (a float mask of weights for the overload).

tie_line_disconnectable instance-attribute #

tie_line_disconnectable

tie_line_disconnectable.npy (a boolean mask of tie lines that can be disconnected)

tie_line_tso_border instance-attribute #

tie_line_tso_border

tie_line_tso_border.npy (a boolean mask of tielines that are leading to TSOs outside the reward area) Currently only used during importing and not part of the PowsyblBackend

dangling_line_for_nminus1 instance-attribute #

dangling_line_for_nminus1

tie_line_disconnectable.npy (a boolean mask of tie lines that are relevant for n-1).

generator_for_nminus1 instance-attribute #

generator_for_nminus1

generator_for_nminus1.npy (a boolean mask of generators that are relevant for n-1).

load_for_nminus1 instance-attribute #

load_for_nminus1

generator_for_nminus1.npy (a boolean mask of loads that are relevant for n-1).

switch_for_nminus1 instance-attribute #

switch_for_nminus1

switches_nminus1.npy (a boolean mask of switches that are relevant for n-1).

switch_for_reward instance-attribute #

switch_for_reward

switches_reward.npy (a boolean mask of switches that are relevant for the reward).

busbar_for_nminus1 instance-attribute #

busbar_for_nminus1

busbar_for_nminus1.npy (a boolean mask of busbars/busbar_sections that are relevant for n-1).

get_list_of_stations #

get_list_of_stations(
    network,
    buses_with_substation_and_voltage,
    switches,
    dangling_lines,
    element_names,
)

Get the list of stations from the relevant buses.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

buses_with_substation_and_voltage

DataFrame with the relevant buses, substation id and voltage level

TYPE: DataFrame

switches

DataFrame with all the switches in the network. Includes the column "name"

TYPE: DataFrame

dangling_lines

DataFrame with all the dangling lines in the network. Includes the column "tie_line_id"

TYPE: DataFrame

element_names

Series with the names of all injections and branches in the network and their ids as index

TYPE: Series

RETURNS DESCRIPTION
station_list

List of all formatted stations of the relevant buses in the network

TYPE: list[Station]

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_list_of_stations(
    network: Network,
    buses_with_substation_and_voltage: pd.DataFrame,
    switches: pd.DataFrame,
    dangling_lines: pd.DataFrame,
    element_names: pd.Series,
) -> list[Station]:
    """Get the list of stations from the relevant buses.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    buses_with_substation_and_voltage: pd.DataFrame
        DataFrame with the relevant buses, substation id and voltage level
    switches: pd.DataFrame
        DataFrame with all the switches in the network. Includes the column "name"
    dangling_lines: pd.DataFrame
        DataFrame with all the dangling lines in the network. Includes the column "tie_line_id"
    element_names: pd.Series
        Series with the names of all injections and branches in the network and their ids as index

    Returns
    -------
    station_list: list[Station]
        List of all formatted stations of the relevant buses in the network
    """
    station_list = []
    for bus_id, bus_info in buses_with_substation_and_voltage.iterrows():
        station_topology = network.get_bus_breaker_topology(bus_info.voltage_level_id)
        station_buses = get_bus_info_from_topology(station_topology.buses, bus_id)
        coupler_elements = get_coupler_info_from_topology(station_topology.switches, switches, station_buses)
        station_elements, switching_matrix = get_asset_info_from_topology(
            station_topology.elements, station_buses, dangling_lines, element_names
        )
        asset_connectivity = np.ones_like(switching_matrix, dtype=bool)
        station = Station(
            grid_model_id=bus_id,
            name=bus_info.substation_id,
            region=bus_info.voltage_level_id[0:2],
            voltage_level=bus_info.nominal_v,
            busbars=get_list_of_busbars_from_df(station_buses),
            couplers=get_list_of_coupler_from_df(coupler_elements),
            assets=get_list_of_switchable_assets_from_df(station_elements),
            asset_switching_table=switching_matrix,
            asset_connectivity=asset_connectivity,
        )
        station_list.append(station)
    return station_list

get_topology #

get_topology(
    network,
    relevant_stations,
    topology_id,
    grid_model_file=None,
)

Get the pydantic topology model from the network.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

relevant_stations

The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in network.get_buses or as a list of bus ids in network.get_buses()

TYPE: Union[list[str], Bool[ndarray, ' n_buses']]

topology_id

Id of the topology to set in the asset topology

TYPE: str

grid_model_file

Path to the grid model file to set in the asset topology

TYPE: Optional[str] DEFAULT: None

RETURNS DESCRIPTION
topology

Topology object, including all relevant stations

TYPE: Topology

Source code in packages/grid_helpers_pkg/src/toop_engine_grid_helpers/powsybl/powsybl_asset_topo.py
def get_topology(
    network: Network,
    relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]],
    topology_id: str,
    grid_model_file: Optional[str] = None,
) -> Topology:
    """Get the pydantic topology model from the network.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    relevant_stations: Union[list[str], Bool[np.ndarray, " n_buses"]]
        The relevant stations to be included in the resulting topology. Either as a boolean mask over all buses in
        network.get_buses or as a list of bus ids in network.get_buses()
    topology_id: str
        Id of the topology to set in the asset topology
    grid_model_file: Optional[str]
        Path to the grid model file to set in the asset topology

    Returns
    -------
    topology: Topology
        Topology object, including all relevant stations
    """
    station_list = get_relevant_stations(network=network, relevant_stations=relevant_stations)
    timestamp = datetime.datetime.now()

    return Topology(
        topology_id=topology_id,
        grid_model_file=grid_model_file,
        stations=station_list,
        timestamp=timestamp,
    )

apply_white_list_to_operational_limits #

apply_white_list_to_operational_limits(
    network, white_list_df
)

Apply the white list to the operational limits of the network.

PARAMETER DESCRIPTION
network

The network to modify. Note: The network is modified in place.

TYPE: Network

white_list_df

DataFrame with the columns "element_id", "Anfangsknoten", "Endknoten", "Auslastungsgrenze_n_0", "Auslastungsgrenze_n_1"

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/dacf_whitelists.py
def apply_white_list_to_operational_limits(network: Network, white_list_df: pd.DataFrame) -> None:
    """Apply the white list to the operational limits of the network.

    Parameters
    ----------
    network : Network
        The network to modify. Note: The network is modified in place.
    white_list_df : pd.DataFrame
        DataFrame with the columns "element_id", "Anfangsknoten", "Endknoten",
        "Auslastungsgrenze_n_0", "Auslastungsgrenze_n_1"

    """
    white_list_df["Auslastungsgrenze_n_0"] = white_list_df["Auslastungsgrenze_n_0"] / 100
    white_list_df["Auslastungsgrenze_n_1"] = white_list_df["Auslastungsgrenze_n_1"] / 100
    # get the current operational limits
    op_lim = network.get_operational_limits().reset_index()
    # filter the operational limits to the elements in the white list
    op_lim = op_lim[op_lim["element_id"].isin(white_list_df["element_id"].to_list())]
    # merge the white list with the operational limits -> add the "Auslastungsgrenze_n_0" and "Auslastungsgrenze_n_1" columns
    op_lim = op_lim.merge(white_list_df, how="left", left_on="element_id", right_on="element_id")
    op_lim.set_index("element_id", inplace=True)
    # remove tie lines, as they can't be set
    op_lim = op_lim[op_lim["element_type"] != "TIE_LINE"]
    # copy the operational limits for N-1 limits
    n1_limits = op_lim[op_lim["Auslastungsgrenze_n_0"] != op_lim["Auslastungsgrenze_n_1"]].copy()
    n1_limits["acceptable_duration"] = 3600
    n1_limits["name"] = "N-1"
    # apply the limits to the operational limits
    n1_limits["value"] = n1_limits["value"] * n1_limits["Auslastungsgrenze_n_1"]
    op_lim["value"] = op_lim["value"] * op_lim["Auslastungsgrenze_n_0"]
    # merge the operational limits with the N-1 limits
    op_lim = pd.concat([op_lim, n1_limits]).sort_index()
    # drop white list columns, to be able to create new operational limits
    op_lim = op_lim.set_index(["side", "type", "group_name"], append=True)[["acceptable_duration", "name", "value"]]
    # drop element_type column -> deprecated
    # create the new operational limits
    network.create_operational_limits(op_lim)

assign_element_id_to_cb_df #

assign_element_id_to_cb_df(
    branches_with_elementname, cb_df
)

Get the element_id for the elements in the cb_df based on the power network model.

PARAMETER DESCRIPTION
branches_with_elementname

powsybl branches DataFrame with the columns "elementName", "bus_breaker_bus1_id", "bus_breaker_bus2_id", "voltage_level1_id", "voltage_level2_id", "pairing_key"

TYPE: DataFrame

cb_df

DataFrame with the columns "Elementname", "Anfangsknoten", "Endknoten" Note: The element_id column is added to the DataFrame in place

TYPE: DataFrame

RETURNS DESCRIPTION
None
Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/dacf_whitelists.py
def assign_element_id_to_cb_df(branches_with_elementname: pd.DataFrame, cb_df: pd.DataFrame) -> None:
    """Get the element_id for the elements in the cb_df based on the power network model.

    Parameters
    ----------
    branches_with_elementname : pd.DataFrame
        powsybl branches DataFrame with the columns "elementName", "bus_breaker_bus1_id",
        "bus_breaker_bus2_id", "voltage_level1_id", "voltage_level2_id", "pairing_key"
    cb_df : pd.DataFrame
        DataFrame with the columns "Elementname", "Anfangsknoten", "Endknoten"
        Note: The element_id column is added to the DataFrame in place

    Returns
    -------
    None

    """
    eight_letter_nodes = 8
    seven_letter_nodes = 7
    cb_df["element_id"] = None
    for index, row in cb_df.iterrows():
        # determine the column names for the bus ids, based on the length of the bus id given in the cb_df
        if len(row["Anfangsknoten"]) == eight_letter_nodes:
            column_start_node = "bus_breaker_bus"
        elif len(row["Anfangsknoten"]) == seven_letter_nodes:
            column_start_node = "voltage_level"
        else:
            # should this trigger an error? -> would be an error in the black/white list
            pass

        if len(row["Endknoten"]) == eight_letter_nodes:
            column_end_node = "bus_breaker_bus"
        elif len(row["Endknoten"]) == seven_letter_nodes:
            column_end_node = "voltage_level"
        else:
            # should this trigger an error? -> would be an error in the black/white list
            pass

        # search for the element in the power network model
        # search for the element name in the branches_with_elementname
        # search for "Anfangsknoten"/start node and "Endknoten"/end node in the bus ids left and right + pairing key
        condition_name = branches_with_elementname["elementName"].str.contains(row["Elementname"])
        condition_start_node_left = (branches_with_elementname[f"{column_start_node}1_id"] == row["Anfangsknoten"]) | (
            branches_with_elementname["pairing_key"] == row["Anfangsknoten"]
        )
        condition_start_node_right = (branches_with_elementname[f"{column_start_node}2_id"] == row["Anfangsknoten"]) | (
            branches_with_elementname["pairing_key"] == row["Anfangsknoten"]
        )
        condition_end_node_left = (branches_with_elementname[f"{column_end_node}1_id"] == row["Endknoten"]) | (
            branches_with_elementname["pairing_key"] == row["Endknoten"]
        )
        condition_end_node_right = (branches_with_elementname[f"{column_end_node}2_id"] == row["Endknoten"]) | (
            branches_with_elementname["pairing_key"] == row["Endknoten"]
        )
        condition_key_order1 = condition_start_node_left & condition_end_node_right
        condition_key_order2 = condition_start_node_right & condition_end_node_left

        # apply the conditions to the branches_with_elementname DataFrame
        found_list = branches_with_elementname[
            condition_name & (condition_key_order1 | condition_key_order2)
        ].index.to_list()
        # check if only one element was found -> add to the cb_df
        if len(found_list) == 1:
            cb_df.at[index, "element_id"] = found_list[0]
        # if more than one/no element was found -> check if the id is in the "bus_breaker_bus" column
        # this often happens for TWO_WINDINGS_TRANSFORMER elements, where powsybl creates it's own id for the voltage level
        # this could be done immediately, but it would be a lot slower
        else:
            column_start_node = "bus_breaker_bus"
            column_end_node = "bus_breaker_bus"
            condition_name = branches_with_elementname["elementName"].str.contains(row["Elementname"])
            condition_start_node_left = (
                branches_with_elementname[f"{column_start_node}1_id"].str.contains(row["Anfangsknoten"])
            ) | (branches_with_elementname["pairing_key"] == row["Anfangsknoten"])
            condition_start_node_right = (
                branches_with_elementname[f"{column_start_node}2_id"].str.contains(row["Anfangsknoten"])
            ) | (branches_with_elementname["pairing_key"] == row["Anfangsknoten"])
            condition_end_node_left = (
                branches_with_elementname[f"{column_end_node}1_id"].str.contains(row["Endknoten"])
            ) | (branches_with_elementname["pairing_key"] == row["Endknoten"])
            condition_end_node_right = (
                branches_with_elementname[f"{column_end_node}2_id"].str.contains(row["Endknoten"])
            ) | (branches_with_elementname["pairing_key"] == row["Endknoten"])
            condition_key_order1 = condition_start_node_left & condition_end_node_right
            condition_key_order2 = condition_start_node_right & condition_end_node_left
            found_list = branches_with_elementname[
                condition_name & (condition_key_order1 | condition_key_order2)
            ].index.to_list()
            if len(found_list) == 1:
                cb_df.at[index, "element_id"] = found_list[0]

apply_cb_lists #

apply_cb_lists(
    network,
    statistics,
    white_list_file,
    black_list_file,
    fs,
)

Run the black or white list to the powsybl network.

PARAMETER DESCRIPTION
network

The network to modify. Note: The network is modified in place.

TYPE: Network

statistics

The statistics to fill with the id lists of the black and white list Note: The statistics are modified in place.

TYPE: ProcessingStatistics

white_list_file

The path to the white list file, if None, no white list is applied.

TYPE: str | None

black_list_file

The path to the black list file, if None, no black list is applied.

TYPE: str | None

fs

The filesystem to use to read the files.

TYPE: AbstractFileSystem

RETURNS DESCRIPTION
statistics

The statistics with the id lists of the black and white list

TYPE: ProcessingStatistics

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/network_analysis.py
def apply_cb_lists(
    network: Network,
    statistics: PreProcessingStatistics,
    white_list_file: str | None,
    black_list_file: str | None,
    fs: AbstractFileSystem,
) -> PreProcessingStatistics:
    """Run the black or white list to the powsybl network.

    Parameters
    ----------
    network : Network
        The network to modify. Note: The network is modified in place.
    statistics : ProcessingStatistics
        The statistics to fill with the id lists of the black and white list
        Note: The statistics are modified in place.
    white_list_file : str | None
        The path to the white list file, if None, no white list is applied.
    black_list_file : str | None
        The path to the black list file, if None, no black list is applied.
    fs : AbstractFileSystem
        The filesystem to use to read the files.

    Returns
    -------
    statistics: ProcessingStatistics
        The statistics with the id lists of the black and white list

    """
    branches_with_elementname = get_branches_df_with_element_name(network)
    branches_with_elementname["pairing_key"] = branches_with_elementname["pairing_key"].str[0:7]
    # get only the rows needed
    branches_with_elementname = branches_with_elementname[
        powsybl_masks.get_mask_for_area_codes(branches_with_elementname, ["D"], "voltage_level1_id", "voltage_level2_id")
    ]
    op_lim = network.get_operational_limits(attributes=[]).index.get_level_values("element_id").to_list()
    branches_with_elementname = branches_with_elementname[branches_with_elementname.index.isin(op_lim)]

    if white_list_file is not None:
        with fs.open(str(white_list_file), "r") as f:
            white_list_df = pd.read_csv(f, delimiter=";").fillna("")
        dacf_whitelists.assign_element_id_to_cb_df(cb_df=white_list_df, branches_with_elementname=branches_with_elementname)
        statistics.import_result.n_white_list = len(white_list_df)
        white_list_df = white_list_df[white_list_df["element_id"].notnull()]
        apply_white_list_to_operational_limits(network, white_list_df)
        statistics.id_lists["white_list"] = white_list_df["element_id"].to_list()
        statistics.import_result.n_white_list_applied = len(white_list_df["element_id"])
    else:
        statistics.id_lists["white_list"] = []
    if black_list_file is not None:
        with fs.open(str(black_list_file), "r") as f:
            black_list_df = pd.read_csv(f, delimiter=";").fillna("")
        dacf_whitelists.assign_element_id_to_cb_df(cb_df=black_list_df, branches_with_elementname=branches_with_elementname)
        statistics.import_result.n_black_list = len(black_list_df)
        black_list_df = black_list_df[black_list_df["element_id"].notnull()]
        statistics.id_lists["black_list"] = black_list_df["element_id"].to_list()
        statistics.import_result.n_black_list_applied = len(black_list_df["element_id"])
    else:
        statistics.id_lists["black_list"] = []
    return statistics

convert_low_impedance_lines #

convert_low_impedance_lines(
    net, voltage_level_prefix, x_threshold_line=0.05
)

Convert all lines in the same voltage level with very low impedance to breakers.

PARAMETER DESCRIPTION
net

The network to modify. Note: This function modifies the network in place.

TYPE: Network

voltage_level_prefix

The prefix of the voltage level to consider.

TYPE: str

x_threshold_line

The threshold for x, everything below will be converted.

TYPE: float DEFAULT: 0.05

RETURNS DESCRIPTION
low_impedance_lines

The lines that were converted to breakers.

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/network_analysis.py
def convert_low_impedance_lines(net: Network, voltage_level_prefix: str, x_threshold_line: float = 0.05) -> pd.DataFrame:
    """Convert all lines in the same voltage level with very low impedance to breakers.

    Parameters
    ----------
    net: Network
        The network to modify. Note: This function modifies the network in place.
    voltage_level_prefix: str
        The prefix of the voltage level to consider.
    x_threshold_line: float
        The threshold for x, everything below will be converted.

    Returns
    -------
    low_impedance_lines: pd.DataFrame
        The lines that were converted to breakers.

    """
    lines = net.get_lines(all_attributes=True)
    low_impedance_lines = lines[
        (lines["voltage_level1_id"] == lines["voltage_level2_id"])
        & (lines["x"] <= x_threshold_line)
        & (lines["voltage_level1_id"].str.startswith(voltage_level_prefix))
        & (lines["connected1"] & lines["connected2"])
    ]
    net.remove_elements(low_impedance_lines.index)
    low_impedance_lines = low_impedance_lines[
        [
            "bus_breaker_bus1_id",
            "bus_breaker_bus2_id",
            "elementName",
            "voltage_level1_id",
        ]
    ].rename(
        columns={
            "bus_breaker_bus1_id": "bus1_id",
            "bus_breaker_bus2_id": "bus2_id",
            "elementName": "name",
            "voltage_level1_id": "voltage_level_id",
        }
    )
    low_impedance_lines["kind"] = "BREAKER"
    low_impedance_lines["open"] = False
    low_impedance_lines["retained"] = True
    net.create_switches(low_impedance_lines)
    return low_impedance_lines

get_branches_df_with_element_name #

get_branches_df_with_element_name(network)

Get the branches with the element name.

PARAMETER DESCRIPTION
network

The network object

TYPE: Network

RETURNS DESCRIPTION
DataFrame

The branches with the element name

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/network_analysis.py
def get_branches_df_with_element_name(network: Network) -> pd.DataFrame:
    """Get the branches with the element name.

    Parameters
    ----------
    network : Network
        The network object

    Returns
    -------
    pd.DataFrame
        The branches with the element name

    """
    branches = network.get_branches(all_attributes=True)
    lines = network.get_lines(all_attributes=True)["elementName"]
    trafos = network.get_2_windings_transformers(all_attributes=True)["elementName"]
    tie_lines = network.get_tie_lines(all_attributes=True)[["elementName_1", "elementName_2", "pairing_key"]]
    tie_lines["elementName"] = tie_lines["elementName_1"] + " + " + tie_lines["elementName_2"]
    tie_lines = tie_lines[["elementName", "pairing_key"]]

    branches = branches.merge(lines, how="left", on="id", suffixes=("", "_1"))
    branches = branches.merge(trafos, how="left", on="id", suffixes=("", "_2"))
    branches = branches.merge(tie_lines, how="left", on="id", suffixes=("", "_3"))
    branches["elementName"] = branches["elementName"].combine_first(branches["elementName_2"])
    branches["elementName"] = branches["elementName"].combine_first(branches["elementName_3"])
    branches = branches.drop(columns=["elementName_2", "elementName_3"])
    return branches

remove_branches_across_switch #

remove_branches_across_switch(net)

Remove all branches that span across a closed switch, i.e. have the same from+to bus.

PARAMETER DESCRIPTION
net

The network to modify. Note: This function modifies the network in place.

TYPE: Network

RETURNS DESCRIPTION
to_remove

The branches that were removed.

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/network_analysis.py
def remove_branches_across_switch(net: Network) -> pd.DataFrame:
    """Remove all branches that span across a closed switch, i.e. have the same from+to bus.

    Parameters
    ----------
    net: Network
        The network to modify. Note: This function modifies the network in place.

    Returns
    -------
    to_remove: pd.DataFrame
        The branches that were removed.

    """
    # remove branches that span across a closed switch
    # the bus1_id == bus2_id, in case the branch is a closed switch
    # in case no switch is between the buses, bus1_id != bus2_id
    # -> a line between two buses is not removed, if there is no switch between them
    to_remove = net.get_branches()[
        (net.get_branches()["bus1_id"] == net.get_branches()["bus2_id"])
        & (net.get_branches()["connected1"] & net.get_branches()["connected2"])
    ]
    net.remove_elements(to_remove.index)
    return to_remove

create_default_network_masks #

create_default_network_masks(network)

Create a default NetworkMasks object with all masks set to False.

PARAMETER DESCRIPTION
network

The powsybl network to create the masks for.

TYPE: Network

RETURNS DESCRIPTION
network_masks

The default NetworkMasks object.

TYPE: NetworkMasks

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
def create_default_network_masks(network: Network) -> NetworkMasks:
    """Create a default NetworkMasks object with all masks set to False.

    Parameters
    ----------
    network: Network
        The powsybl network to create the masks for.

    Returns
    -------
    network_masks: NetworkMasks
        The default NetworkMasks object.

    """
    # Only loading the index is much faster, if we only care for the size
    bus_df = network.get_buses(attributes=[])
    lines_df = network.get_lines(attributes=[])
    trafo_df = network.get_2_windings_transformers(attributes=[])
    tie_df = network.get_tie_lines(attributes=[])
    dangling_df = network.get_dangling_lines(attributes=[])
    generator_df = network.get_generators(attributes=[])
    load_df = network.get_loads(attributes=[])
    switches_df = network.get_switches(attributes=[])
    busbar_df = network.get_busbar_sections(attributes=[])

    return NetworkMasks(
        relevant_subs=np.zeros(len(bus_df), dtype=bool),
        line_for_nminus1=np.zeros(len(lines_df), dtype=bool),
        line_for_reward=np.zeros(len(lines_df), dtype=bool),
        line_overload_weight=np.ones(len(lines_df), dtype=float),
        line_disconnectable=np.zeros(len(lines_df), dtype=bool),
        line_blacklisted=np.zeros(len(lines_df), dtype=bool),
        line_tso_border=np.zeros(len(lines_df), dtype=bool),
        trafo_for_nminus1=np.zeros(len(trafo_df), dtype=bool),
        trafo_for_reward=np.zeros(len(trafo_df), dtype=bool),
        trafo_overload_weight=np.ones(len(trafo_df), dtype=float),
        trafo_disconnectable=np.zeros(len(trafo_df), dtype=bool),
        trafo_blacklisted=np.zeros(len(trafo_df), dtype=bool),
        trafo_n0_n1_max_diff_factor=np.ones(len(trafo_df), dtype=float) * -1,
        trafo_dso_border=np.zeros(len(trafo_df), dtype=bool),
        trafo_pst_controllable=np.zeros(len(trafo_df), dtype=bool),
        tie_line_for_reward=np.zeros(len(tie_df), dtype=bool),
        tie_line_for_nminus1=np.zeros(len(tie_df), dtype=bool),
        tie_line_overload_weight=np.ones(len(tie_df), dtype=float),
        tie_line_disconnectable=np.zeros(len(tie_df), dtype=bool),
        tie_line_tso_border=np.zeros(len(tie_df), dtype=bool),
        dangling_line_for_nminus1=np.zeros(len(dangling_df), dtype=bool),
        generator_for_nminus1=np.zeros(len(generator_df), dtype=bool),
        load_for_nminus1=np.zeros(len(load_df), dtype=bool),
        switch_for_nminus1=np.zeros(len(switches_df), dtype=bool),
        switch_for_reward=np.zeros(len(switches_df), dtype=bool),
        busbar_for_nminus1=np.zeros(len(busbar_df), dtype=bool),
    )

make_masks #

make_masks(
    network,
    importer_parameters,
    filesystem=None,
    blacklisted_ids=None,
)

Create all masks for the network, depending on the import parameters.

PARAMETER DESCRIPTION
network

The network to get the masks for.

TYPE: Network

importer_parameters

The import parameters including control_area, nminus1_area, cutoff_voltage Optional: border_line_factors, border_line_weight, dso_trafo_factors, dso_trafo_weight

TYPE: Union[UcteImporterParameters, CgmesImporterParameters]

filesystem

The filesystem to use for loading the contingency lists from. If not provided, the local filesystem is used.

TYPE: AbstractFileSystem DEFAULT: None

blacklisted_ids

The ids of the branche that are blacklisted.

TYPE: list[str] | None DEFAULT: None

RETURNS DESCRIPTION
network_masks

The masks for the network.

TYPE: NetworkMasks

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
def make_masks(
    network: Network,
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters],
    filesystem: AbstractFileSystem = None,
    blacklisted_ids: list[str] | None = None,
) -> NetworkMasks:
    """Create all masks for the network, depending on the import parameters.

    Parameters
    ----------
    network: Network
        The network to get the masks for.
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters]
        The import parameters including control_area, nminus1_area, cutoff_voltage
        Optional: border_line_factors, border_line_weight, dso_trafo_factors, dso_trafo_weight
    filesystem: AbstractFileSystem
        The filesystem to use for loading the contingency lists from. If not provided, the local filesystem is used.
    blacklisted_ids: list[str] | None
        The ids of the branche that are blacklisted.

    Returns
    -------
    network_masks: NetworkMasks
        The masks for the network.
    """
    if filesystem is None:
        filesystem = LocalFileSystem()
    if blacklisted_ids is None:
        blacklisted_ids = []
    default_masks = create_default_network_masks(network)

    network_masks = update_line_masks(
        default_masks,
        network,
        importer_parameters,
        blacklisted_ids,
    )
    network_masks = update_trafo_masks(
        network_masks,
        network,
        importer_parameters,
        blacklisted_ids,
    )
    network_masks = update_tie_and_dangling_line_masks(network_masks, network, importer_parameters)
    network_masks = update_load_and_generation_masks(network_masks, network, importer_parameters)
    network_masks = update_switch_masks(network_masks, network, importer_parameters)
    network_masks = update_bus_masks(network_masks, network, importer_parameters, filesystem=filesystem)
    network_masks = update_reward_masks_to_include_border_branches(network_masks, importer_parameters)
    network_masks = remove_slack_from_relevant_subs(network, network_masks, distributed_slack=True)

    if importer_parameters.contingency_list_file is not None:
        if importer_parameters.schema_format == "ContingencyImportSchemaPowerFactory":
            network_masks = update_masks_from_power_factory_contingency_list_file(
                network_masks, network, importer_parameters, filesystem=filesystem
            )
        elif importer_parameters.schema_format == "ContingencyImportSchema":
            network_masks = update_masks_from_contingency_list_file(
                network_masks, network, importer_parameters, filesystem=filesystem
            )
        else:
            logger.warning(f"Contingency list processing for {importer_parameters.ingress_id} is not implemented yet.")
    if not validate_network_masks(network_masks, default_masks):
        raise RuntimeError("Network masks are not created correctly.")

    return network_masks

save_masks_to_files #

save_masks_to_files(network_masks, data_folder)

Save the network masks to files.

PARAMETER DESCRIPTION
network_masks

The network masks to save.

TYPE: NetworkMasks

data_folder

The folder to save the masks to.

TYPE: Path

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
def save_masks_to_files(network_masks: NetworkMasks, data_folder: Path) -> None:
    """Save the network masks to files.

    Parameters
    ----------
    network_masks: NetworkMasks
        The network masks to save.
    data_folder: Path
        The folder to save the masks to.
    """
    save_masks_to_filesystem(network_masks, data_folder, filesystem=LocalFileSystem())

validate_network_masks #

validate_network_masks(network_masks, default_mask)

Validate if the network masks are created correctly.

PARAMETER DESCRIPTION
network_masks

The network masks to validate.

TYPE: NetworkMasks

default_mask

The default network masks to validate against.

TYPE: NetworkMasks

RETURNS DESCRIPTION
bool

True if the network masks are created correctly, False otherwise.

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/powsybl_masks.py
def validate_network_masks(network_masks: NetworkMasks, default_mask: NetworkMasks) -> bool:
    """Validate if the network masks are created correctly.

    Parameters
    ----------
    network_masks: NetworkMasks
        The network masks to validate.
    default_mask: NetworkMasks
        The default network masks to validate against.

    Returns
    -------
    bool
        True if the network masks are created correctly, False otherwise.

    """
    if not isinstance(network_masks, NetworkMasks):
        logger.warning("network_masks are not of type NetworkMasks.")
        return False
    for mask_key, mask in asdict(network_masks).items():
        if not isinstance(mask, np.ndarray):
            logger.warning(f"Mask {mask_key} is not a numpy array.")
            return False
        if not mask.shape == asdict(default_mask)[mask_key].shape:
            logger.warning(
                f"Shape of mask {mask_key} is not correct. got: "
                + f"{mask.shape}, expected: {asdict(default_mask)[mask_key].shape}"
            )
            return False
        if mask.dtype != asdict(default_mask)[mask_key].dtype:
            logger.warning(
                f"Dtype of mask {mask_key} is not correct. got: "
                + f"{mask.dtype}, expected: {asdict(default_mask)[mask_key].dtype}"
            )
            return False
    return True

apply_preprocessing_changes_to_network #

apply_preprocessing_changes_to_network(
    network, statistics, status_update_fn=None
)

Apply the default changes to the network.

These changes include: - removing low impedance lines - removing branches across switches

PARAMETER DESCRIPTION
network

The network to apply the changes to. Note: This function modifies the network in place.

TYPE: Network

statistics

The statistics of the preprocessing. Note: This function modifies the statistics in place.

TYPE: PreProcessingStatistics

status_update_fn

A function to call to signal progress in the preprocessing pipeline. Takes a stage and an optional message as parameters

TYPE: Optional[Callable[[PreprocessStage, Optional[str]], None]] DEFAULT: None

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def apply_preprocessing_changes_to_network(
    network: Network,
    statistics: PreProcessingStatistics,
    status_update_fn: Optional[Callable[[PreprocessStage, Optional[str]], None]] = None,
) -> None:
    """Apply the default changes to the network.

    These changes include:
    - removing low impedance lines
    - removing branches across switches

    Parameters
    ----------
    network: Network
        The network to apply the changes to.
        Note: This function modifies the network in place.
    statistics: PreprocessingStatistics
        The statistics of the preprocessing.
        Note: This function modifies the statistics in place.
    status_update_fn: Optional[Callable[[PreprocessStage, Optional[str]], None]]
        A function to call to signal progress in the preprocessing pipeline. Takes a stage and an
        optional message as parameters

    """
    if status_update_fn is None:
        status_update_fn = empty_status_update_fn
    status_update_fn("modify_low_impedance_lines", "Converting low impedance lines to breakers")
    low_impedance_lines = network_analysis.convert_low_impedance_lines(network, "D8")
    statistics.import_result.n_low_impedance_lines = len(low_impedance_lines)
    statistics.network_changes["low_impedance_lines"] = low_impedance_lines.index.to_list()

    status_update_fn("modify_branches_over_switches", "Removing branches across switches")
    branches_across_switch = network_analysis.remove_branches_across_switch(network)
    statistics.import_result.n_branch_across_switch = len(branches_across_switch)
    statistics.network_changes["branches_across_switch"] = branches_across_switch.index.to_list()

convert_file #

convert_file(
    importer_parameters,
    status_update_fn=empty_status_update_fn,
    processed_gridfile_fs=None,
    unprocessed_gridfile_fs=None,
)

Convert the UCTE file to a format that can be used by the RL agent.

Saves data and network to the output folder.

PARAMETER DESCRIPTION
importer_parameters

Parameters that are required to import the data from a UCTE file. This will utilize powsybl and the powsybl backend to the loadflow solver

TYPE: Union[UcteImporterParameters, CgmesImporterParameters]

status_update_fn

A function to call to signal progress in the preprocessing pipeline. Takes a stage and an optional message as parameters

TYPE: Callable[[PreprocessStage, Optional[str]], None] DEFAULT: empty_status_update_fn

processed_gridfile_fs

A filesystem where the processed gridfiles are stored. If None, the local filesystem is used

TYPE: Optional[AbstractFileSystem] DEFAULT: None

unprocessed_gridfile_fs

A filesystem where the unprocessed gridfiles are stored. If None, the local filesystem is used.

TYPE: Optional[AbstractFileSystem] DEFAULT: None

RETURNS DESCRIPTION
UcteImportResult

The result of the import process.

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def convert_file(
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters],
    status_update_fn: Callable[[PreprocessStage, Optional[str]], None] = empty_status_update_fn,
    processed_gridfile_fs: Optional[AbstractFileSystem] = None,
    unprocessed_gridfile_fs: Optional[AbstractFileSystem] = None,
) -> UcteImportResult:
    """Convert the UCTE file to a format that can be used by the RL agent.

    Saves data and network to the output folder.

    Parameters
    ----------
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters]
        Parameters that are required to import the data from a UCTE file. This will utilize
        powsybl and the powsybl backend to the loadflow solver
    status_update_fn: Callable[[PreprocessStage, Optional[str]]
        A function to call to signal progress in the preprocessing pipeline. Takes a stage and an
        optional message as parameters
    processed_gridfile_fs: Optional[AbstractFileSystem]
        A filesystem where the processed gridfiles are stored. If None, the local filesystem is used
    unprocessed_gridfile_fs: Optional[AbstractFileSystem]
        A filesystem where the unprocessed gridfiles are stored. If None, the local filesystem is used.

    Returns
    -------
    UcteImportResult
        The result of the import process.

    """
    if unprocessed_gridfile_fs is None:
        unprocessed_gridfile_fs = LocalFileSystem()
    if processed_gridfile_fs is None:
        processed_gridfile_fs = LocalFileSystem()
    # Copy original grid file
    copy_file_fs(
        src_fs=unprocessed_gridfile_fs,
        src_path=importer_parameters.grid_model_file.as_posix(),
        dest_fs=processed_gridfile_fs,
        dest_path=(
            importer_parameters.data_folder
            / PREPROCESSING_PATHS["original_gridfile_path"]
            / importer_parameters.grid_model_file.name
        ).as_posix(),
    )

    # load network
    status_update_fn("load_ucte", "start loading grid file")
    network = load_powsybl_from_fs(
        filesystem=unprocessed_gridfile_fs,
        file_path=importer_parameters.grid_model_file,
        parameters={"iidm.import.cgmes.post-processors": "cgmesGLImport"},
    )

    network_analysis.remove_branches_with_same_bus(network)
    status_update_fn("load_ucte", "done loading grid file")

    pypowsybl.network.replace_3_windings_transformers_with_3_2_windings_transformers(network)
    if pypowsybl.__version__ <= "1.12.0":
        # Fix the bug, where the operational limits of the 2winding transformers are not set correctly
        op_lim = network.get_operational_limits(all_attributes=True, show_inactive_sets=True)
        trafo3w_lims = op_lim[op_lim.index.str.contains("-Leg")][["group_name"]].rename(
            columns={"group_name": "selected_limits_group_1"}
        )
        trafo3w_lims.index.name = "id"
        network.update_2_windings_transformers(trafo3w_lims)

    statistics = PreProcessingStatistics(
        import_result=UcteImportResult(data_folder=importer_parameters.data_folder),
        import_parameter=importer_parameters,
    )
    status_update_fn("apply_cb_list", "Applying Whitelists")
    if importer_parameters.data_type == "ucte":
        # TODO: move to UCTE Toolset after all PRs are merged
        apply_preprocessing_changes_to_network(
            network=network,
            statistics=statistics,
            status_update_fn=status_update_fn,
        )

        # apply black and white list
        statistics = network_analysis.apply_cb_lists(
            network=network,
            statistics=statistics,
            white_list_file=importer_parameters.white_list_file,
            black_list_file=importer_parameters.black_list_file,
            fs=unprocessed_gridfile_fs,
        )
    elif importer_parameters.data_type == "cgmes":
        statistics.id_lists["white_list"] = []
        statistics.id_lists["black_list"] = []
        logger.warning("CGMES of white_list and black_list not yet implemented")

    # Save and reload Network due to powsybl changing order during save
    grid_file_path = importer_parameters.data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"]
    save_powsybl_to_fs(
        network,
        filesystem=processed_gridfile_fs,
        file_path=grid_file_path,
    )
    # Reload Network because powsybl likes to change order during save
    network = load_powsybl_from_fs(
        filesystem=processed_gridfile_fs,
        file_path=grid_file_path,
    )

    # get N-1 masks
    status_update_fn("get_masks", "Creating Network Masks")
    network_masks = get_network_masks(network, importer_parameters, statistics, filesystem=unprocessed_gridfile_fs)
    save_masks_to_filesystem(
        data_folder=importer_parameters.data_folder, network_masks=network_masks, filesystem=processed_gridfile_fs
    )

    # get nminus1 definition
    nminus1_definition = create_nminus1_definition_from_masks(network, network_masks)
    save_pydantic_model_fs(
        filesystem=processed_gridfile_fs,
        file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"],
        pydantic_model=nminus1_definition,
    )

    if (
        importer_parameters.area_settings.dso_trafo_factors is not None
        or importer_parameters.area_settings.border_line_factors is not None
    ):
        status_update_fn("cross_border_current", "Setting cross border current limit")
        lf_result = pypowsybl.loadflow.run_ac(network, parameters=DISTRIBUTED_SLACK)
        if lf_result[0].status != pypowsybl.loadflow.ComponentStatus.CONVERGED:
            pypowsybl.loadflow.run_dc(network, parameters=DISTRIBUTED_SLACK)
        create_new_border_limits(network, network_masks, importer_parameters)
        # save new border limits
        save_powsybl_to_fs(
            network,
            filesystem=processed_gridfile_fs,
            file_path=grid_file_path,
        )

    save_preprocessing_statistics_filesystem(
        statistics=statistics,
        file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["importer_auxiliary_file_path"],
        filesystem=processed_gridfile_fs,
    )

    status_update_fn("get_topology_model", "Creating Pydantic Topology Model")
    topology_model = get_topology_model(network, network_masks, importer_parameters)

    save_pydantic_model_fs(
        filesystem=processed_gridfile_fs,
        file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["asset_topology_file_path"],
        pydantic_model=topology_model,
        indent=4,
    )

    return statistics.import_result

load_preprocessing_statistics_filesystem #

load_preprocessing_statistics_filesystem(
    file_path, filesystem
)

Load the preprocessing statistics from the file.

PARAMETER DESCRIPTION
file_path

The file to load the preprocessing statistics from.

TYPE: Path

filesystem

The filesystem to load the file from.

TYPE: AbstractFileSystem

RETURNS DESCRIPTION
statistics

The loaded statistics.

TYPE: PreProcessingStatistics

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def load_preprocessing_statistics_filesystem(file_path: Path, filesystem: AbstractFileSystem) -> PreProcessingStatistics:
    """Load the preprocessing statistics from the file.

    Parameters
    ----------
    file_path: Path
        The file to load the preprocessing statistics from.
    filesystem: AbstractFileSystem
        The filesystem to load the file from.

    Returns
    -------
    statistics: PreProcessingStatistics
        The loaded statistics.

    """
    with filesystem.open(str(file_path), "r") as f:
        statistics = json.load(f)
    import_result = PreProcessingStatistics(**statistics)
    return import_result

save_preprocessing_statistics_filesystem #

save_preprocessing_statistics_filesystem(
    statistics, filesystem, file_path
)

Save the preprocessing statistics to the filesystem.

PARAMETER DESCRIPTION
statistics

The statistics to save.

TYPE: PreProcessingStatistics

file_path

The file to save the preprocessing statistics to.

TYPE: Union[str, Path]

filesystem

The filesystem to save the file to.

TYPE: AbstractFileSystem

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def save_preprocessing_statistics_filesystem(
    statistics: PreProcessingStatistics, filesystem: AbstractFileSystem, file_path: Union[str, Path]
) -> None:
    """Save the preprocessing statistics to the filesystem.

    Parameters
    ----------
    statistics: PreProcessingStatistics
        The statistics to save.
    file_path: Path
        The file to save the preprocessing statistics to.
    filesystem: AbstractFileSystem
        The filesystem to save the file to.
    """
    with filesystem.open(str(file_path), "w") as f:
        f.write(statistics.model_dump_json(indent=4))

toop_engine_importer.pypowsybl_import.preprocessing #

Module contains functions for the pypowsybl preprocessing for the grid export into the loadflow solver.

File: preprocessing.py Author: Benjamin Petrick Created: 2024-09-04

logger module-attribute #

logger = Logger(__name__)

CONVERTED_TRAFO3W_ENDING module-attribute #

CONVERTED_TRAFO3W_ENDING = '-Leg[123]$'

save_preprocessing_statistics_filesystem #

save_preprocessing_statistics_filesystem(
    statistics, filesystem, file_path
)

Save the preprocessing statistics to the filesystem.

PARAMETER DESCRIPTION
statistics

The statistics to save.

TYPE: PreProcessingStatistics

file_path

The file to save the preprocessing statistics to.

TYPE: Union[str, Path]

filesystem

The filesystem to save the file to.

TYPE: AbstractFileSystem

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def save_preprocessing_statistics_filesystem(
    statistics: PreProcessingStatistics, filesystem: AbstractFileSystem, file_path: Union[str, Path]
) -> None:
    """Save the preprocessing statistics to the filesystem.

    Parameters
    ----------
    statistics: PreProcessingStatistics
        The statistics to save.
    file_path: Path
        The file to save the preprocessing statistics to.
    filesystem: AbstractFileSystem
        The filesystem to save the file to.
    """
    with filesystem.open(str(file_path), "w") as f:
        f.write(statistics.model_dump_json(indent=4))

load_preprocessing_statistics_filesystem #

load_preprocessing_statistics_filesystem(
    file_path, filesystem
)

Load the preprocessing statistics from the file.

PARAMETER DESCRIPTION
file_path

The file to load the preprocessing statistics from.

TYPE: Path

filesystem

The filesystem to load the file from.

TYPE: AbstractFileSystem

RETURNS DESCRIPTION
statistics

The loaded statistics.

TYPE: PreProcessingStatistics

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def load_preprocessing_statistics_filesystem(file_path: Path, filesystem: AbstractFileSystem) -> PreProcessingStatistics:
    """Load the preprocessing statistics from the file.

    Parameters
    ----------
    file_path: Path
        The file to load the preprocessing statistics from.
    filesystem: AbstractFileSystem
        The filesystem to load the file from.

    Returns
    -------
    statistics: PreProcessingStatistics
        The loaded statistics.

    """
    with filesystem.open(str(file_path), "r") as f:
        statistics = json.load(f)
    import_result = PreProcessingStatistics(**statistics)
    return import_result

create_nminus1_definition_from_masks #

create_nminus1_definition_from_masks(
    network, network_masks
)

Create the N-1 definition from the network masks.

PARAMETER DESCRIPTION
network

The network to create the N-1 definition for.

TYPE: Network

network_masks

The network masks to create the N-1 definition from.

TYPE: NetworkMasks

RETURNS DESCRIPTION
Nminus1Definition

The created N-1 definition.

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def create_nminus1_definition_from_masks(network: Network, network_masks: NetworkMasks) -> Nminus1Definition:
    """Create the N-1 definition from the network masks.

    Parameters
    ----------
    network: Network
        The network to create the N-1 definition for.
    network_masks: NetworkMasks
        The network masks to create the N-1 definition from.

    Returns
    -------
    Nminus1Definition
        The created N-1 definition.
    """
    contingencies = [Contingency(id="BASECASE", name="BASECASE", elements=[])]

    lines = network.get_lines(attributes=["name"])
    monitored_lines = [
        GridElement(id=idx, name=row["name"], type="LINE", kind="branch")
        for idx, row in lines[network_masks.line_for_reward].iterrows()
    ]
    outaged_lines = [
        Contingency(id=idx, name=row["name"], elements=[GridElement(id=idx, name=row["name"], type="LINE", kind="branch")])
        for idx, row in lines[network_masks.line_for_nminus1].iterrows()
    ]

    trafos = network.get_2_windings_transformers(attributes=["name"])
    is_trafo2w = ~trafos.index.str.contains(CONVERTED_TRAFO3W_ENDING)
    monitored_trafos = [
        GridElement(id=idx, name=row["name"], type="TWO_WINDINGS_TRANSFORMER", kind="branch")
        for idx, row in trafos[is_trafo2w & network_masks.trafo_for_reward].iterrows()
    ]
    outaged_trafos = [
        Contingency(
            id=idx,
            name=row["name"],
            elements=[GridElement(id=idx, name=row["name"], type="TWO_WINDINGS_TRANSFORMER", kind="branch")],
        )
        for idx, row in trafos[is_trafo2w & network_masks.trafo_for_nminus1].iterrows()
    ]

    is_trafo3w = trafos.index.str.contains(CONVERTED_TRAFO3W_ENDING)
    trafos.index = trafos.index.str.replace(CONVERTED_TRAFO3W_ENDING, "", regex=True)
    if not trafos.empty:
        trafos.name = trafos.name.str.replace(CONVERTED_TRAFO3W_ENDING, "", regex=True)

    monitored_trafo3w = [
        GridElement(id=idx, name=row["name"], type="THREE_WINDINGS_TRANSFORMER", kind="branch")
        for idx, row in trafos[is_trafo3w & network_masks.trafo_for_reward].drop_duplicates().iterrows()
    ]
    outaged_trafo3w = [
        Contingency(
            id=idx,
            name=row["name"],
            elements=[GridElement(id=idx, name=row["name"], type="THREE_WINDINGS_TRANSFORMER", kind="branch")],
        )
        for idx, row in trafos[is_trafo3w & network_masks.trafo_for_nminus1].drop_duplicates().iterrows()
    ]

    tie_lines = network.get_tie_lines(attributes=["name"])
    monitored_tie_lines = [
        GridElement(id=idx, name=row["name"], type="TIE_LINE", kind="branch")
        for idx, row in tie_lines[network_masks.tie_line_for_reward].iterrows()
    ]
    outaged_tie_lines = [
        Contingency(
            id=idx, name=row["name"], elements=[GridElement(id=idx, name=row["name"], type="TIE_LINE", kind="branch")]
        )
        for idx, row in tie_lines[network_masks.tie_line_for_nminus1].iterrows()
    ]

    dangling_lines = network.get_dangling_lines(attributes=["name", "paired"])
    outaged_dangling = [
        Contingency(
            id=idx,
            name=row["name"],
            elements=[
                GridElement(id=idx, name=row["name"], type="DANGLING_LINE", kind="injection"),
            ],
        )
        for idx, row in dangling_lines[network_masks.dangling_line_for_nminus1 & ~dangling_lines["paired"]].iterrows()
    ]

    generators = network.get_generators(attributes=["name"])
    outaged_generators = [
        Contingency(
            id=idx, name=row["name"], elements=[GridElement(id=idx, name=row["name"], type="GENERATOR", kind="injection")]
        )
        for idx, row in generators[network_masks.generator_for_nminus1].iterrows()
    ]

    loads = network.get_loads(attributes=["name"])
    outaged_loads = [
        Contingency(
            id=idx, name=row["name"], elements=[GridElement(id=idx, name=row["name"], type="LOAD", kind="injection")]
        )
        for idx, row in loads[network_masks.load_for_nminus1].iterrows()
    ]

    switches = network.get_switches(attributes=["name"])
    monitored_switches = [
        GridElement(id=idx, name=row["name"], type="SWITCH", kind="branch")
        for idx, row in switches[network_masks.switch_for_reward].iterrows()
    ]
    outaged_switches = [
        Contingency(id=idx, name=row["name"], elements=[GridElement(id=idx, name=row["name"], type="SWITCH", kind="branch")])
        for idx, row in switches[network_masks.switch_for_nminus1].iterrows()
    ]

    buses = network.get_buses()
    relevant_bus_ids = buses.index[network_masks.relevant_subs].to_list()
    busbar_sections = network.get_busbar_sections(attributes=["name", "bus_id"])
    monitored_busbars = [
        GridElement(id=idx, name=row["name"], type="BUSBAR_SECTION", kind="bus")
        for idx, row in busbar_sections[busbar_sections.index.isin(relevant_bus_ids)].iterrows()
    ]
    busbreaker_buses = network.get_bus_breaker_view_buses(attributes=["name", "bus_id"])
    monitored_busbreakers = [
        GridElement(id=idx, name=row["name"], type="BUS_BREAKER_BUS", kind="bus")
        for idx, row in busbreaker_buses[busbreaker_buses.index.isin(relevant_bus_ids)].iterrows()
    ]

    nminus1_definition = Nminus1Definition(
        monitored_elements=(
            monitored_lines
            + monitored_trafos
            + monitored_trafo3w
            + monitored_tie_lines
            + monitored_switches
            + monitored_busbars
            + monitored_busbreakers
        ),
        contingencies=(
            contingencies
            + outaged_lines
            + outaged_trafos
            + outaged_trafo3w
            + outaged_tie_lines
            + outaged_dangling
            + outaged_generators
            + outaged_loads
            + outaged_switches
        ),
    )
    return nminus1_definition

convert_file #

convert_file(
    importer_parameters,
    status_update_fn=empty_status_update_fn,
    processed_gridfile_fs=None,
    unprocessed_gridfile_fs=None,
)

Convert the UCTE file to a format that can be used by the RL agent.

Saves data and network to the output folder.

PARAMETER DESCRIPTION
importer_parameters

Parameters that are required to import the data from a UCTE file. This will utilize powsybl and the powsybl backend to the loadflow solver

TYPE: Union[UcteImporterParameters, CgmesImporterParameters]

status_update_fn

A function to call to signal progress in the preprocessing pipeline. Takes a stage and an optional message as parameters

TYPE: Callable[[PreprocessStage, Optional[str]], None] DEFAULT: empty_status_update_fn

processed_gridfile_fs

A filesystem where the processed gridfiles are stored. If None, the local filesystem is used

TYPE: Optional[AbstractFileSystem] DEFAULT: None

unprocessed_gridfile_fs

A filesystem where the unprocessed gridfiles are stored. If None, the local filesystem is used.

TYPE: Optional[AbstractFileSystem] DEFAULT: None

RETURNS DESCRIPTION
UcteImportResult

The result of the import process.

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def convert_file(
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters],
    status_update_fn: Callable[[PreprocessStage, Optional[str]], None] = empty_status_update_fn,
    processed_gridfile_fs: Optional[AbstractFileSystem] = None,
    unprocessed_gridfile_fs: Optional[AbstractFileSystem] = None,
) -> UcteImportResult:
    """Convert the UCTE file to a format that can be used by the RL agent.

    Saves data and network to the output folder.

    Parameters
    ----------
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters]
        Parameters that are required to import the data from a UCTE file. This will utilize
        powsybl and the powsybl backend to the loadflow solver
    status_update_fn: Callable[[PreprocessStage, Optional[str]]
        A function to call to signal progress in the preprocessing pipeline. Takes a stage and an
        optional message as parameters
    processed_gridfile_fs: Optional[AbstractFileSystem]
        A filesystem where the processed gridfiles are stored. If None, the local filesystem is used
    unprocessed_gridfile_fs: Optional[AbstractFileSystem]
        A filesystem where the unprocessed gridfiles are stored. If None, the local filesystem is used.

    Returns
    -------
    UcteImportResult
        The result of the import process.

    """
    if unprocessed_gridfile_fs is None:
        unprocessed_gridfile_fs = LocalFileSystem()
    if processed_gridfile_fs is None:
        processed_gridfile_fs = LocalFileSystem()
    # Copy original grid file
    copy_file_fs(
        src_fs=unprocessed_gridfile_fs,
        src_path=importer_parameters.grid_model_file.as_posix(),
        dest_fs=processed_gridfile_fs,
        dest_path=(
            importer_parameters.data_folder
            / PREPROCESSING_PATHS["original_gridfile_path"]
            / importer_parameters.grid_model_file.name
        ).as_posix(),
    )

    # load network
    status_update_fn("load_ucte", "start loading grid file")
    network = load_powsybl_from_fs(
        filesystem=unprocessed_gridfile_fs,
        file_path=importer_parameters.grid_model_file,
        parameters={"iidm.import.cgmes.post-processors": "cgmesGLImport"},
    )

    network_analysis.remove_branches_with_same_bus(network)
    status_update_fn("load_ucte", "done loading grid file")

    pypowsybl.network.replace_3_windings_transformers_with_3_2_windings_transformers(network)
    if pypowsybl.__version__ <= "1.12.0":
        # Fix the bug, where the operational limits of the 2winding transformers are not set correctly
        op_lim = network.get_operational_limits(all_attributes=True, show_inactive_sets=True)
        trafo3w_lims = op_lim[op_lim.index.str.contains("-Leg")][["group_name"]].rename(
            columns={"group_name": "selected_limits_group_1"}
        )
        trafo3w_lims.index.name = "id"
        network.update_2_windings_transformers(trafo3w_lims)

    statistics = PreProcessingStatistics(
        import_result=UcteImportResult(data_folder=importer_parameters.data_folder),
        import_parameter=importer_parameters,
    )
    status_update_fn("apply_cb_list", "Applying Whitelists")
    if importer_parameters.data_type == "ucte":
        # TODO: move to UCTE Toolset after all PRs are merged
        apply_preprocessing_changes_to_network(
            network=network,
            statistics=statistics,
            status_update_fn=status_update_fn,
        )

        # apply black and white list
        statistics = network_analysis.apply_cb_lists(
            network=network,
            statistics=statistics,
            white_list_file=importer_parameters.white_list_file,
            black_list_file=importer_parameters.black_list_file,
            fs=unprocessed_gridfile_fs,
        )
    elif importer_parameters.data_type == "cgmes":
        statistics.id_lists["white_list"] = []
        statistics.id_lists["black_list"] = []
        logger.warning("CGMES of white_list and black_list not yet implemented")

    # Save and reload Network due to powsybl changing order during save
    grid_file_path = importer_parameters.data_folder / PREPROCESSING_PATHS["grid_file_path_powsybl"]
    save_powsybl_to_fs(
        network,
        filesystem=processed_gridfile_fs,
        file_path=grid_file_path,
    )
    # Reload Network because powsybl likes to change order during save
    network = load_powsybl_from_fs(
        filesystem=processed_gridfile_fs,
        file_path=grid_file_path,
    )

    # get N-1 masks
    status_update_fn("get_masks", "Creating Network Masks")
    network_masks = get_network_masks(network, importer_parameters, statistics, filesystem=unprocessed_gridfile_fs)
    save_masks_to_filesystem(
        data_folder=importer_parameters.data_folder, network_masks=network_masks, filesystem=processed_gridfile_fs
    )

    # get nminus1 definition
    nminus1_definition = create_nminus1_definition_from_masks(network, network_masks)
    save_pydantic_model_fs(
        filesystem=processed_gridfile_fs,
        file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["nminus1_definition_file_path"],
        pydantic_model=nminus1_definition,
    )

    if (
        importer_parameters.area_settings.dso_trafo_factors is not None
        or importer_parameters.area_settings.border_line_factors is not None
    ):
        status_update_fn("cross_border_current", "Setting cross border current limit")
        lf_result = pypowsybl.loadflow.run_ac(network, parameters=DISTRIBUTED_SLACK)
        if lf_result[0].status != pypowsybl.loadflow.ComponentStatus.CONVERGED:
            pypowsybl.loadflow.run_dc(network, parameters=DISTRIBUTED_SLACK)
        create_new_border_limits(network, network_masks, importer_parameters)
        # save new border limits
        save_powsybl_to_fs(
            network,
            filesystem=processed_gridfile_fs,
            file_path=grid_file_path,
        )

    save_preprocessing_statistics_filesystem(
        statistics=statistics,
        file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["importer_auxiliary_file_path"],
        filesystem=processed_gridfile_fs,
    )

    status_update_fn("get_topology_model", "Creating Pydantic Topology Model")
    topology_model = get_topology_model(network, network_masks, importer_parameters)

    save_pydantic_model_fs(
        filesystem=processed_gridfile_fs,
        file_path=importer_parameters.data_folder / PREPROCESSING_PATHS["asset_topology_file_path"],
        pydantic_model=topology_model,
        indent=4,
    )

    return statistics.import_result

get_network_masks #

get_network_masks(
    network, importer_parameters, statistics, filesystem
)

Create network masks and save them.

PARAMETER DESCRIPTION
network

The network to create the asset topology for

TYPE: Network

importer_parameters

import parameters that include the datafolder

TYPE: Union[UcteImporterParameters, CgmesImporterParameters]

statistics

preprocessing statistics to fill with information

TYPE: PreProcessingStatistics

filesystem

The filesystem to load the mask files from.

TYPE: AbstractFileSystem

RETURNS DESCRIPTION
NetworkMasks

The created network masks

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def get_network_masks(
    network: Network,
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters],
    statistics: PreProcessingStatistics,
    filesystem: AbstractFileSystem,
) -> NetworkMasks:
    """Create network masks and save them.

    Parameters
    ----------
    network: Network
        The network to create the asset topology for
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters]
        import parameters that include the datafolder
    statistics: PreProcessingStatistics
        preprocessing statistics to fill with information
    filesystem: AbstractFileSystem
        The filesystem to load the mask files from.

    Returns
    -------
    NetworkMasks
        The created network masks
    """
    network_masks = make_masks(
        network=network,
        importer_parameters=importer_parameters,
        blacklisted_ids=statistics.id_lists["black_list"],
        filesystem=filesystem,
    )
    fill_statistics_for_network_masks(network=network, statistics=statistics, network_masks=network_masks)
    return network_masks

get_topology_model #

get_topology_model(
    network, network_masks, importer_parameters
)

Get the initial asset topology.

PARAMETER DESCRIPTION
network

The network to create the asset topology for

TYPE: Network

network_masks

The network masks giving info which elements are relevant

TYPE: NetworkMasks

importer_parameters

import parameters that include the datafolder

TYPE: Union[UcteImporterParameters, CgmesImporterParameters]

RETURNS DESCRIPTION
None
Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def get_topology_model(
    network: Network,
    network_masks: NetworkMasks,
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters],
) -> Topology:
    """Get the initial asset topology.

    Parameters
    ----------
    network: Network
        The network to create the asset topology for
    network_masks: NetworkMasks
        The network masks giving info which elements are relevant
    importer_parameters: Union[UcteImporterParameters, CgmesImporterParameters]
        import parameters that include the datafolder

    Returns
    -------
    None
    """
    if importer_parameters.data_type == "ucte":
        topology_model = get_topology(
            network,
            relevant_stations=network_masks.relevant_subs,
            topology_id=importer_parameters.grid_model_file.name,
            grid_model_file=str(importer_parameters.grid_model_file),
        )
    elif importer_parameters.data_type == "cgmes":
        topology_model = powsybl_station_to_graph.get_topology(network, network_masks, importer_parameters)

    return topology_model

apply_preprocessing_changes_to_network #

apply_preprocessing_changes_to_network(
    network, statistics, status_update_fn=None
)

Apply the default changes to the network.

These changes include: - removing low impedance lines - removing branches across switches

PARAMETER DESCRIPTION
network

The network to apply the changes to. Note: This function modifies the network in place.

TYPE: Network

statistics

The statistics of the preprocessing. Note: This function modifies the statistics in place.

TYPE: PreProcessingStatistics

status_update_fn

A function to call to signal progress in the preprocessing pipeline. Takes a stage and an optional message as parameters

TYPE: Optional[Callable[[PreprocessStage, Optional[str]], None]] DEFAULT: None

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def apply_preprocessing_changes_to_network(
    network: Network,
    statistics: PreProcessingStatistics,
    status_update_fn: Optional[Callable[[PreprocessStage, Optional[str]], None]] = None,
) -> None:
    """Apply the default changes to the network.

    These changes include:
    - removing low impedance lines
    - removing branches across switches

    Parameters
    ----------
    network: Network
        The network to apply the changes to.
        Note: This function modifies the network in place.
    statistics: PreprocessingStatistics
        The statistics of the preprocessing.
        Note: This function modifies the statistics in place.
    status_update_fn: Optional[Callable[[PreprocessStage, Optional[str]], None]]
        A function to call to signal progress in the preprocessing pipeline. Takes a stage and an
        optional message as parameters

    """
    if status_update_fn is None:
        status_update_fn = empty_status_update_fn
    status_update_fn("modify_low_impedance_lines", "Converting low impedance lines to breakers")
    low_impedance_lines = network_analysis.convert_low_impedance_lines(network, "D8")
    statistics.import_result.n_low_impedance_lines = len(low_impedance_lines)
    statistics.network_changes["low_impedance_lines"] = low_impedance_lines.index.to_list()

    status_update_fn("modify_branches_over_switches", "Removing branches across switches")
    branches_across_switch = network_analysis.remove_branches_across_switch(network)
    statistics.import_result.n_branch_across_switch = len(branches_across_switch)
    statistics.network_changes["branches_across_switch"] = branches_across_switch.index.to_list()

fill_statistics_for_network_masks #

fill_statistics_for_network_masks(
    network, statistics, network_masks
)

Fill the statistics with the network masks.

PARAMETER DESCRIPTION
network

The network to get the id lists from.

TYPE: Network

statistics

The statistics to fill. Note: This function modifies the statistics in place.

TYPE: PreProcessingStatistics

network_masks

The masks for the network.

TYPE: NetworkMasks

Source code in packages/importer_pkg/src/toop_engine_importer/pypowsybl_import/preprocessing.py
def fill_statistics_for_network_masks(
    network: Network, statistics: PreProcessingStatistics, network_masks: NetworkMasks
) -> None:
    """Fill the statistics with the network masks.

    Parameters
    ----------
    network: Network
        The network to get the id lists from.
    statistics: PreprocessingStatistics
        The statistics to fill.
        Note: This function modifies the statistics in place.
    network_masks: NetworkMasks
        The masks for the network.

    """
    statistics.id_lists["relevant_subs"] = network.get_buses(attributes=[])[network_masks.relevant_subs].index.to_list()
    statistics.id_lists["line_for_nminus1"] = network.get_lines(attributes=[])[
        network_masks.line_for_nminus1
    ].index.to_list()
    statistics.id_lists["trafo_for_nminus1"] = network.get_2_windings_transformers(attributes=[])[
        network_masks.trafo_for_nminus1
    ].index.to_list()
    statistics.id_lists["tie_line_for_nminus1"] = network.get_tie_lines(attributes=[])[
        network_masks.tie_line_for_nminus1
    ].index.to_list()
    statistics.id_lists["dangling_line_for_nminus1"] = network.get_dangling_lines(attributes=[])[
        network_masks.dangling_line_for_nminus1
    ].index.to_list()
    statistics.id_lists["generator_for_nminus1"] = network.get_generators(attributes=[])[
        network_masks.generator_for_nminus1
    ].index.to_list()
    statistics.id_lists["load_for_nminus1"] = network.get_loads(attributes=[])[
        network_masks.load_for_nminus1
    ].index.to_list()
    statistics.id_lists["switch_for_nminus1"] = network.get_switches(attributes=[])[
        network_masks.switch_for_nminus1
    ].index.to_list()
    statistics.id_lists["line_disconnectable"] = network.get_lines(attributes=[])[
        network_masks.line_disconnectable
    ].index.to_list()

    statistics.import_result.n_relevant_subs = int(network_masks.relevant_subs.sum())
    statistics.import_result.n_line_for_nminus1 = int(network_masks.line_for_nminus1.sum())
    statistics.import_result.n_line_for_reward = int(network_masks.line_for_reward.sum())
    statistics.import_result.n_line_disconnectable = int(network_masks.line_disconnectable.sum())

    statistics.import_result.n_trafo_for_nminus1 = int(network_masks.trafo_for_nminus1.sum())
    statistics.import_result.n_trafo_for_reward = int(network_masks.trafo_for_reward.sum())
    statistics.import_result.n_tie_line_disconnectable = int(network_masks.tie_line_disconnectable.sum())
    statistics.import_result.n_tie_line_for_nminus1 = int(network_masks.tie_line_for_nminus1.sum())
    statistics.import_result.n_tie_line_for_reward = int(network_masks.tie_line_for_reward.sum())
    statistics.import_result.n_tie_line_disconnectable = int(network_masks.tie_line_disconnectable.sum())
    statistics.import_result.n_dangling_line_for_nminus1 = int(network_masks.dangling_line_for_nminus1.sum())
    statistics.import_result.n_generator_for_nminus1 = int(network_masks.generator_for_nminus1.sum())
    statistics.import_result.n_load_for_nminus1 = int(network_masks.load_for_nminus1.sum())
    statistics.import_result.n_switch_for_nminus1 = int(network_masks.switch_for_nminus1.sum())
    statistics.import_result.n_switch_for_reward = int(network_masks.switch_for_reward.sum())

Network Graph#

toop_engine_importer.network_graph #

Functions to get an AssetTopology from a Node-Breaker grid model.

BRANCH_TYPES module-attribute #

BRANCH_TYPES_PANDAPOWER module-attribute #

BRANCH_TYPES_PANDAPOWER = Literal[
    "line",
    "trafo",
    "trafo3w",
    "dcline",
    "tcsc",
    "impedance",
]

BRANCH_TYPES_POWSYBL module-attribute #

BRANCH_TYPES_POWSYBL = Literal[
    "LINE",
    "TWO_WINDING_TRANSFORMER",
    "PHASE_SHIFTER",
    "TWO_WINDING_TRANSFORMER_WITH_TAP_CHANGER",
    "THREE_WINDINGS_TRANSFORMER",
]

NODE_TYPES module-attribute #

NODE_TYPES = Literal['busbar', 'node']

SWITCH_TYPES module-attribute #

SWITCH_TYPES = Literal[
    "DISCONNECTOR",
    "BREAKER",
    "LOAD_BREAK_SWITCH",
    "CB",
    "DS",
    "LBS",
]

__all__ module-attribute #

__all__ = [
    "BRANCH_TYPES",
    "BRANCH_TYPES_PANDAPOWER",
    "BRANCH_TYPES_POWSYBL",
    "NODE_TYPES",
    "SWITCH_TYPES",
    "AssetSchema",
    "BranchSchema",
    "BusbarConnectionInfo",
    "EdgeConnectionInfo",
    "HelperBranchSchema",
    "NetworkGraphData",
    "NodeAssetSchema",
    "NodeSchema",
    "SubstationInformation",
    "SwitchSchema",
    "WeightValues",
    "add_graph_specific_data",
    "add_node_tuple_column",
    "generate_graph",
    "get_asset_bay",
    "get_busbar_df",
    "get_coupler_df",
    "get_empty_dataframe_from_df_model",
    "get_network_graph",
    "get_network_graph_data",
    "get_node_breaker_topology_graph",
    "get_station_connection_tables",
    "get_switchable_asset",
    "node_breaker_topology_to_graph_data",
    "remove_helper_branches",
    "run_default_filter_strategy",
    "set_all_busbar_coupling_switches",
    "set_all_weights",
    "set_asset_bay_edge_attr",
    "set_bay_weights",
    "set_connectable_busbars",
    "set_empty_bay_weights",
    "set_switch_busbar_connection_info",
    "set_zero_impedance_connected",
    "shortest_paths_to_target_ids",
]

AssetSchema #

Bases: DataFrameModel

An AssetSchema is a DataFrameModel that represents an asset in a network graph.

This is the parent class for SwitchSchema and BranchSchema and should not be used directly.

grid_model_id instance-attribute #

grid_model_id

The unique ID of the node in the grid model.

foreign_id class-attribute instance-attribute #

foreign_id = Field(coerce=False)

The unique ID of the node in the foreign model. This id is optional is only dragged along. Can for instance used for the DGS model.

asset_type instance-attribute #

asset_type

The type of the asset.

int_id class-attribute instance-attribute #

int_id = Field(
    check_name=False, description="Index of Dataframe"
)

The int_id of the asset. This is the index of the dataframe and is expected to be unique for the asset DataFrame and of type int.

in_service class-attribute instance-attribute #

in_service = Field(default=True)

The state of the asset. True: The asset is in service. Normally expected to be True or not included in the network graph.

BranchSchema #

Bases: AssetSchema

A BranchSchema is an AssetSchema that represents a branch in a network graph.

A branch is a connection between two nodes in a network graph. It can be for instance a LINE, TWO_WINDING_TRANSFORMER, but not a THREE_WINDING_TRANSFORMER. A SwitchSchema is a special type of BranchSchema that represents a switch in a network graph.

grid_model_id instance-attribute #

grid_model_id

The unique ID of the node in the grid model.

foreign_id class-attribute instance-attribute #

foreign_id = Field(coerce=False)

The unique ID of the node in the foreign model. This id is optional is only dragged along. Can for instance used for the DGS model.

int_id class-attribute instance-attribute #

int_id = Field(
    check_name=False, description="Index of Dataframe"
)

The int_id of the asset. This is the index of the dataframe and is expected to be unique for the asset DataFrame and of type int.

in_service class-attribute instance-attribute #

in_service = Field(default=True)

The state of the asset. True: The asset is in service. Normally expected to be True or not included in the network graph.

from_node instance-attribute #

from_node

The nodes int_id of the node where the branch starts.

to_node instance-attribute #

to_node

The nodes int_id of the node where the branch ends.

asset_type class-attribute instance-attribute #

asset_type = Field(
    isin=[
        branch_type
        for branch_type_model in (__args__)
        for branch_type in (__args__)
    ]
)

The type of the branch.

node_tuple class-attribute instance-attribute #

node_tuple = Field(
    default=None, nullable=True, description="optional"
)

The node tuple of the branch. The node tuple is a tuple of two nodes int_id that are connected by the branch.

BusbarConnectionInfo #

Bases: BaseModel

BusbarConnectionInfo is a data class that contains information about the connections of a busbar.

The given connection information is always related to a specific busbar. A connection that is only possible when including another busbar in it's path is never included.

model_config class-attribute instance-attribute #

model_config = ConfigDict(extra='forbid')

The configuration for the model. The extra parameter is set to forbid, to raise an error if an unexpected field is passed to the model.

connectable_assets_node_ids class-attribute instance-attribute #

connectable_assets_node_ids = Field(default_factory=list)

The node_ids of the connectable assets. Connectable is referring to the assets that can be connected and disconnected with a circuit breaker switch. A connectable asset can be a single node or a tuple of two nodes, if the asset is a branch.

connectable_assets class-attribute instance-attribute #

connectable_assets = Field(default_factory=list)

The connectable assets grid_model_id. Connectable is referring to the assets that can be connected and disconnected

connectable_busbars_node_ids class-attribute instance-attribute #

connectable_busbars_node_ids = Field(default_factory=list)

The node_ids of the connectable busbars. Interconnectable is referring to the busbars that can be connected and disconnected with a circuit breaker switch.

connectable_busbars class-attribute instance-attribute #

connectable_busbars = Field(default_factory=list)

The connectable busbars grid_model_id. Interconnectable is referring to the busbars that can be connected and disconnected with a circuit breaker switch.

zero_impedance_connected_assets class-attribute instance-attribute #

zero_impedance_connected_assets = Field(
    default_factory=list
)

The asset grid_model_ids, connected by zero impedance. Zero impedance refers to assets that are currently connected to the busbar with a path where all switches are closed. Only the asset bays are considered.

zero_impedance_connected_assets_node_ids class-attribute instance-attribute #

zero_impedance_connected_assets_node_ids = Field(
    default_factory=list
)

The asset node_ids, connected by zero impedance. Zero impedance refers to assets that are currently connected to the busbar with a path where all switches are closed. Only the asset bays are considered.

zero_impedance_connected_busbars class-attribute instance-attribute #

zero_impedance_connected_busbars = Field(
    default_factory=list
)

The busbars grid_model_ids, connected by zero impedance. Zero impedance refers to busbars that are currently connected to the busbar with a path where all switches are closed. Only busbar/cross coupler are considered that directly connect the two busbars.

zero_impedance_connected_busbars_node_ids class-attribute instance-attribute #

zero_impedance_connected_busbars_node_ids = Field(
    default_factory=list
)

The busbars node_ids, connected by zero impedance Zero impedance refers to busbars that are currently connected to the busbar with a path where all switches are closed. Only busbar/cross coupler are considered that directly connect the two busbars.

node_assets class-attribute instance-attribute #

node_assets = Field(default_factory=list)

The node assets grid_model_id. Node assets are assets that are connected to a node in the network graph. For instance, a transformer or line at the border of the network graph or a generator or load.

node_assets_ids class-attribute instance-attribute #

node_assets_ids = Field(default_factory=list)

The node assets node_id. Node assets are assets that are connected to a node in the network graph. For instance, a transformer or line at the border of the network graph or a generator or load. The node asset is for instance the index of the node_assets DataFrame or the node_tuple of the branch.

EdgeConnectionInfo #

Bases: BaseModel

EdgeConnectionInfo is a data class that contains information about an edge in relation to busbars or bays.

Use this class to store information about the connection of an edge.

model_config class-attribute instance-attribute #

model_config = ConfigDict(extra='forbid')

The configuration for the model. The extra parameter is set to forbid, to raise an error if an unexpected field is passed to the model.

direct_busbar_grid_model_id class-attribute instance-attribute #

direct_busbar_grid_model_id = ''

The direct_busbar_grid_model_id is set only if the edge is directly connected to a busbar. Leave blank if there is any edge in between the busbar and the edge. Fill with the grid_model_id of the busbar if the edge is directly connected to the busbar.

bay_id class-attribute instance-attribute #

bay_id = ''

The bay_id is set only if the edge is part of an asset/coupler bay. Leave blank if the edge is not part of an asset/coupler bay. Fill with the grid_model_id of the asset/coupler if the edge is part of an asset/coupler bay.

coupler_type class-attribute instance-attribute #

coupler_type = ''

The coupler_type is set only if the edge is part of a busbar coupler. Leave blank if the edge is not part of a busbar coupler. Fill with the type of the busbar coupler if the edge is part of a busbar coupler.

coupler_grid_model_id_list class-attribute instance-attribute #

coupler_grid_model_id_list = Field(default_factory=list)

The coupler_grid_model_id_list is set only if the edge is part of a busbar coupler. Leave blank if the edge is not part of a busbar coupler. Fill with the grid_model_id of the busbar coupler if the edge is part of a busbar coupler. Reason for tuples: one coupler can connect only two busbars at once. Reason for list: one coupler can have multiple from and to busbars. Example: [("busbar1", "busbar2"), ("busbar1", "busbar3"), ("busbar2", "busbar3")]

from_busbar_grid_model_ids class-attribute instance-attribute #

from_busbar_grid_model_ids = Field(default_factory=list)

The from_busbar_grid_model_ids is set only if the edge is part of a busbar coupler. Leave blank if the edge is not part of a busbar coupler. Fill with the grid_model_id of the busbar if the edge is part of a busbar coupler. Reason for list: one coupler can have multiple from busbars. Example: ["busbar1", "busbar2", "busbar3"]

to_busbar_grid_model_ids class-attribute instance-attribute #

to_busbar_grid_model_ids = Field(default_factory=list)

The to_busbar_grid_model_ids is set only if the edge is part of a busbar coupler. Leave blank if the edge is not part of a busbar coupler. Fill with the grid_model_id of the busbar if the edge is part of a busbar coupler. Reason for list: one coupler can have multiple to busbars. Example: ["busbar1", "busbar2", "busbar3"]

from_coupler_ids class-attribute instance-attribute #

from_coupler_ids = Field(default_factory=list)

The from_coupler_ids is set only if the edge is part of a busbar coupler. Leave blank if the edge is not part of a busbar coupler. Fill with the grid_model_id all edges that are part of the busbar coupler. Enables the correct identification an coupler bay.

to_coupler_ids class-attribute instance-attribute #

to_coupler_ids = Field(default_factory=list)

The to_coupler_ids is set only if the edge is part of a busbar coupler. Leave blank if the edge is not part of a busbar coupler. Fill with the grid_model_id all edges that are part of the busbar coupler. Enables the correct identification an coupler bay.

HelperBranchSchema #

Bases: DataFrameModel

A HelperBranchSchema is a BranchSchema that represents a helper branch in a network graph.

Helper branches no real branches, but are used to connect nodes in the network graph. These branches can occur for instance when there is an other abstraction level in the network graph e.g. a node for plotting svg. Note: The HelperBranch may contain all branches and switches in addition to the helper branches.

from_node instance-attribute #

from_node

The nodes int_id of the node where the branch starts.

to_node instance-attribute #

to_node

The nodes int_id of the node where the branch ends.

grid_model_id class-attribute instance-attribute #

grid_model_id = Field(isin=[''])

A helper branch does not have a grid_model_id. It is set to an empty string, creating all edges with a grid_model_id.

NetworkGraphData #

Bases: BaseModel

A NetworkGraphData contains all data to create a nx.Graph from a grid model (e.g. from CGMES).

It contains nodes, switches, branches and node_assets. This network is used to find substations and categorize the elements of the substation into known categories. It can be used to create an AssetTopology model and an action_set for the substation.

nodes instance-attribute #

nodes

A DataFrame containing the nodes.

switches instance-attribute #

switches

A DataFrame containing the switches.

branches class-attribute instance-attribute #

branches = Field(
    default_factory=lambda: get_empty_dataframe_from_df_model(
        df_model=BranchSchema
    )
)

A DataFrame containing the branches.

node_assets class-attribute instance-attribute #

node_assets = Field(
    default_factory=lambda: get_empty_dataframe_from_df_model(
        df_model=NodeAssetSchema
    )
)

A DataFrame containing the node assets

helper_branches class-attribute instance-attribute #

helper_branches = Field(
    default_factory=lambda: get_empty_dataframe_from_df_model(
        df_model=HelperBranchSchema
    )
)

A DataFrame containing the helper branches.

validate_network_graph_data #

validate_network_graph_data()

Validate the NetworkGraphData model.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/data_classes.py
@model_validator(mode="after")
def validate_network_graph_data(self) -> Self:
    """Validate the NetworkGraphData model."""
    if self.branches.empty and self.node_assets.empty:
        raise ValueError("Branches or node_assets must be provided.")
    # validate the input data
    self.nodes = NodeSchema.validate(self.nodes)
    self.switches = SwitchSchema.validate(self.switches)
    self.branches = BranchSchema.validate(self.branches)
    self.node_assets = NodeAssetSchema.validate(self.node_assets)
    self.helper_branches = HelperBranchSchema.validate(self.helper_branches)
    return self

NodeAssetSchema #

Bases: AssetSchema

A NodeAssetSchema is an AssetSchema that represents an asset in a network graph.

A NodeAssetSchema is an asset that is located at a node in the network graph. It can be for instance a transformer or line at the border of the network graph or a generator or load.

grid_model_id instance-attribute #

grid_model_id

The unique ID of the node in the grid model.

foreign_id class-attribute instance-attribute #

foreign_id = Field(coerce=False)

The unique ID of the node in the foreign model. This id is optional is only dragged along. Can for instance used for the DGS model.

asset_type instance-attribute #

asset_type

The type of the asset.

int_id class-attribute instance-attribute #

int_id = Field(
    check_name=False, description="Index of Dataframe"
)

The int_id of the asset. This is the index of the dataframe and is expected to be unique for the asset DataFrame and of type int.

in_service class-attribute instance-attribute #

in_service = Field(default=True)

The state of the asset. True: The asset is in service. Normally expected to be True or not included in the network graph.

node instance-attribute #

node

The nodes_index of the node where the asset is located.

NodeSchema #

Bases: DataFrameModel

A NodeSchema is a DataFrameModel that represents a node in a network graph.

int_id class-attribute instance-attribute #

int_id = Field(
    check_name=False, description="Index of Dataframe"
)

The int_id of the node, used to connect Assets by their node_id. This needs to be a unique int_id for the nodes DataFrame.

grid_model_id instance-attribute #

grid_model_id

The unique ID of the node in the grid model.

foreign_id class-attribute instance-attribute #

foreign_id = Field(coerce=False)

The unique ID of the node in the foreign model. This id is optional is only dragged along. Can for instance used for the DGS model.

node_type class-attribute instance-attribute #

node_type = Field(isin=__args__)

The type of the node NODE_TYPES.

voltage_level class-attribute instance-attribute #

voltage_level = Field(
    in_range={"min_value": 0, "max_value": 800}
)

The voltage level of the node.

bus_id class-attribute instance-attribute #

bus_id = Field(nullable=True)

The bus_id of the node. The bus_id is the refers to the id in the bus-branch topology.

system_operator instance-attribute #

system_operator

The system operator of the node. Can be used to categorize the node. For instance to identify border lines.

substation_id class-attribute instance-attribute #

substation_id = Field(nullable=True)

The unique ID of the substation. This id is optional and can be used to identify the substation the node is part of. If set as an empty string, the node is not part of a relevant substation. If set as None, the node is part of an unknown substation.

helper_node class-attribute instance-attribute #

helper_node = Field(default=False)

A helper node is a node that is used to connect other nodes in the network graph. Helper nodes are not part of the network and do not contain any information.

in_service class-attribute instance-attribute #

in_service = Field(default=True)

The state of the node. True: The node is in service. Normally expected to be True or not included in the network graph.

SubstationInformation #

Bases: BaseModel

SubstationInformation contains information about a powsybl substation.

model_config class-attribute instance-attribute #

model_config = ConfigDict(extra='forbid')

name instance-attribute #

name

The name of the substation. from net.get_substations()["name"]

region instance-attribute #

region

The region of the substation. from net.get_substations(attributes=["country"])

voltage_level_id instance-attribute #

voltage_level_id

The voltage level of the substation. from net.get_voltage_levels()

nominal_v instance-attribute #

nominal_v

The nominal voltage of the substation. from net.get_voltage_levels()

SwitchSchema #

Bases: AssetSchema

A SwitchSchema is a BranchSchema that represents a switch in a network graph.

grid_model_id instance-attribute #

grid_model_id

The unique ID of the node in the grid model.

foreign_id class-attribute instance-attribute #

foreign_id = Field(coerce=False)

The unique ID of the node in the foreign model. This id is optional is only dragged along. Can for instance used for the DGS model.

int_id class-attribute instance-attribute #

int_id = Field(
    check_name=False, description="Index of Dataframe"
)

The int_id of the asset. This is the index of the dataframe and is expected to be unique for the asset DataFrame and of type int.

in_service class-attribute instance-attribute #

in_service = Field(default=True)

The state of the asset. True: The asset is in service. Normally expected to be True or not included in the network graph.

from_node instance-attribute #

from_node

The nodes int_id of the node where the branch starts.

to_node instance-attribute #

to_node

The nodes int_id of the node where the branch ends.

asset_type class-attribute instance-attribute #

asset_type = Field(isin=__args__)

The type of the switch SWITCH_TYPES.

open instance-attribute #

open

The state of the switch. True: The switch is open. False: The switch is closed.

node_tuple class-attribute instance-attribute #

node_tuple = Field(
    default=None, nullable=True, description="optional"
)

The node tuple of the branch. The node tuple is a tuple of two nodes int_id that are connected by the branch.

WeightValues #

Bases: Enum

WeightValues is an Enum that contains the weight values for the edges in the NetworkGraphData model.

The weight values are used to map assets to categories in the network graph. high: A values used to cut off shortest paths. half: A value confidently ignore the steps, but still use a weight, where step or a few max_step are set. low: A value to ignore the edge in the shortest path. step: A value to count the steps in the shortest path. max_step: A value to set the cutoff in the shortest path. over_step: A value to set the cutoff in the shortest path. Used if this edge should not be part in the max_step cutoff, but may be part of a longer path max_coupler: A max value, counting switches in a busbar coupler path.

high class-attribute instance-attribute #

high = 100.0

half class-attribute instance-attribute #

half = 50.0

low class-attribute instance-attribute #

low = 0.0

step class-attribute instance-attribute #

step = 1.0

max_step class-attribute instance-attribute #

max_step = 10.0

over_step class-attribute instance-attribute #

over_step = 11.0

max_coupler class-attribute instance-attribute #

max_coupler = 5.0

get_empty_dataframe_from_df_model #

get_empty_dataframe_from_df_model(df_model)

Get an empty DataFrame from a DataFrameModel.

This functions creates an empty DataFrame with the columns and correct dtype of the DataFrameModel. It does not initialize Optional columns.

PARAMETER DESCRIPTION
df_model

The DataFrameModel to get an empty DataFrame from.

TYPE: DataFrameModel

RETURNS DESCRIPTION
DataFrame

An empty DataFrame with the columns and correct dtype of the DataFrameModel.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/data_classes.py
def get_empty_dataframe_from_df_model(df_model: pa.DataFrameModel) -> pd.DataFrame:
    """Get an empty DataFrame from a DataFrameModel.

    This functions creates an empty DataFrame with the columns and correct dtype of the DataFrameModel.
    It does not initialize Optional columns.

    Parameters
    ----------
    df_model : pa.DataFrameModel
        The DataFrameModel to get an empty DataFrame from.

    Returns
    -------
    pd.DataFrame
        An empty DataFrame with the columns and correct dtype of the DataFrameModel.
    """
    schema = df_model.to_schema()
    columns_dtypes = {
        column_name: column_type.type
        for column_name, column_type in schema.dtypes.items()
        if schema.columns[column_name].description != "optional"
    }
    return pd.DataFrame(columns=columns_dtypes.keys()).astype(columns_dtypes)

run_default_filter_strategy #

run_default_filter_strategy(graph)

Run the default strategy on the nx.Graph created by the NetworkGraphData model.

This is the main function for the default strategy. The default strategy fills - BusbarConnectionInfo of all busbars in the network graph. - EdgeConnectionInfo of all edges in the network graph.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def run_default_filter_strategy(graph: nx.Graph) -> None:
    """Run the default strategy on the nx.Graph created by the NetworkGraphData model.

    This is the main function for the default strategy.
    The default strategy fills
    - BusbarConnectionInfo of all busbars in the network graph.
    - EdgeConnectionInfo of all edges in the network graph.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    set_switch_busbar_connection_info(graph=graph)
    set_bay_weights(graph=graph)
    set_empty_bay_weights(graph=graph)
    set_connectable_busbars(graph=graph)
    set_all_busbar_coupling_switches(graph=graph)
    set_zero_impedance_connected(graph=graph)

set_all_busbar_coupling_switches #

set_all_busbar_coupling_switches(graph)

Set all connection paths between busbars, be it BREAKER or DISCONNECTOR.

  1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker)
  2. Loop over BREAKER one by one 2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights) 2.2 Set coupler sides 2.3 After loop: Set bay_id 2.4 Set coupler type
  3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B)
  4. Repeat step 2. for DS
PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_all_busbar_coupling_switches(
    graph: nx.Graph,
) -> None:
    """Set all connection paths between busbars, be it BREAKER or DISCONNECTOR.

    1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker)
    2. Loop over BREAKER one by one
        2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights)
        2.2 Set coupler sides
        2.3 After loop: Set bay_id
        2.4 Set coupler type
    3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B)
    4. Repeat step 2. for DS

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
    """
    # 1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker)
    no_bay_breaker_edges = get_switches_with_no_bay_id(graph=graph, asset_type="BREAKER")
    # 2. Loop over BREAKER one by one
    # 2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights)
    set_switch_bay_from_edge_ids(graph=graph, edge_ids=no_bay_breaker_edges)
    # 3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B)
    no_bay_breaker_edges = get_switches_with_no_bay_id(graph=graph, asset_type="DISCONNECTOR")
    set_switch_bay_from_edge_ids(graph=graph, edge_ids=no_bay_breaker_edges)

set_asset_bay_edge_attr #

set_asset_bay_edge_attr(graph, asset_bay_update_dict)

Set the bay information in the nx.Graph.

This function updates the initial edge weights and id related to the asset bay. Sets the bay_id, bay_weight, and coupler_weight for each edge in the shortest path to a busbar.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

asset_bay_update_dict

A dictionary containing the shortest path to a busbar for each busbar. key: bay_id (a str grid_model_id) value: dictionary from shortest_paths_to_target_ids key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[str, dict[int, list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/helper_functions.py
def set_asset_bay_edge_attr(
    graph: nx.Graph,
    asset_bay_update_dict: dict[str, dict[int, list[int]]],
) -> None:
    """Set the bay information in the nx.Graph.

    This function updates the initial edge weights and id related to the asset bay.
    Sets the bay_id, bay_weight, and coupler_weight for each edge in the shortest path to a busbar.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    asset_bay_update_dict : dict[str, dict[int, list[int]]]
        A dictionary containing the shortest path to a busbar for each busbar.
        key: bay_id (a str grid_model_id)
        value: dictionary from shortest_paths_to_target_ids
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    """
    edge_update_dict = {}
    content_dict = {
        "bay_weight": WeightValues.over_step.value,
        "coupler_weight": WeightValues.over_step.value,
    }
    for grid_model_id, shortest_path_to_busbar_dict in asset_bay_update_dict.items():
        for path in shortest_path_to_busbar_dict.values():
            # TODO: edge_update_dict.update does not work if one bay has multiple assets
            # refactor bay_id to list[str]
            # current behavior: if multiple assets are connected to the same bay,
            # the bay_id is set to the last asset in the path
            # set bay_id, bay_weight, coupler_weight
            edge_update_dict.update({(from_id, to_id): {"bay_id": grid_model_id} for from_id, to_id in pairwise(path)})
            update_dict = {(s_id, t_id): content_dict for s_id, t_id in pairwise(path)}
            nx.set_edge_attributes(graph, update_dict)
    update_edge_connection_info(graph=graph, update_edge_dict=edge_update_dict)

set_bay_weights #

set_bay_weights(graph)

Set the bay weights in the NetworkGraphData model for asset_nodes and branches.

The bay weight is used to categorize paths in the network and assign a bay to an asset. Enables DGS export and switching into topology using a branch_id, by linking the branch_id to a switch.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_bay_weights(graph: nx.Graph) -> dict[int, list[int]]:
    """Set the bay weights in the NetworkGraphData model for asset_nodes and branches.

    The bay weight is used to categorize paths in the network and assign a bay to an asset.
    Enables DGS export and switching into topology using a branch_id, by linking the branch_id to a switch.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    update_node_dict, bay_update_dict = get_asset_bay_update_dict(graph=graph)
    set_asset_bay_edge_attr(graph=graph, asset_bay_update_dict=bay_update_dict)
    update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict)

set_connectable_busbars #

set_connectable_busbars(graph)

Set the connectable busbars in BusbarConnectionInfo for each node in connectable_busbars.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_connectable_busbars(graph: nx.Graph) -> None:
    """Set the connectable busbars in BusbarConnectionInfo for each node in connectable_busbars.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    connectable_busbars, _busbar_shortest_path = calculate_connectable_busbars(graph=graph)
    update_node_dict = get_connectable_busbars_update_dict(shortest_path=connectable_busbars, graph=graph)
    update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict)

set_empty_bay_weights #

set_empty_bay_weights(graph)

Set a bay weight for empty bays.

Due to data quality issues, some bays may be empty. Finding and categorizing couplers depends on all bays weights being set. This function sets a bay weight for empty bays.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/empty_bay.py
def set_empty_bay_weights(graph: nx.Graph) -> None:
    """Set a bay weight for empty bays.

    Due to data quality issues, some bays may be empty.
    Finding and categorizing couplers depends on all bays weights being set.
    This function sets a bay weight for empty bays.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    empty_asset_bay_lists = get_empty_bay_list(graph=graph)
    update_dict = get_empty_bay_update_dict(empty_bay_lists=empty_asset_bay_lists)
    nx.set_edge_attributes(graph, update_dict)

set_switch_busbar_connection_info #

set_switch_busbar_connection_info(graph)

Set the switch busbar connection information in the network graph model.

why: - cut off the shortest path at a busbar - direct_busbar_grid_model_id in combination with bay_id for switching table uses: - graph.nodes[node_id]["node_type"] == "busbar" sets: - set the direct_busbar_grid_model_id in the switches - set the busbar_weight to step for all edges around a busbar. This enables to cut off a shortest path at a busbar.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_switch_busbar_connection_info(graph: nx.Graph) -> None:
    """Set the switch busbar connection information in the network graph model.

    why:
        - cut off the shortest path at a busbar
        - direct_busbar_grid_model_id in combination with bay_id for switching table
    uses:
        - graph.nodes[node_id][`"node_type"`] == "busbar"
    sets:
        - set the direct_busbar_grid_model_id in the switches
        - set the busbar_weight to step for all edges around a busbar.
          This enables to cut off a shortest path at a busbar.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    busbars = get_node_list_by_attribute(graph=graph, attribute="node_type", value=["busbar"])
    min_len_path = 2  # since we are searching for an edge, we need two nodes
    update_edge_dict = {}
    for busbar_id in busbars:
        # helper_nodes = get_helper_node_ids(network_graph_data)
        helper_nodes = get_helper_node_ids(graph=graph)
        length, path = nx.single_source_dijkstra(
            graph, source=busbar_id, cutoff=WeightValues.step.value, weight="station_weight"
        )
        # sort out connections that span over multiple busbars
        # take only none helper nodes -> jump over to the next true node
        # save k: busbar v: a non helper node and none busbar
        station_switch_nodes = {
            k: v
            for k, v in path.items()
            if (k not in helper_nodes)
            and (len(v) >= min_len_path)
            and (v[-2] not in helper_nodes)
            and (length[k] == WeightValues.step.value)
        }
        # get only the shortest path -> if there is a helper branch at the end, it is filtered out here
        station_switch_nodes = {key: station_switch_nodes[key] for key in find_shortest_path_ids(station_switch_nodes)}
        busbar_grid_model_id = graph.nodes[busbar_id]["grid_model_id"]
        update_edge_dict.update(
            {(v[-2], v[-1]): {"direct_busbar_grid_model_id": busbar_grid_model_id} for v in station_switch_nodes.values()}
        )
        # set busbar_weight to graph
        update_dict = {(v[-2], v[-1]): {"busbar_weight": WeightValues.max_step.value} for v in station_switch_nodes.values()}
        nx.set_edge_attributes(graph, update_dict)
    update_edge_connection_info(graph=graph, update_edge_dict=update_edge_dict)

set_zero_impedance_connected #

set_zero_impedance_connected(graph)

Set zero_impedance_connected in the graph model.

A shortest path to the connected assets respecting the switch open state is calculated.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_zero_impedance_connected(graph: nx.Graph) -> None:
    """Set zero_impedance_connected in the graph model.

    A shortest path to the connected assets respecting the switch open state is calculated.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified
    """
    busbar_ids = get_node_list_by_attribute(graph=graph, attribute="node_type", value=["busbar"])
    update_node_dict = {}
    for busbar_id in busbar_ids:
        connected_assets_dict = calculate_zero_impedance_connected(graph=graph, busbar_id=busbar_id)
        connected_assets = []
        connected_asset_node_ids = []
        for node_id, connected_asset_list in connected_assets_dict.items():
            if connected_asset_list != []:
                connected_assets += connected_asset_list
                connected_asset_node_ids.append(node_id)

        connected_busbar_node_ids = [node_id for node_id in connected_assets_dict.keys() if node_id in busbar_ids]
        connected_busbars = [graph.nodes[node_id]["grid_model_id"] for node_id in connected_busbar_node_ids]
        update_node_dict[busbar_id] = {
            "zero_impedance_connected_assets": connected_assets,
            "zero_impedance_connected_assets_node_ids": connected_asset_node_ids,
            "zero_impedance_connected_busbars": connected_busbars,
            "zero_impedance_connected_busbars_node_ids": connected_busbar_node_ids,
        }
    update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict)

shortest_paths_to_target_ids #

shortest_paths_to_target_ids(
    graph,
    target_node_ids,
    start_node_id,
    weight="station_weight",
    cutoff=int(high.value),
)

Find the shortest paths from one busbar to a list of busbars in the NetworkX graph.

PARAMETER DESCRIPTION
graph

The NetworkX graph.

TYPE: Graph

target_node_ids

The list of busbar node ids, to which the shortest path is calculated.

TYPE: list[int]

start_node_id

The node id from which the shortest path is calculated.

TYPE: int

weight

string or Callable If this is a string, then edge weights will be accessed via the edge attribute with this key (that is, the weight of the edge joining u to v will be G.edges[u, v][weight]). If no such edge attribute exists, the weight of the edge is assumed to be one.

If this is a function, the weight of an edge is the value returned by the function. The function must accept exactly three positional arguments: the two endpoints of an edge and the dictionary of edge attributes for that edge. The function must return a number or None to indicate a hidden edge.

TYPE: Union[str, Callable] DEFAULT: "station_weight"

cutoff

The cutoff value for the shortest path.

TYPE: int DEFAULT: int(high.value)

RETURNS DESCRIPTION
shortest_path_dict dict[int, list[int]]

The shortest path to the busbar. key: target_node_ids value: list of node_ids from the start_node_id to the key (the target_node_id) Note: not all target_node_ids are in the dict, only the ones that are reachable from the start_node_id. Reachable means that the path is shorter than the cutoff value.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph.py
def shortest_paths_to_target_ids(
    graph: nx.Graph,
    target_node_ids: list[int],
    start_node_id: int,
    weight: Union[str, Callable] = "station_weight",
    cutoff: int = int(WeightValues.high.value),
) -> dict[int, list[int]]:
    """Find the shortest paths from one busbar to a list of busbars in the NetworkX graph.

    Parameters
    ----------
    graph : nx.Graph
        The NetworkX graph.
    target_node_ids : list[int]
        The list of busbar node ids, to which the shortest path is calculated.
    start_node_id : int
        The node id from which the shortest path is calculated.
    weight : Union[str, Callable], default: "station_weight"
        string or Callable
        If this is a string, then edge weights will be accessed via the
        edge attribute with this key (that is, the weight of the edge
        joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
        such edge attribute exists, the weight of the edge is assumed to
        be one.

        If this is a function, the weight of an edge is the value
        returned by the function. The function must accept exactly three
        positional arguments: the two endpoints of an edge and the
        dictionary of edge attributes for that edge. The function must
        return a number or None to indicate a hidden edge.
    cutoff : int, optional
        The cutoff value for the shortest path.

    Returns
    -------
    shortest_path_dict dict[int, list[int]]
        The shortest path to the busbar.
        key: target_node_ids
        value: list of node_ids from the start_node_id to the key (the target_node_id)
        Note: not all target_node_ids are in the dict, only the ones that are reachable from the start_node_id.
              Reachable means that the path is shorter than the cutoff value.
    """
    shortest_path_dict = nx.single_source_dijkstra_path(graph, source=start_node_id, weight=weight, cutoff=cutoff)
    shortest_path_dict = {k: v for k, v in shortest_path_dict.items() if k in target_node_ids}
    return shortest_path_dict

set_all_weights #

set_all_weights(
    branches_df, switches_df, helper_branches_df
)

Set all weights for the edges DataFrame in the NetworkGraphData model.

All weights are set in place in the DataFrames. This function sets the following weights for the edges: - station_weight - bay_weight - trafo_weight - coupler_weight - busbar_weight - switch_open_weight

PARAMETER DESCRIPTION
branches_df

The BranchSchema DataFrame from the NetworkGraphData model. Note: The DataFrame is modified in place.

TYPE: BranchSchema

switches_df

The SwitchSchema DataFrame from the NetworkGraphData model. Note: The DataFrame is modified in place.

TYPE: SwitchSchema

helper_branches_df

The HelperBranchSchema DataFrame from the NetworkGraphData model. Note: The DataFrame is modified in place.

TYPE: HelperBranchSchema

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_weights.py
def set_all_weights(branches_df: BranchSchema, switches_df: SwitchSchema, helper_branches_df: HelperBranchSchema) -> None:
    """Set all weights for the edges DataFrame in the NetworkGraphData model.

    All weights are set in place in the DataFrames.
    This function sets the following weights for the edges:
    - station_weight
    - bay_weight
    - trafo_weight
    - coupler_weight
    - busbar_weight
    - switch_open_weight

    Parameters
    ----------
    branches_df : BranchSchema
        The BranchSchema DataFrame from the NetworkGraphData model.
        Note: The DataFrame is modified in place.
    switches_df : SwitchSchema
        The SwitchSchema DataFrame from the NetworkGraphData model.
        Note: The DataFrame is modified in place.
    helper_branches_df : HelperBranchSchema
        The HelperBranchSchema DataFrame from the NetworkGraphData model.
        Note: The DataFrame is modified in place.
    """
    set_station_weight(branches_df=branches_df, switches_df=switches_df, helper_branches_df=helper_branches_df)
    set_bay_weight(branches_df=branches_df, switches_df=switches_df, helper_branches_df=helper_branches_df)
    set_trafo_weight(branches_df=branches_df, switches_df=switches_df, helper_branches_df=helper_branches_df)
    set_coupler_weight(branches_df=branches_df, switches_df=switches_df, helper_branches_df=helper_branches_df)
    set_busbar_weight(branches_df=branches_df, switches_df=switches_df, helper_branches_df=helper_branches_df)
    set_switch_open_weight(branches_df=branches_df, switches_df=switches_df, helper_branches_df=helper_branches_df)

get_asset_bay #

get_asset_bay(
    switches_df,
    asset_grid_model_id,
    busbar_df,
    edge_connection_info,
)

Get the asset bay for a asset_grid_model_id.

PARAMETER DESCRIPTION
switches_df

Dataframe with all switches of the substation. expects NetworkGraphData.switches

TYPE: SwitchSchema

asset_grid_model_id

Asset grid model id for which the asset bays should be retrieved.

TYPE: str

busbar_df

Dataframe with all busbars of the substation. expects get_busbar_df

TYPE: DataFrame

edge_connection_info

Dictionary with all edge connections of the substation. expects network_graph.get_edge_connection_info()

TYPE: dict[str, EdgeConnectionInfo]

RETURNS DESCRIPTION
asset_bay

AssetBay of the specified asset. or None if no AssetBay is found.

TYPE: Union[AssetBay, None]

logs

List of logs that are created during the process.

TYPE: list[str]

RAISES DESCRIPTION
ValueError

If the switches found not match the number of switches in the asset bay.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
def get_asset_bay(
    switches_df: SwitchSchema,
    asset_grid_model_id: str,
    busbar_df: pd.DataFrame,
    edge_connection_info: dict[str, EdgeConnectionInfo],
) -> Union[AssetBay, None]:
    """Get the asset bay for a asset_grid_model_id.

    Parameters
    ----------
    switches_df: SwitchSchema
        Dataframe with all switches of the substation.
        expects NetworkGraphData.switches
    asset_grid_model_id: str
        Asset grid model id for which the asset bays should be retrieved.
    busbar_df: pd.DataFrame
        Dataframe with all busbars of the substation.
        expects get_busbar_df
    edge_connection_info: dict[str, EdgeConnectionInfo]
        Dictionary with all edge connections of the substation.
        expects network_graph.get_edge_connection_info()

    Returns
    -------
    asset_bay: Union[AssetBay, None]
        AssetBay of the specified asset.
        or None if no AssetBay is found.
    logs: list[str]
        List of logs that are created during the process.

    Raises
    ------
    ValueError
        If the switches found not match the number of switches in the asset bay.

    """
    # get the edge connection info for the asset bay
    asset_bays_df = get_asset_bay_df(
        switches_df=switches_df,
        asset_grid_model_id=asset_grid_model_id,
        busbar_df=busbar_df,
        edge_connection_info=edge_connection_info,
    )
    asset_bay_dict = {}

    sl_switch, sl_log, n_sl_sw_found = get_sl_switch(asset_bays_df)
    if sl_switch is not None:
        # no sl switch in the asset bay -> is allowed -> no error
        asset_bay_dict["sl_switch_grid_model_id"] = sl_switch

    asset_bay_dict["dv_switch_grid_model_id"], dv_logs, n_dv_sw_found = get_dv_switch(
        asset_bays_df=asset_bays_df, asset_grid_model_id=asset_grid_model_id
    )
    asset_bay_dict["sr_switch_grid_model_id"] = get_sr_switch(asset_bays_df=asset_bays_df)
    dv_sr_dict, dv_sr_logs = get_dv_sr_switch(asset_bays_df=asset_bays_df)
    asset_bay_dict["sr_switch_grid_model_id"] = {**asset_bay_dict["sr_switch_grid_model_id"], **dv_sr_dict}

    # check if the number of switches found match the number of switches in the asset bay
    switches_found = n_sl_sw_found + n_dv_sw_found + len(asset_bay_dict["sr_switch_grid_model_id"])
    if switches_found != len(asset_bays_df):
        raise ValueError(f"Expected {len(asset_bays_df)} switches, but got: {asset_bay_dict}")

    logs = []
    logs.extend(sl_log)
    logs.extend(dv_logs)
    logs.extend(dv_sr_logs)
    if len(asset_bay_dict["sr_switch_grid_model_id"]) == 0:
        logs.append(
            "Warning: There should be at least one sr switch but got 0,"
            f" AssetBay ignored for grid_model_id: {asset_grid_model_id}"
        )
        return None, logs
    return AssetBay(**asset_bay_dict), logs

get_busbar_df #

get_busbar_df(nodes_df, substation_id)

Get the busbar from the NetworkGraphData nodes dataframe.

PARAMETER DESCRIPTION
nodes_df

Dataframe with all nodes of the substation or whole network. expects NetworkGraphData.nodes

TYPE: NodeSchema

substation_id

Substation id for which the busbar should be retrieved.

TYPE: str

RETURNS DESCRIPTION
busbar_df

busbar_df of the specified substation.

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
def get_busbar_df(nodes_df: NodeSchema, substation_id: str) -> pd.DataFrame:
    """Get the busbar from the NetworkGraphData nodes dataframe.

    Parameters
    ----------
    nodes_df: NodeSchema
        Dataframe with all nodes of the substation or whole network.
        expects NetworkGraphData.nodes
    substation_id: str
        Substation id for which the busbar should be retrieved.

    Returns
    -------
    busbar_df: pd.DataFrame
        busbar_df of the specified substation.
    """
    busbar_df = nodes_df[(nodes_df["substation_id"] == substation_id) & (nodes_df["node_type"] == "busbar")].copy()

    busbar_df = (
        busbar_df.sort_values(by="grid_model_id")
        .reset_index()
        .rename(columns={"foreign_id": "name", "node_type": "type", "bus_id": "bus_branch_bus_id"})
    )
    busbar_df["int_id"] = busbar_df.index

    busbar_df = busbar_df[["grid_model_id", "type", "name", "int_id", "in_service", "bus_branch_bus_id"]]

    return busbar_df

get_coupler_df #

get_coupler_df(
    switches_df, busbar_df, substation_id, graph
)

Get the busbar couplers from the NetworkGraphData edges dataframe.

PARAMETER DESCRIPTION
switches_df

Dataframe with all switches of the substation. expects NetworkGraphData.switches

TYPE: SwitchSchema

busbar_df

Dataframe with all busbars of the substation. expects get_busbar_df

TYPE: DataFrame

substation_id

Substation id for which the busbar couplers should be retrieved. looks for the "substation_id", "coupler_type" and "asset_type" column in the edges dataframe.

TYPE: str

graph

The graph from the NetworkGraphData of the substation or whole network. Note: the default strategy functions must been executed before calling this function.

TYPE: Graph

RETURNS DESCRIPTION
coupler_df

coupler_df of the specified substation.

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
def get_coupler_df(switches_df: SwitchSchema, busbar_df: pd.DataFrame, substation_id: str, graph: nx.Graph) -> pd.DataFrame:
    """Get the busbar couplers from the NetworkGraphData edges dataframe.

    Parameters
    ----------
    switches_df: SwitchSchema
        Dataframe with all switches of the substation.
        expects NetworkGraphData.switches
    busbar_df: pd.DataFrame
        Dataframe with all busbars of the substation.
        expects get_busbar_df
    substation_id: str
        Substation id for which the busbar couplers should be retrieved.
        looks for the "substation_id", "coupler_type" and "asset_type" column in the edges dataframe.
    graph: nx.Graph
        The graph from the NetworkGraphData of the substation or whole network.
        Note: the default strategy functions must been executed before calling this function.

    Returns
    -------
    coupler_df: pd.DataFrame
        coupler_df of the specified substation.
    """
    edge_connection_dict_list = {
        graph.get_edge_data(row["from_node"], row["to_node"])["grid_model_id"]: graph.get_edge_data(
            row["from_node"], row["to_node"]
        )["edge_connection_info"].model_dump()
        for _, row in switches_df.iterrows()
    }
    edge_connection_df = pd.DataFrame.from_dict(edge_connection_dict_list, orient="index")

    switches_connected_to_busbars = switches_df.merge(
        edge_connection_df,
        left_on="grid_model_id",
        right_index=True,
        how="left",
    )
    coupler_df = switches_connected_to_busbars[switches_connected_to_busbars["coupler_type"] != ""]
    coupler_df["from_busbar_grid_model_id"] = ""
    coupler_df["to_busbar_grid_model_id"] = ""
    coupler_df["type"] = ""
    if coupler_df.empty:
        logger.warning(f"No couplers found in the substation {substation_id}. Please check Station.")
        return coupler_df
    busbar_out_of_service = busbar_df[~busbar_df["in_service"]]["grid_model_id"].to_list()
    for index, row in coupler_df.iterrows():
        grid_model_id = row["grid_model_id"]
        bay_df = switches_connected_to_busbars[switches_connected_to_busbars["bay_id"] == grid_model_id]
        coupler_df.loc[index, "from_busbar_grid_model_id"] = select_one_busbar_for_coupler_side(
            coupler_index=index,
            bay_df=bay_df,
            side="from",
            out_of_service_busbar_ids=busbar_out_of_service,
        )
        coupler_df.loc[index, "to_busbar_grid_model_id"] = select_one_busbar_for_coupler_side(
            coupler_index=index,
            bay_df=bay_df,
            side="to",
            out_of_service_busbar_ids=busbar_out_of_service,
            ignore_busbar_id=coupler_df.loc[index, "from_busbar_grid_model_id"],
        )
        coupler_df.loc[index, "type"] = row["asset_type"]
        bay_state = get_state_of_coupler_based_on_bay(
            coupler_index=index,
            bay_df=bay_df,
        )
        if bay_state:
            # coupler is open, if one side has all switches open
            coupler_df.loc[index, "open"] = True
            # coupler state is independent of the bay state, if bay sides have a closed switch
            # -> no else block

    coupler_df = coupler_df.sort_values(by="grid_model_id").reset_index().rename(columns={"foreign_id": "name"})
    # get the busbar id
    coupler_df = coupler_df.merge(
        busbar_df[["grid_model_id", "int_id"]],
        left_on="from_busbar_grid_model_id",
        right_on="grid_model_id",
        how="left",
        suffixes=("", "_from"),
    )
    coupler_df.rename(columns={"int_id": "busbar_from_id"}, inplace=True)
    coupler_df = coupler_df.merge(
        busbar_df[["grid_model_id", "int_id"]],
        left_on="to_busbar_grid_model_id",
        right_on="grid_model_id",
        how="left",
        suffixes=("", "_to"),
    )
    coupler_df.rename(columns={"int_id": "busbar_to_id"}, inplace=True)
    coupler_df = coupler_df[["grid_model_id", "type", "name", "in_service", "open", "busbar_from_id", "busbar_to_id"]]

    return coupler_df

get_station_connection_tables #

get_station_connection_tables(
    busbar_connection_info, busbar_df, switchable_assets_df
)

Get the switching table physically from the NetworkGraphData busbar_connection_info dict.

PARAMETER DESCRIPTION
busbar_connection_info

Dictionary with all busbar connections of the substation. expects NetworkGraphData.busbar_connection_info

TYPE: dict[str, BusbarConnectionInfo]

busbar_df

Dataframe with all busbars of the substation. expects get_busbar_df

TYPE: DataFrame

switchable_assets_df

Dataframe with all switchable assets of the substation. expects get_switchable_asset

TYPE: SwitchableAssetSchema

RETURNS DESCRIPTION
asset_connectivity

asset_connectivity of the specified substation. Holds the all possible layouts of the switching table, shape (n_bus n_asset). An entry is true if it is possible to connected an asset to the busbar.

TYPE: Bool[Array, ' n_bus n_asset']

asset_switching_table

Holds the switching of each asset to each busbar, shape (n_bus n_asset). An entry is true if the asset is connected to the busbar.

TYPE: Bool[Array, ' n_bus n_asset']

busbar_connectivity

Holds the all possible layouts of the switching table, shape (n_bus n_bus). An entry is true if it is possible to connected an busbar to the busbar.

TYPE: Bool[Array, " n_bus n_bus"]]

busbar_switching_table

Holds the switching of each busbar to each busbar, shape (n_bus n_bus). An entry is true if a busbar is connected to the busbar.

TYPE: Bool[Array, " n_bus n_bus"]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
def get_station_connection_tables(
    busbar_connection_info: dict[str, BusbarConnectionInfo],
    busbar_df: pd.DataFrame,
    switchable_assets_df: SwitchableAssetSchema,
) -> tuple[
    Bool[Array, " n_bus n_asset"], Bool[Array, " n_bus n_asset"], Bool[Array, " n_bus n_bus"], Bool[Array, " n_bus n_bus"]
]:
    """Get the switching table physically from the NetworkGraphData busbar_connection_info dict.

    Parameters
    ----------
    busbar_connection_info: dict[str, BusbarConnectionInfo]
        Dictionary with all busbar connections of the substation.
        expects NetworkGraphData.busbar_connection_info
    busbar_df: pd.DataFrame
        Dataframe with all busbars of the substation.
        expects get_busbar_df
    switchable_assets_df: pd.DataFrame
        Dataframe with all switchable assets of the substation.
        expects get_switchable_asset

    Returns
    -------
    asset_connectivity: Bool[Array, " n_bus n_asset"]
        asset_connectivity of the specified substation.
        Holds the all possible layouts of the switching table, shape (n_bus n_asset).
        An entry is true if it is possible to connected an asset to the busbar.
    asset_switching_table: Bool[Array, " n_bus n_asset"]
        Holds the switching of each asset to each busbar, shape (n_bus n_asset).
        An entry is true if the asset is connected to the busbar.
    busbar_connectivity: Bool[Array, " n_bus n_bus"]]
        Holds the all possible layouts of the switching table, shape (n_bus n_bus).
        An entry is true if it is possible to connected an busbar to the busbar.
    busbar_switching_table: Bool[Array, " n_bus n_bus"]]
        Holds the switching of each busbar to each busbar, shape (n_bus n_bus).
        An entry is true if a busbar is connected to the busbar.
    """
    n_bus = busbar_df.shape[0]
    n_asset = switchable_assets_df.shape[0]
    asset_connectivity = np.zeros((n_bus, n_asset), dtype=bool)
    busbar_connectivity = np.zeros((n_bus, n_bus), dtype=bool)
    asset_switching_table = np.zeros((n_bus, n_asset), dtype=bool)
    busbar_switching_table = np.zeros((n_bus, n_bus), dtype=bool)

    for _, row in busbar_df.iterrows():
        # get the asset ids that are connectable to the busbar, not respecting open switches
        asset_grid_model_ids = busbar_connection_info[row["grid_model_id"]].connectable_assets
        asset_ids = switchable_assets_df[switchable_assets_df["grid_model_id"].isin(asset_grid_model_ids)].index.to_list()
        asset_connectivity[row["int_id"], asset_ids] = True

        # get the asset ids that are connectable to the busbar, respecting open switches
        asset_grid_model_ids = busbar_connection_info[row["grid_model_id"]].zero_impedance_connected_assets
        asset_ids = switchable_assets_df[switchable_assets_df["grid_model_id"].isin(asset_grid_model_ids)].index.to_list()
        asset_switching_table[row["int_id"], asset_ids] = True

        # get the busbar ids that are connectable to the busbar, not respecting open switches
        connectable_busbar_grid_model_ids = busbar_connection_info[row["grid_model_id"]].connectable_busbars
        connectable_busbar_ids = busbar_df[
            busbar_df["grid_model_id"].isin(connectable_busbar_grid_model_ids)
        ].index.to_list()
        busbar_connectivity[row["int_id"], connectable_busbar_ids] = True

        # get the busbar ids that are connectable to the busbar, respecting open switches
        connectable_busbar_grid_model_ids = busbar_connection_info[row["grid_model_id"]].zero_impedance_connected_busbars
        connectable_busbar_ids = busbar_df[
            busbar_df["grid_model_id"].isin(connectable_busbar_grid_model_ids)
        ].index.to_list()
        busbar_switching_table[row["int_id"], connectable_busbar_ids] = True

    return asset_connectivity, asset_switching_table, busbar_connectivity, busbar_switching_table

get_switchable_asset #

get_switchable_asset(
    busbar_connection_info, node_assets_df, branches_df
)

Get the switchable assets from the NetworkGraphData busbar_connection_info dict.

PARAMETER DESCRIPTION
busbar_connection_info

Dictionary with all busbar connections of the substation. expects network_graph.get_busbar_connection_info()

TYPE: dict[str, BusbarConnectionInfo]

node_assets_df

Dataframe with all nodes of the substation. expects get_node_assets_df

TYPE: NodeAssetSchema

branches_df

Dataframe with all branches of the substation. expects get_branches_df

TYPE: BranchSchema

RETURNS DESCRIPTION
connected_asset_df

connected_asset_df of the specified substation.

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/graph_to_asset_topo.py
def get_switchable_asset(
    busbar_connection_info: dict[str, BusbarConnectionInfo], node_assets_df: NodeAssetSchema, branches_df: BranchSchema
) -> SwitchableAssetSchema:
    """Get the switchable assets from the NetworkGraphData busbar_connection_info dict.

    Parameters
    ----------
    busbar_connection_info: dict[str, BusbarConnectionInfo]
        Dictionary with all busbar connections of the substation.
        expects network_graph.get_busbar_connection_info()
    node_assets_df: NodeAssetSchema
        Dataframe with all nodes of the substation.
        expects get_node_assets_df
    branches_df: BranchSchema
        Dataframe with all branches of the substation.
        expects get_branches_df

    Returns
    -------
    connected_asset_df: pd.DataFrame
        connected_asset_df of the specified substation.
    """
    connected_assets_list = [asset for assets in busbar_connection_info.values() for asset in assets.connectable_assets]
    connected_asset_df = pd.DataFrame(connected_assets_list, columns=["grid_model_id"]).drop_duplicates()
    # merge node_assets_df
    # -> node_assets_df.columns get added ['grid_model_id', 'foreign_id', 'node', 'asset_type', 'in_service']
    connected_asset_df = connected_asset_df.merge(
        node_assets_df[["grid_model_id", "foreign_id", "asset_type", "in_service"]],
        left_on="grid_model_id",
        right_on="grid_model_id",
        how="left",
    )
    # merge branches_df -> branches_df.columns get added with suffix "_1"
    connected_asset_df = connected_asset_df.merge(
        branches_df[["grid_model_id", "foreign_id", "asset_type", "in_service"]],
        left_on="grid_model_id",
        right_on="grid_model_id",
        how="left",
        suffixes=("", "_1"),
    )
    # merge the values of the added columns
    connected_asset_df["asset_type"] = np.where(
        connected_asset_df["asset_type"].notna(), connected_asset_df["asset_type"], connected_asset_df["asset_type_1"]
    )
    connected_asset_df["foreign_id"] = np.where(
        connected_asset_df["foreign_id"].notna(), connected_asset_df["foreign_id"], connected_asset_df["foreign_id_1"]
    )
    connected_asset_df["in_service"] = np.where(
        connected_asset_df["in_service"].notna(), connected_asset_df["in_service"], connected_asset_df["in_service_1"]
    )
    # rename columns to match the AssetTopology
    connected_asset_df.rename(columns={"asset_type": "type", "foreign_id": "name"}, inplace=True)
    connected_asset_df = connected_asset_df[["grid_model_id", "name", "type", "in_service"]]
    # ensure the order of the assets
    connected_asset_df.sort_values(by="grid_model_id", inplace=True)
    connected_asset_df.reset_index(drop=True, inplace=True)
    connected_asset_df["in_service"] = connected_asset_df["in_service"].astype(bool)
    SwitchableAssetSchema.validate(connected_asset_df)
    return connected_asset_df

generate_graph #

generate_graph(network_graph_data)

Generate a NetworkX graph from the NetworkGraphData model.

PARAMETER DESCRIPTION
network_graph_data

The NetworkGraphData model. Needs to be filled e.g. using pandapower_network_to_graph or powsybl_station_to_graph

TYPE: NetworkGraphData

RETURNS DESCRIPTION
Graph

The NetworkX graph from the NetworkGraphData model.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph.py
def generate_graph(network_graph_data: NetworkGraphData) -> nx.Graph:
    """Generate a NetworkX graph from the NetworkGraphData model.

    Parameters
    ----------
    network_graph_data : NetworkGraphData
        The NetworkGraphData model.
        Needs to be filled e.g. using pandapower_network_to_graph or powsybl_station_to_graph

    Returns
    -------
    nx.Graph
        The NetworkX graph from the NetworkGraphData model.
    """
    graph = nx.Graph()

    # create nodes
    graph_creation_nodes_helper(nodes_df=network_graph_data.nodes, graph=graph)
    # add nodes_assets to the nodes
    if not network_graph_data.node_assets.empty:
        graph_creation_node_assets_helper(node_assets_df=network_graph_data.node_assets, graph=graph)
    # create edges
    if not network_graph_data.switches.empty:
        graph_creation_edge_helper(edge_df=network_graph_data.switches, graph=graph)
    if not network_graph_data.branches.empty:
        graph_creation_edge_helper(edge_df=network_graph_data.branches, graph=graph)
        update_node_dict = get_branch_node_asset_update_dict(branch_df=network_graph_data.branches)
        update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict, method="append")
    if not network_graph_data.helper_branches.empty:
        # IMPORTANT: the helper_branches need to be added last
        # -> existing branches and nodes are being ignored
        graph_creation_edge_helper(edge_df=network_graph_data.helper_branches, graph=graph)
    return graph

add_graph_specific_data #

add_graph_specific_data(network_graph_data)

Add graph specific data to the NetworkGraphData model.

This functions prepares the NetworkGraphData model for the network graph by setting general data. The network graph uses the node_tuple to identify nodes for switches and branches. The weight strategy is set for the branches and switches. The network graph data is validated.

PARAMETER DESCRIPTION
network_graph_data

The NetworkGraphData model to prepare for the network graph Note: The NetworkGraphData is modified in place.

TYPE: NetworkGraphData

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_data.py
def add_graph_specific_data(network_graph_data: NetworkGraphData) -> None:
    """Add graph specific data to the NetworkGraphData model.

    This functions prepares the NetworkGraphData model for the network graph by setting general data.
    The network graph uses the node_tuple to identify nodes for switches and branches.
    The weight strategy is set for the branches and switches.
    The network graph data is validated.

    Parameters
    ----------
    network_graph_data : NetworkGraphData
        The NetworkGraphData model to prepare for the network graph
        Note: The NetworkGraphData is modified in place.

    """
    add_node_tuple_column(network_graph_data)
    set_all_weights(network_graph_data.branches, network_graph_data.switches, network_graph_data.helper_branches)
    add_suffix_to_duplicated_grid_model_id(df=network_graph_data.node_assets)
    network_graph_data.validate_network_graph_data()

add_node_tuple_column #

add_node_tuple_column(network_graph_data)

Add node tuple to the edges of the NetworkGraphData model.

This function adds the node tuple to the NetworkGraphData model. The node tuple is used to identify the nodes for the switches and branches.

PARAMETER DESCRIPTION
network_graph_data

The NetworkGraphData model to add the node tuple. Note: The NetworkGraphData is modified in place.

TYPE: NetworkGraphData

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_data.py
def add_node_tuple_column(network_graph_data: NetworkGraphData) -> None:
    """Add node tuple to the edges of the NetworkGraphData model.

    This function adds the node tuple to the NetworkGraphData model.
    The node tuple is used to identify the nodes for the switches and branches.

    Parameters
    ----------
    network_graph_data : NetworkGraphData
        The NetworkGraphData model to add the node tuple.
        Note: The NetworkGraphData is modified in place.
    """
    network_graph_data.switches["node_tuple"] = list(
        map(
            lambda x: tuple(map(int, sorted(set(x)))),
            np.column_stack((network_graph_data.switches["from_node"], network_graph_data.switches["to_node"])),
        )
    )
    if not network_graph_data.branches.empty:
        network_graph_data.branches["node_tuple"] = list(
            map(
                lambda x: tuple(map(int, sorted(set(x)))),
                np.column_stack((network_graph_data.branches["from_node"], network_graph_data.branches["to_node"])),
            )
        )
    if not network_graph_data.helper_branches.empty:
        network_graph_data.helper_branches["node_tuple"] = list(
            map(
                lambda x: tuple(map(int, sorted(set(x)))),
                np.column_stack(
                    (network_graph_data.helper_branches["from_node"], network_graph_data.helper_branches["to_node"])
                ),
            )
        )

remove_helper_branches #

remove_helper_branches(
    nodes_df,
    helper_branches_df,
    node_assets_df,
    switches_df,
    branches_df,
)

Remove helper branches from the network.

Helper branches are used to connect nodes in the network, but are not part of the grid model. They are an artifact of the network model creation and it is optional to remove them. This function removes the helper branches and the helper nodes from the network. It also removes some nodes that are not specified as helper nodes, if they hold not additional information.

This function is mainly used to test the functionality with the helper branches vs without helper branches.

PARAMETER DESCRIPTION
nodes_df

The DataFrame containing the nodes. See the NodeSchema for more information. Note: The nodes_df is modified in place.

TYPE: NodeSchema

helper_branches_df

The DataFrame containing the helper branches. See the HelperBranchSchema for more information. Note: this DataFrame is obsolete after the function is called.

TYPE: HelperBranchSchema

node_assets_df

The DataFrame containing the node assets. See the NodeAssetsSchema for more information.

TYPE: NodeAssetSchema

switches_df

The DataFrame containing the switches. See the SwitchSchema for more information. Note: The switches_df is modified in place.

TYPE: SwitchSchema

branches_df

The DataFrame containing the branches. See the BranchSchema for more information. Note: The branches_df is modified in place.

TYPE: BranchSchema

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/network_graph_data.py
def remove_helper_branches(
    nodes_df: NodeSchema,
    helper_branches_df: HelperBranchSchema,
    node_assets_df: NodeAssetSchema,
    switches_df: SwitchSchema,
    branches_df: BranchSchema,
) -> None:
    """Remove helper branches from the network.

    Helper branches are used to connect nodes in the network, but are not part of the grid model.
    They are an artifact of the network model creation and it is optional to remove them.
    This function removes the helper branches and the helper nodes from the network.
    It also removes some nodes that are not specified as helper nodes, if they hold not additional information.

    This function is mainly used to test the functionality with the helper branches vs without helper branches.

    Parameters
    ----------
    nodes_df : NodeSchema
        The DataFrame containing the nodes. See the NodeSchema for more information.
        Note: The nodes_df is modified in place.
    helper_branches_df : HelperBranchSchema
        The DataFrame containing the helper branches. See the HelperBranchSchema for more information.
        Note: this DataFrame is obsolete after the function is called.
    node_assets_df : NodeAssetSchema
        The DataFrame containing the node assets. See the NodeAssetsSchema for more information.
    switches_df : SwitchSchema
        The DataFrame containing the switches. See the SwitchSchema for more information.
        Note: The switches_df is modified in place.
    branches_df : BranchSchema
        The DataFrame containing the branches. See the BranchSchema for more information.
        Note: The branches_df is modified in place.
    """
    helper_bus = nodes_df[nodes_df["helper_node"]]
    delete_node = []
    for helper_node in helper_bus.index.to_list():
        nodes_list = helper_branches_df[helper_branches_df["to_node"] == helper_node]["from_node"].to_list()
        nodes_list.extend(helper_branches_df[helper_branches_df["from_node"] == helper_node]["to_node"].to_list())
        grid_model_id = nodes_df.loc[nodes_list]
        grid_model_id_entry = grid_model_id[grid_model_id["grid_model_id"] != ""]
        connected_assets = node_assets_df[node_assets_df["node"].isin(nodes_df.loc[nodes_list].index)]
        if (len(connected_assets) == 1) and (len(grid_model_id_entry) == 0):
            final_node = connected_assets["node"].values[0]
            delete_node.append(helper_node)
        elif (len(connected_assets) == 0) and (len(grid_model_id_entry) == 1):
            final_node = grid_model_id_entry.index[0]
            delete_node.append(helper_node)
        elif (len(connected_assets) == 0) and (len(grid_model_id_entry) == 0):
            final_node = grid_model_id.index[0]
            delete_node.append(helper_node)
        else:
            raise ValueError(f"The helper node {helper_node} is not connected to only one node or asset.")
        switches_df.loc[switches_df["from_node"].isin(nodes_list), "from_node"] = final_node
        switches_df.loc[switches_df["to_node"].isin(nodes_list), "to_node"] = final_node
        branches_df.loc[branches_df["from_node"].isin(nodes_list), "from_node"] = final_node
        branches_df.loc[branches_df["to_node"].isin(nodes_list), "to_node"] = final_node
    nodes_df.drop(delete_node, inplace=True)
    assert len(nodes_df[nodes_df["helper_node"]]) == 0, "There are still helper nodes in the network"

get_network_graph #

get_network_graph(network_graph_data)

Get the network graph from the NetworkGraphData and run default filter.

PARAMETER DESCRIPTION
network_graph_data

The NetworkGraphData containing the nodes, switches, branches and node_assets.

TYPE: NetworkGraphData

RETURNS DESCRIPTION
Graph

The network graph.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/pandapower_network_to_graph.py
def get_network_graph(network_graph_data: NetworkGraphData) -> nx.Graph:
    """Get the network graph from the NetworkGraphData and run default filter.

    Parameters
    ----------
    network_graph_data : NetworkGraphData
        The NetworkGraphData containing the nodes, switches, branches and node_assets.

    Returns
    -------
    nx.Graph
        The network graph.
    """
    graph = generate_graph(network_graph_data)
    set_substation_id(graph=graph, network_graph_data=network_graph_data)
    run_default_filter_strategy(graph=graph)
    return graph

get_network_graph_data #

get_network_graph_data(net, only_relevant_col=True)

Get the network graph from the pandapower network.

PARAMETER DESCRIPTION
net

The pandapower network.

TYPE: pandapower

only_relevant_col

Whether to return only the relevant columns, by default True relevant is determined by the default BranchSchema columns

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
net_graph

The network graph in the format of the NetworkGraph class. Contains the full network with all substations.

TYPE: NetworkGraphData

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/pandapower_network_to_graph.py
def get_network_graph_data(net: pandapowerNet, only_relevant_col: bool = True) -> NetworkGraphData:
    """Get the network graph from the pandapower network.

    Parameters
    ----------
    net : pandapower
        The pandapower network.
    only_relevant_col : bool, optional
        Whether to return only the relevant columns, by default True
        relevant is determined by the default BranchSchema columns

    Returns
    -------
    net_graph : NetworkGraphData
        The network graph  in the format of the NetworkGraph class.
        Contains the full network with all substations.
    """
    branches_df = get_branch_df(net, only_relevant_col=only_relevant_col)
    nodes_df = get_nodes(net, only_relevant_col=only_relevant_col)
    switches_df = get_switches_df(net, only_relevant_col=only_relevant_col)
    logger.warning("generators are missing in the network graph - they are not implemented yet")
    network_graph_data = NetworkGraphData(nodes=nodes_df, switches=switches_df, branches=branches_df)
    add_graph_specific_data(network_graph_data)
    return network_graph_data

get_node_breaker_topology_graph #

get_node_breaker_topology_graph(network_graph_data)

Get the network graph from the NetworkGraphData and run default filter.

PARAMETER DESCRIPTION
network_graph_data

The NetworkGraphData containing the nodes, switches, branches and node_assets.

TYPE: NetworkGraphData

RETURNS DESCRIPTION
Graph

The network graph.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_node_breaker_topology_graph(network_graph_data: NetworkGraphData) -> nx.Graph:
    """Get the network graph from the NetworkGraphData and run default filter.

    Parameters
    ----------
    network_graph_data : NetworkGraphData
        The NetworkGraphData containing the nodes, switches, branches and node_assets.

    Returns
    -------
    nx.Graph
        The network graph.
    """
    graph = generate_graph(network_graph_data)
    run_default_filter_strategy(graph=graph)
    return graph

node_breaker_topology_to_graph_data #

node_breaker_topology_to_graph_data(net, substation_info)

Convert a node breaker topology to a NetworkGraph.

This function is WIP.

PARAMETER DESCRIPTION
net

The network to convert.

TYPE: Network

substation_info

The substation information to retrieve the node breaker topology.

TYPE: SubstationInformation

RETURNS DESCRIPTION
NetworkGraphData.
Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def node_breaker_topology_to_graph_data(net: Network, substation_info: SubstationInformation) -> NetworkGraphData:
    """Convert a node breaker topology to a NetworkGraph.

    This function is WIP.

    Parameters
    ----------
    net : Network
        The network to convert.
    substation_info : SubstationInformation
        The substation information to retrieve the node breaker topology.

    Returns
    -------
    NetworkGraphData.
    """
    all_names_df = get_all_element_names(net, line_trafo_name_col="name")
    nbt = net.get_node_breaker_topology(substation_info.voltage_level_id)

    switches_df = get_switches(switches_df=nbt.switches)
    busbar_sections_names_df = get_busbar_sections_with_in_service(network=net, attributes=["name", "in_service", "bus_id"])
    nodes_df = get_nodes(
        busbar_sections_names_df=busbar_sections_names_df,
        nodes_df=nbt.nodes,
        switches_df=switches_df,
        substation_info=substation_info,
    )
    helper_branches = get_helper_branches(internal_connections_df=nbt.internal_connections)
    node_assets_df = get_node_assets(nodes_df=nodes_df, all_names_df=all_names_df)

    graph_data = NetworkGraphData(
        nodes=nodes_df,
        switches=switches_df,
        helper_branches=helper_branches,
        node_assets=node_assets_df,
    )
    add_graph_specific_data(graph_data)
    return graph_data

toop_engine_importer.network_graph.default_filter_strategy #

The default filter strategy for the NetworkGraphData model.

The default strategy fills: - the BusbarConnectionInfo of all relevant nodes in the network graph. - the bay weights for asset nodes. - EdgeConnectionInfo

logger module-attribute #

logger = Logger(__name__)

run_default_filter_strategy #

run_default_filter_strategy(graph)

Run the default strategy on the nx.Graph created by the NetworkGraphData model.

This is the main function for the default strategy. The default strategy fills - BusbarConnectionInfo of all busbars in the network graph. - EdgeConnectionInfo of all edges in the network graph.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def run_default_filter_strategy(graph: nx.Graph) -> None:
    """Run the default strategy on the nx.Graph created by the NetworkGraphData model.

    This is the main function for the default strategy.
    The default strategy fills
    - BusbarConnectionInfo of all busbars in the network graph.
    - EdgeConnectionInfo of all edges in the network graph.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    set_switch_busbar_connection_info(graph=graph)
    set_bay_weights(graph=graph)
    set_empty_bay_weights(graph=graph)
    set_connectable_busbars(graph=graph)
    set_all_busbar_coupling_switches(graph=graph)
    set_zero_impedance_connected(graph=graph)

set_switch_busbar_connection_info #

set_switch_busbar_connection_info(graph)

Set the switch busbar connection information in the network graph model.

why: - cut off the shortest path at a busbar - direct_busbar_grid_model_id in combination with bay_id for switching table uses: - graph.nodes[node_id]["node_type"] == "busbar" sets: - set the direct_busbar_grid_model_id in the switches - set the busbar_weight to step for all edges around a busbar. This enables to cut off a shortest path at a busbar.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_switch_busbar_connection_info(graph: nx.Graph) -> None:
    """Set the switch busbar connection information in the network graph model.

    why:
        - cut off the shortest path at a busbar
        - direct_busbar_grid_model_id in combination with bay_id for switching table
    uses:
        - graph.nodes[node_id][`"node_type"`] == "busbar"
    sets:
        - set the direct_busbar_grid_model_id in the switches
        - set the busbar_weight to step for all edges around a busbar.
          This enables to cut off a shortest path at a busbar.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    busbars = get_node_list_by_attribute(graph=graph, attribute="node_type", value=["busbar"])
    min_len_path = 2  # since we are searching for an edge, we need two nodes
    update_edge_dict = {}
    for busbar_id in busbars:
        # helper_nodes = get_helper_node_ids(network_graph_data)
        helper_nodes = get_helper_node_ids(graph=graph)
        length, path = nx.single_source_dijkstra(
            graph, source=busbar_id, cutoff=WeightValues.step.value, weight="station_weight"
        )
        # sort out connections that span over multiple busbars
        # take only none helper nodes -> jump over to the next true node
        # save k: busbar v: a non helper node and none busbar
        station_switch_nodes = {
            k: v
            for k, v in path.items()
            if (k not in helper_nodes)
            and (len(v) >= min_len_path)
            and (v[-2] not in helper_nodes)
            and (length[k] == WeightValues.step.value)
        }
        # get only the shortest path -> if there is a helper branch at the end, it is filtered out here
        station_switch_nodes = {key: station_switch_nodes[key] for key in find_shortest_path_ids(station_switch_nodes)}
        busbar_grid_model_id = graph.nodes[busbar_id]["grid_model_id"]
        update_edge_dict.update(
            {(v[-2], v[-1]): {"direct_busbar_grid_model_id": busbar_grid_model_id} for v in station_switch_nodes.values()}
        )
        # set busbar_weight to graph
        update_dict = {(v[-2], v[-1]): {"busbar_weight": WeightValues.max_step.value} for v in station_switch_nodes.values()}
        nx.set_edge_attributes(graph, update_dict)
    update_edge_connection_info(graph=graph, update_edge_dict=update_edge_dict)

set_bay_weights #

set_bay_weights(graph)

Set the bay weights in the NetworkGraphData model for asset_nodes and branches.

The bay weight is used to categorize paths in the network and assign a bay to an asset. Enables DGS export and switching into topology using a branch_id, by linking the branch_id to a switch.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_bay_weights(graph: nx.Graph) -> dict[int, list[int]]:
    """Set the bay weights in the NetworkGraphData model for asset_nodes and branches.

    The bay weight is used to categorize paths in the network and assign a bay to an asset.
    Enables DGS export and switching into topology using a branch_id, by linking the branch_id to a switch.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    update_node_dict, bay_update_dict = get_asset_bay_update_dict(graph=graph)
    set_asset_bay_edge_attr(graph=graph, asset_bay_update_dict=bay_update_dict)
    update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict)

get_asset_bay_update_dict #

get_asset_bay_update_dict(graph)

Get the asset bay update dictionary for the nx.Graph (bases on NetworkGraphData model).

The asset bay update dictionary is used to categorize the asset nodes in the network graph.

PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

RETURNS DESCRIPTION
update_node_dict

A dictionary containing the update information for each node. keys: BusbarConnectionInfo attributes values: list of values to update

TYPE: dict

update_bay_dict

A dictionary containing the update information for each bay. keys: bay_id values: dict key: busbar_id value: list of node_ids from the asset_node

TYPE: dict

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def get_asset_bay_update_dict(
    graph: nx.Graph,
) -> tuple[dict[int, dict[str, list[Union[str, int]]]], dict[str, dict[int, list[int]]]]:
    """Get the asset bay update dictionary for the nx.Graph (bases on NetworkGraphData model).

    The asset bay update dictionary is used to categorize the asset nodes in the network graph.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.

    Returns
    -------
    update_node_dict : dict
        A dictionary containing the update information for each node.
        keys: BusbarConnectionInfo attributes
        values: list of values to update
    update_bay_dict : dict
        A dictionary containing the update information for each bay.
        keys: bay_id
        values: dict
            key: busbar_id
            value: list of node_ids from the asset_node
    """
    # get update dict for node asset
    node_ids_with_node_assets = get_nodes_ids_with_a_connected_asset(graph=graph)
    connectable_node_assets_to_busbar, update_bay_dict = get_asset_bay_node_asset_dict(
        graph=graph, node_ids_with_node_assets=node_ids_with_node_assets
    )
    connectable_assets_dict = get_connectable_assets_update_dict(connectable_node_assets_to_busbar, graph=graph)
    connectable_busbars_dict = get_connectable_busbars_update_dict(
        shortest_path=connectable_node_assets_to_busbar, graph=graph
    )
    update_node_dict = {**connectable_assets_dict, **connectable_busbars_dict}
    return update_node_dict, update_bay_dict

get_asset_bay_node_asset_dict #

get_asset_bay_node_asset_dict(
    graph, node_ids_with_node_assets
)

Get the asset bay node asset dictionary for the nx.Graph (based on NetworkGraphData model).

The asset bay node asset dictionary is used to categorize the asset nodes in the network graph.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

node_ids_with_node_assets

A list of node ids with a connected asset. This can be a node at the outer end of a Graph, e.g. a node where a line is incoming. Can also be a in the middle of a Graph, e.g. a BREAKER to be mapped.

TYPE: list[int]

RETURNS DESCRIPTION
connectable_asset_node_to_busbar

A dictionary containing the connectable busbars for each node. Key: node_id Value: list of connectable busbar ids

TYPE: dict[int, list[int]]

asset_bay_update_dict

A dictionary containing the update information for each bay. keys: bay_id values: dict key: busbar_id value: list of node_ids from the asset_node

TYPE: dict[str, dict[int, list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def get_asset_bay_node_asset_dict(
    graph: nx.Graph,
    node_ids_with_node_assets: list[int],
) -> tuple[dict[int, list[str]], dict[str, dict[int, list[int]]]]:
    """Get the asset bay node asset dictionary for the nx.Graph (based on NetworkGraphData model).

    The asset bay node asset dictionary is used to categorize the asset nodes in the network graph.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    node_ids_with_node_assets : list[int]
        A list of node ids with a connected asset.
        This can be a node at the outer end of a Graph, e.g. a node where a line is incoming.
        Can also be a in the middle of a Graph, e.g. a BREAKER to be mapped.

    Returns
    -------
    connectable_asset_node_to_busbar : dict[int, list[int]]
        A dictionary containing the connectable busbars for each node.
        Key: node_id
        Value: list of connectable busbar ids
    asset_bay_update_dict : dict[str, dict[int, list[int]]]
        A dictionary containing the update information for each bay.
        keys: bay_id
        values: dict
            key: busbar_id
            value: list of node_ids from the asset_node
    """
    busbars, busbars_helper_nodes = get_busbar_true_nodes(graph=graph)
    connectable_node_assets_to_busbar = {}
    asset_bay_update_dict = {}
    for node_assets_id in node_ids_with_node_assets:
        grid_model_ids = graph.nodes[node_assets_id]["busbar_connection_info"].node_assets
        bay_id = " + ".join(grid_model_ids)
        shortest_path_to_busbar_dict = calculate_asset_bay_for_node_assets(
            graph=graph, asset_node=node_assets_id, busbars_helper_nodes=busbars_helper_nodes
        )
        asset_bay_update_dict[bay_id] = shortest_path_to_busbar_dict
        connectable_node_assets_to_busbar[node_assets_id] = [
            find_matching_node_in_list(busbar_node_id, busbars_helper_nodes, busbars)
            for busbar_node_id in shortest_path_to_busbar_dict.keys()
        ]
    return connectable_node_assets_to_busbar, asset_bay_update_dict

get_connectable_busbars_update_dict #

get_connectable_busbars_update_dict(graph, shortest_path)

Get the node update dictionary for the BusbarConnectionInfo.

PARAMETER DESCRIPTION
graph

The network graph from the NetworkGraphData.

TYPE: Graph

shortest_path

A dictionary containing the shortest path to a busbar for each busbar. key: busbars_helper_nodes value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict

RETURNS DESCRIPTION
update_nodes

A dictionary containing the update information for each node. keys: BusbarConnectionInfo attributes values: list of values to update

TYPE: dict

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def get_connectable_busbars_update_dict(
    graph: nx.Graph, shortest_path: dict[int, list[int]]
) -> dict[int, dict[str, list[Union[str, int]]]]:
    """Get the node update dictionary for the BusbarConnectionInfo.

    Parameters
    ----------
    graph : nx.Graph
        The network graph from the NetworkGraphData.
    shortest_path : dict
        A dictionary containing the shortest path to a busbar for each busbar.
        key: busbars_helper_nodes
        value: list of node_ids from the asset_node the key (a busbars_helper_node)

    Returns
    -------
    update_nodes : dict
        A dictionary containing the update information for each node.
        keys: BusbarConnectionInfo attributes
        values: list of values to update
    """
    update_nodes = {}
    for node_id, connectable_list in shortest_path.items():
        grid_model_id_list = [graph.nodes[node]["grid_model_id"] for node in connectable_list]
        update_nodes[node_id] = {
            "connectable_busbars": grid_model_id_list,
            "connectable_busbars_node_ids": connectable_list,
        }
    return update_nodes

get_connectable_assets_update_dict #

get_connectable_assets_update_dict(
    connectable_node_assets_to_busbars, graph
)

Get the connectable assets update dictionary for the BusbarConnectionInfo.

Uses the reversed connectable_node_assets_to_busbars to update the connectable assets in the BusbarConnectionInfo.

PARAMETER DESCRIPTION
connectable_node_assets_to_busbars

A dictionary containing the connectable busbars for each asset node. key: node_id value: list of connectable busbar ids

TYPE: dict

graph

The network graph from the NetworkGraphData.

TYPE: Graph

RETURNS DESCRIPTION
update_nodes

A dictionary containing the update information for each node. keys: BusbarConnectionInfo attributes values: list of values to update

TYPE: dict

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def get_connectable_assets_update_dict(
    connectable_node_assets_to_busbars: dict[int, list[int]], graph: nx.Graph
) -> dict[int, dict[str, list[Union[str, int]]] :]:
    """Get the connectable assets update dictionary for the BusbarConnectionInfo.

    Uses the reversed connectable_node_assets_to_busbars to update the connectable assets in the BusbarConnectionInfo.

    Parameters
    ----------
    connectable_node_assets_to_busbars : dict
        A dictionary containing the connectable busbars for each asset node.
        key: node_id
        value: list of connectable busbar ids
    graph : nx.Graph
        The network graph from the NetworkGraphData.

    Returns
    -------
    update_nodes : dict
        A dictionary containing the update information for each node.
        keys: BusbarConnectionInfo attributes
        values: list of values to update
    """
    connectable_node_assets = reverse_dict_list(connectable_node_assets_to_busbars)
    update_nodes = {}
    for node_id, connectable_list in connectable_node_assets.items():
        grid_model_id_lists = [graph.nodes[node]["busbar_connection_info"].node_assets for node in connectable_list]
        grid_model_id_list = list(flatten_list_of_mixed_entries(grid_model_id_lists))

        update_nodes[node_id] = {"connectable_assets": grid_model_id_list, "connectable_assets_node_ids": connectable_list}
    return update_nodes

set_connectable_busbars #

set_connectable_busbars(graph)

Set the connectable busbars in BusbarConnectionInfo for each node in connectable_busbars.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_connectable_busbars(graph: nx.Graph) -> None:
    """Set the connectable busbars in BusbarConnectionInfo for each node in connectable_busbars.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    connectable_busbars, _busbar_shortest_path = calculate_connectable_busbars(graph=graph)
    update_node_dict = get_connectable_busbars_update_dict(shortest_path=connectable_busbars, graph=graph)
    update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict)

calculate_connectable_busbars #

calculate_connectable_busbars(graph)

Calculate the connectable busbars in the NetworkGraphData model.

The connectable busbars are the busbars that are reachable from a busbar.

PARAMETER DESCRIPTION
graph

The network graph from the NetworkGraphData.

TYPE: Graph

RETURNS DESCRIPTION
(busbar_interconnectable, busbar_shortest_path) : tuple[dict[int, list[int]], dict[int, dict[int, list[int]]]]

the first dictionary contains the connectable busbars for each node. the second dictionary contains the shortest path to a busbar for each busbar.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def calculate_connectable_busbars(graph: nx.Graph) -> tuple[dict[int, list[int]], dict[int, dict[int, list[int]]]]:
    """Calculate the connectable busbars in the NetworkGraphData model.

    The connectable busbars are the busbars that are reachable from a busbar.

    Parameters
    ----------
    graph : nx.Graph
        The network graph from the NetworkGraphData.

    Returns
    -------
    (busbar_interconnectable, busbar_shortest_path) : tuple[dict[int, list[int]], dict[int, dict[int, list[int]]]]
        the first dictionary contains the connectable busbars for each node.
        the second dictionary contains the shortest path to a busbar for each busbar.
    """
    busbars, _busbars_helper_nodes = get_busbar_true_nodes(graph=graph)
    busbar_interconnectable = {busbar_id: [] for busbar_id in busbars}
    busbar_shortest_path = {busbar_id: {} for busbar_id in busbars}
    weights_list = ["coupler_weight", "bay_weight"]
    for busbar in busbars:
        shortest_path_dict = shortest_paths_to_target_ids(
            graph=graph,
            target_node_ids=busbars,
            start_node_id=busbar,
            weight=multi_weight_function(weights_list),
            cutoff=WeightValues.max_coupler.value,
        )
        # sort out connections that span over multiple busbars
        # shortest path always contains the busbar itself + the target busbar -> 3 is not allowed
        shortest_path_dict = {k: v for k, v in shortest_path_dict.items() if sum(1 for x in v if x in busbars) == 2}
        busbar_interconnectable[busbar] = [busbar_id for busbar_id in shortest_path_dict.keys()]
        busbar_shortest_path[busbar] = {busbar_id: path for busbar_id, path in shortest_path_dict.items()}

    return busbar_interconnectable, busbar_shortest_path

calculate_zero_impedance_connected #

calculate_zero_impedance_connected(graph, busbar_id)

Calculate the zero impedance connected assets and busbars for a busbar_id.

The zero impedance connections, are e.g. connections where open switches are filtered out.

PARAMETER DESCRIPTION
graph

The network graph from the NetworkGraphData.

TYPE: Graph

busbar_id

The node id of the busbar.

TYPE: int

RETURNS DESCRIPTION
connected_assets_dict

A dictionary containing the connected assets for each node. key: node_id (a node asset id or a different busbar id than the input busbar_id) value: BusbarConnectionInfo.node_assets

TYPE: dict[int, list[str]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def calculate_zero_impedance_connected(graph: nx.Graph, busbar_id: int) -> dict[int, list[str]]:
    """Calculate the zero impedance connected assets and busbars for a busbar_id.

    The zero impedance connections, are e.g. connections where open switches are filtered out.

    Parameters
    ----------
    graph : nx.Graph
        The network graph from the NetworkGraphData.
    busbar_id : int
        The node id of the busbar.

    Returns
    -------
    connected_assets_dict : dict[int, list[str]]
        A dictionary containing the connected assets for each node.
        key: node_id (a node asset id or a different busbar id than the input busbar_id)
        value: BusbarConnectionInfo.node_assets
    """
    # We now filter for connections that have a zero impedance connection
    # recap of the filter weights:
    # switch_open_weight: is set to "low" for all switches, except for open switches
    # -> any path that contains an open switch is cut off
    # busbar_weight: is set to "max_step" for all edges around a busbar
    # -> to reach an asset we need at least the "max_step" to not cut off
    # -> to reach another busbar we need at least 2*max_step to not cut off
    # station_weight: is set to "step" for all edges around a substation
    # -> to reach an asset we need up to 5*"step to not cut off
    # -> to reach another busbar we need at least 2*max_step to not cut off + the steps for each edge
    # The maximum step we expect is 2.4*max_step = 2*max_step (busbar) + 4*step (coupler)
    # The value 2.9 is choses to indicate that we search everything below 3 max_step,
    # which refers to the hops we need due to the busbar_weight
    # To conclude: with 2.9 all connections are found to the connected busbars and assets
    # respecting the switch open state with no busbar in between.
    step_multiplier = 2.9

    weights_list = ["switch_open_weight", "busbar_weight", "station_weight"]
    connectable_assets_node_ids = list(
        flatten_list_of_mixed_entries(graph.nodes[busbar_id]["busbar_connection_info"].connectable_assets_node_ids)
    )
    target_node_ids = (
        connectable_assets_node_ids + graph.nodes[busbar_id]["busbar_connection_info"].connectable_busbars_node_ids
    )
    path = shortest_paths_to_target_ids(
        graph=graph,
        target_node_ids=target_node_ids,
        start_node_id=busbar_id,
        weight=multi_weight_function(weights_list),
        cutoff=step_multiplier * WeightValues.max_step.value,
    )
    path_ids = list(path.keys())
    connected_assets_dict = {node_id: graph.nodes[node_id]["busbar_connection_info"].node_assets for node_id in path_ids}
    return connected_assets_dict

set_zero_impedance_connected #

set_zero_impedance_connected(graph)

Set zero_impedance_connected in the graph model.

A shortest path to the connected assets respecting the switch open state is calculated.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/default_filter_strategy.py
def set_zero_impedance_connected(graph: nx.Graph) -> None:
    """Set zero_impedance_connected in the graph model.

    A shortest path to the connected assets respecting the switch open state is calculated.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified
    """
    busbar_ids = get_node_list_by_attribute(graph=graph, attribute="node_type", value=["busbar"])
    update_node_dict = {}
    for busbar_id in busbar_ids:
        connected_assets_dict = calculate_zero_impedance_connected(graph=graph, busbar_id=busbar_id)
        connected_assets = []
        connected_asset_node_ids = []
        for node_id, connected_asset_list in connected_assets_dict.items():
            if connected_asset_list != []:
                connected_assets += connected_asset_list
                connected_asset_node_ids.append(node_id)

        connected_busbar_node_ids = [node_id for node_id in connected_assets_dict.keys() if node_id in busbar_ids]
        connected_busbars = [graph.nodes[node_id]["grid_model_id"] for node_id in connected_busbar_node_ids]
        update_node_dict[busbar_id] = {
            "zero_impedance_connected_assets": connected_assets,
            "zero_impedance_connected_assets_node_ids": connected_asset_node_ids,
            "zero_impedance_connected_busbars": connected_busbars,
            "zero_impedance_connected_busbars_node_ids": connected_busbar_node_ids,
        }
    update_busbar_connection_info(graph=graph, update_node_dict=update_node_dict)

toop_engine_importer.network_graph.powsybl_station_to_graph #

Convert a pypowsybl network to a NetworkGraph.

logger module-attribute #

logger = Logger(__name__)

node_breaker_topology_to_graph_data #

node_breaker_topology_to_graph_data(net, substation_info)

Convert a node breaker topology to a NetworkGraph.

This function is WIP.

PARAMETER DESCRIPTION
net

The network to convert.

TYPE: Network

substation_info

The substation information to retrieve the node breaker topology.

TYPE: SubstationInformation

RETURNS DESCRIPTION
NetworkGraphData.
Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def node_breaker_topology_to_graph_data(net: Network, substation_info: SubstationInformation) -> NetworkGraphData:
    """Convert a node breaker topology to a NetworkGraph.

    This function is WIP.

    Parameters
    ----------
    net : Network
        The network to convert.
    substation_info : SubstationInformation
        The substation information to retrieve the node breaker topology.

    Returns
    -------
    NetworkGraphData.
    """
    all_names_df = get_all_element_names(net, line_trafo_name_col="name")
    nbt = net.get_node_breaker_topology(substation_info.voltage_level_id)

    switches_df = get_switches(switches_df=nbt.switches)
    busbar_sections_names_df = get_busbar_sections_with_in_service(network=net, attributes=["name", "in_service", "bus_id"])
    nodes_df = get_nodes(
        busbar_sections_names_df=busbar_sections_names_df,
        nodes_df=nbt.nodes,
        switches_df=switches_df,
        substation_info=substation_info,
    )
    helper_branches = get_helper_branches(internal_connections_df=nbt.internal_connections)
    node_assets_df = get_node_assets(nodes_df=nodes_df, all_names_df=all_names_df)

    graph_data = NetworkGraphData(
        nodes=nodes_df,
        switches=switches_df,
        helper_branches=helper_branches,
        node_assets=node_assets_df,
    )
    add_graph_specific_data(graph_data)
    return graph_data

get_node_breaker_topology_graph #

get_node_breaker_topology_graph(network_graph_data)

Get the network graph from the NetworkGraphData and run default filter.

PARAMETER DESCRIPTION
network_graph_data

The NetworkGraphData containing the nodes, switches, branches and node_assets.

TYPE: NetworkGraphData

RETURNS DESCRIPTION
Graph

The network graph.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_node_breaker_topology_graph(network_graph_data: NetworkGraphData) -> nx.Graph:
    """Get the network graph from the NetworkGraphData and run default filter.

    Parameters
    ----------
    network_graph_data : NetworkGraphData
        The NetworkGraphData containing the nodes, switches, branches and node_assets.

    Returns
    -------
    nx.Graph
        The network graph.
    """
    graph = generate_graph(network_graph_data)
    run_default_filter_strategy(graph=graph)
    return graph

get_switches #

get_switches(switches_df)

Get switches from a node breaker topology.

Get the switches from a node breaker topology, rename and retype for the NetworkGraph.

PARAMETER DESCRIPTION
switches_df

The switches DataFrame from the node NodeBreakerTopology.

TYPE: DataFrame

RETURNS DESCRIPTION
switches_df

The switches as a DataFrame, with renamed columns for the NetworkGraph.

TYPE: SwitchSchema

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_switches(switches_df: pd.DataFrame) -> SwitchSchema:
    """Get switches from a node breaker topology.

    Get the switches from a node breaker topology, rename and retype for the NetworkGraph.

    Parameters
    ----------
    switches_df : pd.DataFrame
        The switches DataFrame from the node NodeBreakerTopology.

    Returns
    -------
    switches_df : SwitchSchema
        The switches as a DataFrame, with renamed columns for the NetworkGraph.
    """
    switches_df.reset_index(inplace=True)
    switches_df.rename(
        columns={
            "id": "grid_model_id",
            "name": "foreign_id",
            "kind": "asset_type",
            "node1": "from_node",
            "node2": "to_node",
        },
        inplace=True,
    )
    switches_df.fillna({"foreign_id": ""}, inplace=True)
    cond = switches_df["foreign_id"] == ""
    switches_df.loc[cond, "foreign_id"] = switches_df.loc[cond, "grid_model_id"]
    switches_df["from_node"] = switches_df["from_node"].astype(int)
    switches_df["to_node"] = switches_df["to_node"].astype(int)
    # TODO: might need to be changed once there is more information about the in_service state
    switches_df["in_service"] = True
    return switches_df

get_nodes #

get_nodes(
    busbar_sections_names_df,
    nodes_df,
    switches_df,
    substation_info,
)

Get nodes from a node breaker topology.

Get the nodes from a node breaker topology, rename and retype for the NetworkGraph. Adds additional information to the nodes.

PARAMETER DESCRIPTION
busbar_sections_names_df

The busbar sections names. from get_busbar_sections_with_in_service(network=net, attributes=["name", "in_service"])

TYPE: DataFrame

nodes_df

The nodes DataFrame from the net.get_node_breaker_topology(voltage_level_id).nodes

TYPE: DataFrame

switches_df

The switches DataFrame from the node NodeBreakerTopology.

TYPE: DataFrame

substation_info

The substation information to add as node information.

TYPE: SubstationInformation

RETURNS DESCRIPTION
nodes_df

The nodes as a DataFrame, with renamed columns for the NetworkGraph.

TYPE: NodeSchema

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_nodes(
    busbar_sections_names_df: pd.DataFrame,
    nodes_df: pd.DataFrame,
    switches_df: pd.DataFrame,
    substation_info: SubstationInformation,
) -> NodeSchema:
    """Get nodes from a node breaker topology.

    Get the nodes from a node breaker topology, rename and retype for the NetworkGraph.
    Adds additional information to the nodes.

    Parameters
    ----------
    busbar_sections_names_df : pd.DataFrame
        The busbar sections names.
        from get_busbar_sections_with_in_service(network=net, attributes=["name", "in_service"])
    nodes_df : pd.DataFrame
        The nodes DataFrame from the net.get_node_breaker_topology(voltage_level_id).nodes
    switches_df : pd.DataFrame
        The switches DataFrame from the node NodeBreakerTopology.
    substation_info : SubstationInformation
        The substation information to add as node information.

    Returns
    -------
    nodes_df : NodeSchema
        The nodes as a DataFrame, with renamed columns for the NetworkGraph.
    """
    nodes_df = nodes_df.merge(busbar_sections_names_df, left_on="connectable_id", right_index=True, how="left")
    nodes_df["grid_model_id"] = ""
    nodes_df["node_type"] = "node"
    nodes_df["substation_id"] = substation_info.name
    nodes_df["system_operator"] = substation_info.region
    nodes_df["voltage_level"] = int(substation_info.nominal_v)
    nodes_df["helper_node"] = False

    nodes_df.rename(columns={"name": "foreign_id"}, inplace=True)
    nodes_df.index = nodes_df.index.astype(int)
    cond_busbar = nodes_df["connectable_type"] == "BUSBAR_SECTION"
    nodes_df.loc[cond_busbar, "node_type"] = "busbar"
    nodes_df.loc[cond_busbar, "grid_model_id"] = nodes_df.loc[cond_busbar, "connectable_id"]
    cond_helper_node = (nodes_df["connectable_type"] == "") & (
        ~nodes_df.index.isin(switches_df["from_node"].to_list() + switches_df["to_node"].to_list())
    )
    nodes_df.loc[cond_helper_node, "helper_node"] = True
    nodes_df.fillna({"foreign_id": ""}, inplace=True)
    cond = nodes_df["foreign_id"] == ""
    nodes_df.loc[cond, "foreign_id"] = nodes_df.loc[cond, "grid_model_id"]

    return NodeSchema.validate(nodes_df)

get_helper_branches #

get_helper_branches(internal_connections_df)

Get helper branches from a node breaker topology.

Get the helper branches from a node breaker topology, rename and retype for the NetworkGraph.

PARAMETER DESCRIPTION
internal_connections_df

The internal connections DataFrame from the node NodeBreakerTopology.

TYPE: DataFrame

RETURNS DESCRIPTION
helper_branches

The helper branches as a DataFrame, with renamed columns for the NetworkGraph.

TYPE: HelperBranchSchema

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_helper_branches(internal_connections_df: pd.DataFrame) -> HelperBranchSchema:
    """Get helper branches from a node breaker topology.

    Get the helper branches from a node breaker topology, rename and retype for the NetworkGraph.

    Parameters
    ----------
    internal_connections_df : pd.DataFrame
        The internal connections DataFrame from the node NodeBreakerTopology.

    Returns
    -------
    helper_branches : HelperBranchSchema
        The helper branches as a DataFrame, with renamed columns for the NetworkGraph.
    """
    helper_branches = internal_connections_df
    helper_branches.rename(columns={"node1": "from_node", "node2": "to_node"}, inplace=True)
    helper_branches["from_node"] = helper_branches["from_node"].astype(int)
    helper_branches["to_node"] = helper_branches["to_node"].astype(int)
    # helper branches have no grid model id, but are needed for consistency of edges
    helper_branches["grid_model_id"] = ""
    # all helper branches are in service
    helper_branches["in_service"] = True
    return helper_branches

get_node_assets #

get_node_assets(nodes_df, all_names_df)

Get node assets from a node breaker topology.

Get the node assets from a node breaker topology, rename and retype for the NetworkGraph.

PARAMETER DESCRIPTION
nodes_df

The nodes DataFrame from the node NodeBreakerTopology.

TYPE: DataFrame

all_names_df

The names of all elements in the network.

TYPE: DataFrame

RETURNS DESCRIPTION
node_assets_df

The node assets as a DataFrame, with renamed columns for the NetworkGraph

TYPE: NodeAssetSchema

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_node_assets(nodes_df: pd.DataFrame, all_names_df: pd.DataFrame) -> NodeAssetSchema:
    """Get node assets from a node breaker topology.

    Get the node assets from a node breaker topology, rename and retype for the NetworkGraph.

    Parameters
    ----------
    nodes_df : pd.DataFrame
        The nodes DataFrame from the node NodeBreakerTopology.
    all_names_df : pd.DataFrame
        The names of all elements in the network.

    Returns
    -------
    node_assets_df : NodeAssetSchema
        The node assets as a DataFrame, with renamed columns for the NetworkGraph
    """
    node_assets_df = nodes_df[(nodes_df["connectable_type"] != "") & (nodes_df["connectable_type"] != "BUSBAR_SECTION")]
    node_assets_df["grid_model_id"] = node_assets_df["connectable_id"]
    node_assets_df.reset_index(inplace=True, drop=False)
    node_assets_df["node"] = node_assets_df["node"].astype(int)
    node_assets_df.drop(columns=["foreign_id"], inplace=True)
    node_assets_df = node_assets_df.merge(all_names_df, how="left", left_on="grid_model_id", right_index=True)
    node_assets_df.rename(columns={"connectable_type": "asset_type", "name": "foreign_id"}, inplace=True)
    node_assets_df.fillna({"foreign_id": ""}, inplace=True)
    node_assets_df = node_assets_df[["grid_model_id", "foreign_id", "node", "asset_type"]]
    # TODO: might need to be changed once there is more information about the in_service state
    node_assets_df["in_service"] = True
    return node_assets_df

get_station #

get_station(network, bus_id, station_info)

Get the station from a pypowsybl network.

PARAMETER DESCRIPTION
network

The powsybl network.

TYPE: Network

bus_id

bus id of the station. (the substation grid_model_id)

TYPE: str

station_info

The substation information.

TYPE: SubstationInformation

RETURNS DESCRIPTION
station

The station as a AssetTopology.

TYPE: Station

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_station(network: Network, bus_id: str, station_info: SubstationInformation) -> Station:
    """Get the station from a pypowsybl network.

    Parameters
    ----------
    network : Network
        The powsybl network.
    bus_id : str
        bus id of the station.
        (the substation grid_model_id)
    station_info : SubstationInformation
        The substation information.

    Returns
    -------
    station : Station
        The station as a AssetTopology.
    """
    station_logs = []
    substation_id = station_info.name
    graph_data = node_breaker_topology_to_graph_data(network, substation_info=station_info)
    graph = get_node_breaker_topology_graph(graph_data)

    busbar_df = get_busbar_df(nodes_df=graph_data.nodes, substation_id=substation_id)
    coupler_df = get_coupler_df(
        switches_df=graph_data.switches, busbar_df=busbar_df, substation_id=substation_id, graph=graph
    )
    busbar_connection_info = get_busbar_connection_info(graph=graph)
    edge_connection_info = get_edge_connection_info(graph=graph)
    switchable_assets_df = get_switchable_asset(busbar_connection_info, graph_data.node_assets, graph_data.branches)
    asset_bay_dict = {}
    for asset_grid_model_id in switchable_assets_df["grid_model_id"].to_list():
        asset_bay, logs = get_asset_bay(
            graph_data.switches,
            asset_grid_model_id=asset_grid_model_id,
            busbar_df=busbar_df,
            edge_connection_info=edge_connection_info,
        )
        station_logs.extend(logs)
        if asset_bay is None:
            continue
        asset_bay_dict[asset_grid_model_id] = asset_bay

    asset_connectivity, asset_switching_table, busbar_connectivity, busbar_switching_table = get_station_connection_tables(
        busbar_connection_info, busbar_df=busbar_df, switchable_assets_df=switchable_assets_df
    )
    # remove connections that are at two busbars simultaneously
    asset_switching_table = remove_double_connections(asset_switching_table, substation_id=substation_id)
    busbars = get_list_of_busbars_from_df(busbar_df)
    couplers = get_list_of_coupler_from_df(coupler_df)
    assets = get_list_of_switchable_assets_from_df(station_branches=switchable_assets_df, asset_bay_dict=asset_bay_dict)
    remove_suffix_from_switchable_assets(assets)

    station = Station(
        grid_model_id=bus_id,
        name=substation_id,
        region=station_info.region,
        voltage_level=int(station_info.nominal_v),
        busbars=busbars,
        couplers=couplers,
        assets=assets,
        asset_switching_table=asset_switching_table,
        asset_connectivity=asset_connectivity,
        busbar_switching_table=busbar_switching_table,
        busbar_connectivity=busbar_connectivity,
        model_log=station_logs,
    )
    return station

get_station_list #

get_station_list(
    network, relevant_voltage_level_with_region
)

Get the station list from a pypowsybl network.

Note: include only wanted voltage levels and regions.

PARAMETER DESCRIPTION
network

The powsybl network.

TYPE: Network

relevant_voltage_level_with_region

DataFrame with the voltage level and region information.

TYPE: DataFrame

RETURNS DESCRIPTION
station_list

The station list as a AssetTopology.

TYPE: list[Station]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_station_list(network: Network, relevant_voltage_level_with_region: pd.DataFrame) -> list[Station]:
    """Get the station list from a pypowsybl network.

    Note: include only wanted voltage levels and regions.

    Parameters
    ----------
    network : Network
        The powsybl network.
    relevant_voltage_level_with_region : pd.DataFrame
        DataFrame with the voltage level and region information.

    Returns
    -------
    station_list : list[Station]
        The station list as a AssetTopology.
    """
    station_list = []
    for bus_id, row in relevant_voltage_level_with_region.iterrows():
        station_info = SubstationInformation(
            name=row["name"],
            region=row["region"],
            nominal_v=row["nominal_v"],
            voltage_level_id=row["voltage_level_id"],
        )
        try:
            station = get_station(network=network, bus_id=bus_id, station_info=station_info)
            station_list.append(station)
        except ValidationError as e:
            logger.warning(
                f"ValidationError while getting station: {station_info} with error: {e}. "
                "Consider checking the Station or adding to ignore list."
            )
        except KeyError as e:
            logger.warning(
                f"KeyError while getting station: {station_info} with error: {e}. "
                "Consider checking the Station or adding to ignore list. "
                "Likely a maintenance busbar present - Currently working."
            )
        except ValueError as e:
            logger.warning(
                f"ValueError while getting station: {station_info} with error: {e}. "
                "Consider checking the Station or adding to ignore list."
            )

    return station_list

get_relevant_voltage_levels #

get_relevant_voltage_levels(network, network_masks)

Get all relevant voltage level from the network.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

network_masks

NetworkMasks object with the relevant_subs mask

TYPE: NetworkMasks

RETURNS DESCRIPTION
relevant_voltage_level_with_region_and_bus_id

DataFrame with the relevant voltage level and region information with bus_id as index.

TYPE: DataFrame

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_relevant_voltage_levels(network: Network, network_masks: NetworkMasks) -> pd.DataFrame:
    """Get all relevant voltage level from the network.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    network_masks: NetworkMasks
        NetworkMasks object with the relevant_subs mask

    Returns
    -------
    relevant_voltage_level_with_region_and_bus_id: pd.DataFrame
        DataFrame with the relevant voltage level and region information with bus_id as index.
    """
    attributes = ["name", "substation_id", "nominal_v", "high_voltage_limit", "low_voltage_limit", "region", "topology_kind"]
    voltage_levels = get_voltage_level_with_region(network, attributes=attributes)
    busses = network.get_buses()
    relevant_voltage_levels = busses[network_masks.relevant_subs]["voltage_level_id"]
    relevant_voltage_level_with_region = voltage_levels[voltage_levels.index.isin(relevant_voltage_levels)]
    relevant_voltage_level_with_region_and_bus_id = relevant_voltage_level_with_region.merge(
        relevant_voltage_levels, left_index=True, right_on="voltage_level_id", how="left"
    )
    return relevant_voltage_level_with_region_and_bus_id

get_topology #

get_topology(network, network_masks, importer_parameters)

Get the pydantic topology model from the network.

PARAMETER DESCRIPTION
network

pypowsybl network object

TYPE: Network

network_masks

NetworkMasks object with the relevant voltage levels.

TYPE: NetworkMasks

importer_parameters

UCTE importer parameters

TYPE: CgmesImporterParameters

RETURNS DESCRIPTION
topology

Topology object, including all relevant stations

TYPE: Topology

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/powsybl_station_to_graph.py
def get_topology(network: Network, network_masks: NetworkMasks, importer_parameters: CgmesImporterParameters) -> Topology:
    """Get the pydantic topology model from the network.

    Parameters
    ----------
    network: Network
        pypowsybl network object
    network_masks: NetworkMasks
        NetworkMasks object with the relevant voltage levels.
    importer_parameters: UcteImporterParameters
        UCTE importer parameters

    Returns
    -------
    topology: Topology
        Topology object, including all relevant stations
    """
    relevant_voltage_level_with_region = get_relevant_voltage_levels(network=network, network_masks=network_masks)
    station_list = get_station_list(network=network, relevant_voltage_level_with_region=relevant_voltage_level_with_region)
    grid_model_file = str(importer_parameters.grid_model_file.name)
    topology_id = importer_parameters.grid_model_file.name
    timestamp = datetime.datetime.now()

    return Topology(
        topology_id=topology_id,
        grid_model_file=grid_model_file,
        stations=station_list,
        timestamp=timestamp,
    )

toop_engine_importer.network_graph.filter_strategy.empty_bay #

Empty Bay Filter Strategy.

This module contains functions to identify and handle empty bays in a network graph. An empty bay is defined as a node in the graph that has no connected assets, but has still a bay.

These empty bays need to be identified, so couplers can be found and categorized correctly. The main function is set_empty_bay_weights, which sets the bay weight for empty bays.

logger module-attribute #

logger = Logger(__name__)

set_empty_bay_weights #

set_empty_bay_weights(graph)

Set a bay weight for empty bays.

Due to data quality issues, some bays may be empty. Finding and categorizing couplers depends on all bays weights being set. This function sets a bay weight for empty bays.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/empty_bay.py
def set_empty_bay_weights(graph: nx.Graph) -> None:
    """Set a bay weight for empty bays.

    Due to data quality issues, some bays may be empty.
    Finding and categorizing couplers depends on all bays weights being set.
    This function sets a bay weight for empty bays.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    """
    empty_asset_bay_lists = get_empty_bay_list(graph=graph)
    update_dict = get_empty_bay_update_dict(empty_bay_lists=empty_asset_bay_lists)
    nx.set_edge_attributes(graph, update_dict)

get_empty_bay_list #

get_empty_bay_list(graph)

Get a list of empty bays.

PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

RETURNS DESCRIPTION
empty_asset_bay_lists

A list of empty bays. contains the node_ids of the empty bay path.

TYPE: list[list[int]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/empty_bay.py
def get_empty_bay_list(graph: nx.Graph) -> list[list[int]]:
    """Get a list of empty bays.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.

    Returns
    -------
    empty_asset_bay_lists : list[list[int]]
        A list of empty bays.
        contains the node_ids of the empty bay path.
    """
    busbars, busbars_helper_nodes = get_busbar_true_nodes(graph=graph)
    weight_list = ["busbar_weight", "bay_weight"]
    cutoff = WeightValues.max_step.value
    empty_asset_bay_lists = []
    for busbar_id in busbars_helper_nodes:
        station_node_paths = nx.single_source_dijkstra_path(
            graph, source=busbar_id, weight=multi_weight_function(weight_list), cutoff=cutoff
        )
        longest_path_ids = find_longest_path_ids(station_node_paths)
        # has only one neighbor -> dead end -> empty bay
        # dead end is not a busbar in the helper nodes system
        asset_bay_nodes = [node_id for node_id in longest_path_ids if len(graph[node_id]) == 1 and node_id not in busbars]
        empty_asset_bay_lists += [station_node_paths[asset_bay_node] for asset_bay_node in asset_bay_nodes]
    return empty_asset_bay_lists

get_empty_bay_update_dict #

get_empty_bay_update_dict(empty_bay_lists)

Get the empty bay update dictionary for the nx.Graph.

The empty bay update dictionary is used set the bay weight.

PARAMETER DESCRIPTION
empty_bay_lists

A list of empty bays. contains the node_ids of the empty bay path.

TYPE: list[list[int]]

RETURNS DESCRIPTION
update_edge_dict

A dictionary containing the update information for each edge. keys: edge_id (a tuple of node_ids) values: {"bay_weight": WeightValues.max_step.value}

TYPE: dict[tuple[int, int], dict[str, WeightValues]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/empty_bay.py
def get_empty_bay_update_dict(empty_bay_lists: list[list[int]]) -> dict[tuple[int, int], dict[str, WeightValues]]:
    """Get the empty bay update dictionary for the nx.Graph.

    The empty bay update dictionary is used set the bay weight.

    Parameters
    ----------
    empty_bay_lists : list[list[int]]
        A list of empty bays.
        contains the node_ids of the empty bay path.

    Returns
    -------
    update_edge_dict : dict[tuple[int,int], dict[str, WeightValues]]
        A dictionary containing the update information for each edge.
        keys: edge_id (a tuple of node_ids)
        values: {"bay_weight": WeightValues.max_step.value}

    """
    content = {"bay_weight": WeightValues.max_step.value}
    empty_bay_paired_tuples = [get_pair_tuples_from_list(empty_bay_list) for empty_bay_list in empty_bay_lists]
    empty_bay_paired_tuples = flatten_list_of_mixed_entries(empty_bay_paired_tuples)
    # remove duplicates
    empty_bay_paired_tuples = list(set(empty_bay_paired_tuples))
    update_edge_dict = {edge_id: content for edge_id in empty_bay_paired_tuples}
    return update_edge_dict

toop_engine_importer.network_graph.filter_strategy.switches #

Filter strategy for switches.

The Issue: There are many paths between two busbars, list of all known connection paths. DISCONNECTOR -> DS CIRCUIT BREAKER -> CB BUSBAR -> B

Busbar to Busbar (single path with no additional parallel busbars): - B -> DS -> B - B -> CB -> B - B -> DS -> CB -> B - B -> CB -> DS -> B - B -> DS -> CB -> DS -> B - B -> DS -> CB -> CB -> DS -> B - B -> DS -> CB -> DS -> CB -> DS -> B

More complex paths, e.g. with multiple busbars, all paths above can be extended with additional busbars: only few examples. Two busbars on either/both sides of a busbar: - B1 -> DS1 -> CB -> DS3 -> B3 ^ - B2 -> DS2 |

Three busbars on either/both sides of a busbar: - B1 -> DS1 -> CB -> DS3 -> B3 ^ - B2 -> DS2 | | - B4 -> DS4 |

Solution: 1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker) 2. Loop over BREAKER one by one 2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights) 2.2 Set coupler sides 2.3 Set bay_id 2.4 Set coupler type 3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B) 4. Repeat step 2. for DS 5. assert all bay_id are set for all DS and CB in the graph

logger module-attribute #

logger = Logger(__name__)

set_all_busbar_coupling_switches #

set_all_busbar_coupling_switches(graph)

Set all connection paths between busbars, be it BREAKER or DISCONNECTOR.

  1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker)
  2. Loop over BREAKER one by one 2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights) 2.2 Set coupler sides 2.3 After loop: Set bay_id 2.4 Set coupler type
  3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B)
  4. Repeat step 2. for DS
PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_all_busbar_coupling_switches(
    graph: nx.Graph,
) -> None:
    """Set all connection paths between busbars, be it BREAKER or DISCONNECTOR.

    1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker)
    2. Loop over BREAKER one by one
        2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights)
        2.2 Set coupler sides
        2.3 After loop: Set bay_id
        2.4 Set coupler type
    3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B)
    4. Repeat step 2. for DS

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
    """
    # 1. Get all BREAKER, which have no bay_id (e.g. excludes Line/Load breaker)
    no_bay_breaker_edges = get_switches_with_no_bay_id(graph=graph, asset_type="BREAKER")
    # 2. Loop over BREAKER one by one
    # 2.1 Loop: Get all shortest paths from BREAKER to all BUSBARs (respect weights)
    set_switch_bay_from_edge_ids(graph=graph, edge_ids=no_bay_breaker_edges)
    # 3. Get all DISCONNECTOR (to find left over connections like B -> DS -> B)
    no_bay_breaker_edges = get_switches_with_no_bay_id(graph=graph, asset_type="DISCONNECTOR")
    set_switch_bay_from_edge_ids(graph=graph, edge_ids=no_bay_breaker_edges)

set_switch_bay_from_edge_ids #

set_switch_bay_from_edge_ids(graph, edge_ids)

Set the bay for a switch.

Loops over the edge_ids and sets the bay for each edge_id. If there are multiple edges with the same bay_id, the first one is used. Note: if you provide all switches of one bay, the function will set the first edge as the bay_id, no matter the type of the edge. You may provide e.g. all BREAKER first, then all DISCONNECTOR with no bay_id. This way the bay id will be set for a BREAKER if there is one in the path.

PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

edge_ids

A list of edges to find the bay for. Note: only the first edge (regardless of type) in a path is used to set the bay_id.

TYPE: list[EDGE_ID]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_switch_bay_from_edge_ids(
    graph: nx.Graph,
    edge_ids: list[EDGE_ID],
) -> None:
    """Set the bay for a switch.

    Loops over the edge_ids and sets the bay for each edge_id.
    If there are multiple edges with the same bay_id, the first one is used.
    Note: if you provide all switches of one bay, the function will
    set the first edge as the bay_id, no matter the type of the edge.
    You may provide e.g. all BREAKER first, then all DISCONNECTOR with no bay_id.
    This way the bay id will be set for a BREAKER if there is one in the path.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
    edge_ids : list[EDGE_ID]
        A list of edges to find the bay for.
        Note: only the first edge (regardless of type) in a path is used to set the bay_id.
    """
    # 2. Loop over BREAKER or DISCONNECTOR one by one
    asset_bay_edge_id_update_dict = get_switch_bay_dict(graph=graph, switch_edge_list=edge_ids)
    # 2.2 Set coupler sides
    coupler_busbar_sides = get_busbar_sides_of_coupler(
        graph=graph,
        asset_bay_edge_id_update_dict=asset_bay_edge_id_update_dict,
    )
    set_coupler_busbar_sides(graph=graph, busbar_sides_of_coupler=coupler_busbar_sides)
    # 2.3 After loop: Set bay_id for whole path
    coupler_update, side1_update, side2_update = get_asset_bay_id_grid_model_update_dict(
        asset_bay_edge_id_update_dict=asset_bay_edge_id_update_dict
    )
    set_coupler_bay_ids(
        graph=graph,
        side1_update=side1_update,
        side2_update=side2_update,
    )
    set_bay_attr_for_coupler_paths(
        graph=graph,
        coupler_update=coupler_update,
        side1_update=side1_update,
        side2_update=side2_update,
    )
    # 2.4 Set coupler type
    set_coupler_type(graph=graph, coupler_sides=coupler_busbar_sides)

set_bay_attr_for_coupler_paths #

set_bay_attr_for_coupler_paths(
    graph, coupler_update, side1_update, side2_update
)

Set the bay attributes for the coupler paths.

This function sets the bay attributes for the coupler paths. The coupler paths are the paths between the busbars and the coupler.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

coupler_update

A dictionary containing the found busbars. key: bay_edge_id value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[EDGE_ID, dict[int, list[int]]]

side1_update

A dictionary containing the found busbars. key: bay_edge_id value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[EDGE_ID, dict[int, list[int]]]

side2_update

A dictionary containing the found busbars. key: bay_edge_id value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[EDGE_ID, dict[int, list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_bay_attr_for_coupler_paths(
    graph: nx.Graph,
    coupler_update: dict[EDGE_ID, dict[int, list[int]]],
    side1_update: dict[EDGE_ID, dict[int, list[int]]],
    side2_update: dict[EDGE_ID, dict[int, list[int]]],
) -> None:
    """Set the bay attributes for the coupler paths.

    This function sets the bay attributes for the coupler paths.
    The coupler paths are the paths between the busbars and the coupler.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    coupler_update : dict[EDGE_ID, dict[int, list[int]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    side1_update : dict[EDGE_ID, dict[int, list[int]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    side2_update : dict[EDGE_ID, dict[int, list[int]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    """
    # get the bay_id for the coupler paths
    coupler_grid_model_id_update = get_edge_attr_for_dict_key(
        graph=graph,
        input_dict=coupler_update,
        attribute="grid_model_id",
    )
    side1_grid_model_id_update = get_edge_attr_for_dict_key(
        graph=graph,
        input_dict=side1_update,
        attribute="grid_model_id",
    )
    side2_grid_model_id_update = get_edge_attr_for_dict_key(
        graph=graph,
        input_dict=side2_update,
        attribute="grid_model_id",
    )
    # set the bay_id for the coupler paths
    set_asset_bay_edge_attr(graph=graph, asset_bay_update_dict=coupler_grid_model_id_update)
    set_asset_bay_edge_attr(graph=graph, asset_bay_update_dict=side1_grid_model_id_update)
    set_asset_bay_edge_attr(graph=graph, asset_bay_update_dict=side2_grid_model_id_update)

set_coupler_bay_ids #

set_coupler_bay_ids(graph, side1_update, side2_update)

Set the from_coupler_ids and to_coupler_ids in the EdgeConnectionInfo of the coupler.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

side1_update

A dictionary containing the found busbars. key: bay_edge_id value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[EDGE_ID, dict[int, list[int]]]

side2_update

A dictionary containing the found busbars. key: bay_edge_id value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[EDGE_ID, dict[int, list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_coupler_bay_ids(
    graph: nx.Graph,
    side1_update: dict[EDGE_ID, dict[int, list[int]]],
    side2_update: dict[EDGE_ID, dict[int, list[int]]],
) -> None:
    """Set the from_coupler_ids and to_coupler_ids in the EdgeConnectionInfo of the coupler.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    side1_update : dict[EDGE_ID, dict[int, list[int]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    side2_update : dict[EDGE_ID, dict[int, list[int]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    """
    side1_bay_dict = get_coupler_bay_edge_ids(asset_bay_edge_id_update_dict=side1_update)
    side2_bay_dict = get_coupler_bay_edge_ids(asset_bay_edge_id_update_dict=side2_update)
    side_1_grid_model_ids = get_edge_attr_for_dict_list(
        graph=graph,
        input_dict=side1_bay_dict,
        attribute="grid_model_id",
    )
    side_2_grid_model_ids = get_edge_attr_for_dict_list(
        graph=graph,
        input_dict=side2_bay_dict,
        attribute="grid_model_id",
    )
    side1_update_dict = {}
    side2_update_dict = {}
    for edge_id, grid_model_ids in side_1_grid_model_ids.items():
        side1_update_dict[edge_id] = {"from_coupler_ids": grid_model_ids}
    for edge_id, grid_model_ids in side_2_grid_model_ids.items():
        side2_update_dict[edge_id] = {"to_coupler_ids": grid_model_ids}
    update_edge_connection_info(graph, side1_update_dict)
    update_edge_connection_info(graph, side2_update_dict)

get_coupler_bay_edge_ids #

get_coupler_bay_edge_ids(asset_bay_edge_id_update_dict)

Get the coupler bay ids.

PARAMETER DESCRIPTION
asset_bay_edge_id_update_dict

A dictionary containing the found busbars. key: coupler_edge_id value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[tuple[int, int], dict[int, list[int]]]

RETURNS DESCRIPTION
coupler_bay_ids

A dictionary containing the coupler bay ids. key: coupler_edge_id value: list of edge_ids (a tuple of node_ids) that are part of the coupler leading to the busbar

TYPE: dict[tuple[int, int], list[tuple[int, int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def get_coupler_bay_edge_ids(
    asset_bay_edge_id_update_dict: dict[EDGE_ID, dict[int, list[int]]],
) -> dict[EDGE_ID, list[EDGE_ID]]:
    """Get the coupler bay ids.

    Parameters
    ----------
    asset_bay_edge_id_update_dict : dict[tuple[int,int], dict[int, list[int]]]
        A dictionary containing the found busbars.
        key: coupler_edge_id
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)

    Returns
    -------
    coupler_bay_ids : dict[tuple[int,int], list[tuple[int,int]]]
        A dictionary containing the coupler bay ids.
        key: coupler_edge_id
        value: list of edge_ids (a tuple of node_ids) that are part of the coupler
               leading to the busbar
    """
    bay_dict = {}
    for edge_id, shortest_path_to_busbar_dict in asset_bay_edge_id_update_dict.items():
        bay_list = []
        for path in shortest_path_to_busbar_dict.values():
            bay_list += [(from_id, to_id) for from_id, to_id in pairwise(path)]
        bay_dict[edge_id] = bay_list

    return bay_dict

set_coupler_type #

set_coupler_type(graph, coupler_sides)

Set the coupler type in the nx.Graph (based on NetworkGraphData model).

Warning: this assumes that all assets of horizontal connections are connected have an bay to all busbars.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

coupler_sides

A dictionary containing the sides of the coupler. key: edge_id (a tuple of node_ids) value: tuple of two lists of busbar ids which side is from and which side is to is not defined. The order of the busbar ids is not important.

TYPE: dict[EDGE_ID, tuple[list[int], list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_coupler_type(
    graph: nx.Graph,
    coupler_sides: dict[EDGE_ID, tuple[list[int], list[int]]],
) -> None:
    """Set the coupler type in the nx.Graph (based on NetworkGraphData model).

    Warning: this assumes that all assets of horizontal connections are connected have an bay to all busbars.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    coupler_sides : dict[EDGE_ID, tuple[list[int], list[int]]]
        A dictionary containing the sides of the coupler.
        key: edge_id (a tuple of node_ids)
        value: tuple of two lists of busbar ids
        which side is from and which side is to is not defined.
        The order of the busbar ids is not important.
    """
    connectable_assets_dict = get_busbar_connection_info_attribute(graph, "connectable_assets", node_type="busbar")
    coupler_categories = get_coupler_type(connectable_assets=connectable_assets_dict, coupler_sides=coupler_sides)
    set_coupler_type_graph(graph=graph, coupler_categories=coupler_categories)

get_coupler_type #

get_coupler_type(connectable_assets, coupler_sides)

Categorize the coupler in the NetworkGraphData model.

A logic to categorize the coupler into busbar coupler and cross coupler. Uses the BusbarConnectionInfo of each node to find the coupler.

Note: this function is independent of any coupler information. Match the results with the couplers in the graph.

PARAMETER DESCRIPTION
connectable_assets

A dictionary containing the connectable assets. Key: busbar_id Value: list of connectable asset ids

TYPE: dict[int, list[Union[str, int]]]

coupler_sides

A dictionary containing the sides of the coupler. key: edge_id (a tuple of node_ids) value: tuple of two lists of busbar ids which side is from and which side is to is not defined. The order of the busbar ids is not important.

TYPE: dict[EDGE_ID, tuple[list[int], list[int]]]

RETURNS DESCRIPTION
coupler_categories

A dictionary containing the coupler_categories. Key: busbar_coupler or cross_coupler Value: list of busbar id tuples

TYPE: dict[str, list[EDGE_ID]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def get_coupler_type(
    connectable_assets: dict[int, list[Union[str, int]]],
    coupler_sides: dict[EDGE_ID, tuple[list[int], list[int]]],
) -> dict[str, list[EDGE_ID]]:
    """Categorize the coupler in the NetworkGraphData model.

    A logic to categorize the coupler into busbar coupler and cross coupler.
    Uses the BusbarConnectionInfo of each node to find the coupler.

    Note: this function is independent of any coupler information.
    Match the results with the couplers in the graph.

    Parameters
    ----------
    connectable_assets : dict[int, list[Union[str, int]]]
        A dictionary containing the connectable assets.
        Key: busbar_id
        Value: list of connectable asset ids
    coupler_sides : dict[EDGE_ID, tuple[list[int], list[int]]]
        A dictionary containing the sides of the coupler.
        key: edge_id (a tuple of node_ids)
        value: tuple of two lists of busbar ids
        which side is from and which side is to is not defined.
        The order of the busbar ids is not important.

    Returns
    -------
    coupler_categories: dict[str, list[EDGE_ID]]
        A dictionary containing the coupler_categories.
        Key: busbar_coupler or cross_coupler
        Value: list of busbar id tuples
    """
    coupler_categories = {"busbar_coupler": [], "cross_coupler": []}
    # get A set of busbar ids for each coupler
    busbar_coupler_tuple = {
        coupler: (busbar_side1[0], busbar_side2[0]) for coupler, (busbar_side1, busbar_side2) in coupler_sides.items()
    }

    for coupler, (busbar1, busbar2) in busbar_coupler_tuple.items():
        if busbar_coupler_condition(busbar1=busbar1, busbar2=busbar2, connectable_assets=connectable_assets):
            coupler_categories["busbar_coupler"].append(coupler)
        else:
            coupler_categories["cross_coupler"].append(coupler)
    return coupler_categories

busbar_coupler_condition #

busbar_coupler_condition(
    busbar1, busbar2, connectable_assets, threshold=0.5
)

Check if a busbar is a busbar coupler.

A busbar is a busbar coupler if a percentage of connected assets are connected to all other busbars.

PARAMETER DESCRIPTION
busbar1

The busbar1 id.

TYPE: int

busbar2

The busbar2 id.

TYPE: int

connectable_assets

A dictionary containing the connectable assets. Key: busbar_id Value: list of connectable asset ids

TYPE: dict[int, list[str]]

threshold

The threshold of connected assets to all other busbars. The percentage above the threshold is considered a busbar coupler. example: connectable_assets[busbar] = [1,2,3,4] connectable_assets[connected_bus] = [1,2,3] threshold = 0.5 -> busbar is still a busbar coupler, even if only 3 out of 4 assets are connected to both busbars.

TYPE: float DEFAULT: 0.5

RETURNS DESCRIPTION
bool

True if the busbar is a busbar coupler.

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def busbar_coupler_condition(
    busbar1: int, busbar2: int, connectable_assets: dict[int, list[str]], threshold: float = 0.5
) -> bool:
    """Check if a busbar is a busbar coupler.

    A busbar is a busbar coupler if a percentage of connected assets are connected to all other busbars.

    Parameters
    ----------
    busbar1 : int
        The busbar1 id.
    busbar2 : int
        The busbar2 id.
    connectable_assets : dict[int, list[str]]
        A dictionary containing the connectable assets.
        Key: busbar_id
        Value: list of connectable asset ids
    threshold : float
        The threshold of connected assets to all other busbars.
        The percentage above the threshold is considered a busbar coupler.
        example:
            connectable_assets[busbar] = [1,2,3,4]
            connectable_assets[connected_bus] = [1,2,3]
            threshold = 0.5
            -> busbar is still a busbar coupler,
            even if only 3 out of 4 assets are connected to both busbars.

    Returns
    -------
    bool
        True if the busbar is a busbar coupler.
    """
    n_connected_elements = sum(elem in connectable_assets[busbar2] for elem in connectable_assets[busbar1])
    if len(connectable_assets[busbar1]) >= len(connectable_assets[busbar2]):
        n_total_elements = len(connectable_assets[busbar1])
    else:
        n_total_elements = len(connectable_assets[busbar2])
    if n_total_elements > 0 and n_connected_elements / n_total_elements >= threshold:
        return True
    return False

set_coupler_type_graph #

set_coupler_type_graph(graph, coupler_categories)

Set the "coupler_type" in the NetworkX Graph EdgeConnectionInfo.

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

coupler_categories

A dictionary containing the coupler_categories. Key: busbar_coupler or cross_coupler Value: list of busbar id tuples

TYPE: dict[str, list[EDGE_ID]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_coupler_type_graph(
    graph: nx.Graph,
    coupler_categories: dict[str, list[EDGE_ID]],
) -> None:
    """Set the "coupler_type" in the NetworkX Graph EdgeConnectionInfo.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    coupler_categories : dict[str, list[EDGE_ID]]
        A dictionary containing the coupler_categories.
        Key: busbar_coupler or cross_coupler
        Value: list of busbar id tuples
    """
    update_edge = reverse_dict_list(dict_of_lists=coupler_categories)
    update_edge_dict = {}
    for edge, type_content in update_edge.items():
        # check if the edge is already in the update_edge_dict
        update_edge_dict[edge] = {"coupler_type": type_content[0]}
    update_edge_connection_info(graph=graph, update_edge_dict=update_edge_dict)

get_switches_with_no_bay_id #

get_switches_with_no_bay_id(graph, asset_type)

Get all switches with no bay_id.

PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

asset_type

The asset type to find. Can be either "BREAKER" or "DISCONNECTOR".

TYPE: Literal['BREAKER', 'DISCONNECTOR']

RETURNS DESCRIPTION
switches

A list of all switches with no bay_id.

TYPE: list[EDGE_ID]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def get_switches_with_no_bay_id(graph: nx.Graph, asset_type: Literal["BREAKER", "DISCONNECTOR"]) -> list[EDGE_ID]:
    """Get all switches with no bay_id.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
    asset_type : Literal["BREAKER", "DISCONNECTOR"]
        The asset type to find.
        Can be either "BREAKER" or "DISCONNECTOR".

    Returns
    -------
    switches : list[EDGE_ID]
        A list of all switches with no bay_id.
    """
    breaker_switches_tuple = get_edge_list_by_attribute(graph, attribute="asset_type", value=[asset_type])
    no_bay_edges = get_edge_list_by_attribute(graph, attribute="bay_weight", value=[0.0])
    no_bay_breaker_edges = list(set(breaker_switches_tuple) & set(no_bay_edges))
    return no_bay_breaker_edges

get_switch_bay_dict #

get_switch_bay_dict(graph, switch_edge_list)

Get the bay for a switch.

Note: if there are two BREAKER in one connection path, this function will return only the first one.

Examples:

  • B -> DS -> CB1 -> CB2 -> DS -> B
  • B -> DS -> CB1 -> DS -> CB2 -> DS -> B Assuming CB1 is the first in the list, the function will return only CB1.
PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

switch_edge_list

A list of edges to find the bay for. This can be a list of edges with a BREAKER or DISCONNECTOR. The edges are used to find the bay for the asset.

TYPE: list[EDGE_ID]

RETURNS DESCRIPTION
asset_bay_update_dict

A dictionary containing the found busbars. key: bay_edge_id value: tuple of dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[tuple[int, int], tuple[dict[int, list[int]], dict[int, list[int]]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def get_switch_bay_dict(
    graph: nx.Graph,
    switch_edge_list: list[EDGE_ID],
) -> dict[EDGE_ID, tuple[dict[int, list[int]], dict[int, list[int]]]]:
    """Get the bay for a switch.

    Note: if there are two BREAKER in one connection path, this function will return
    only the first one.

    Examples
    --------
    - B -> DS -> CB1 -> CB2 -> DS -> B
    - B -> DS -> CB1 -> DS -> CB2 -> DS -> B
    Assuming CB1 is the first in the list, the function will return only CB1.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
    switch_edge_list : list[EDGE_ID]
        A list of edges to find the bay for.
        This can be a list of edges with a BREAKER or DISCONNECTOR.
        The edges are used to find the bay for the asset.

    Returns
    -------
    asset_bay_update_dict : dict[tuple[int,int], tuple[dict[int, list[int]], dict[int, list[int]]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: tuple of dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    """
    _busbars, busbars_helper_nodes = get_busbar_true_nodes(graph=graph)
    asset_bay_edge_id_update_dict = {}

    for edge_id in switch_edge_list:
        # set the bay weight for the edge_id, ensures that only one side is found
        set_single_bay_weight(graph=graph, edge_id=edge_id, bay_weight=WeightValues.over_step.value)

        if edge_id[0] in busbars_helper_nodes:
            # the edge is directly connected to the busbar
            shortest_path_to_busbar_side1_dict = {edge_id[0]: [edge_id[0]]}
        else:
            shortest_path_to_busbar_side1_dict = calculate_asset_bay_for_node_assets(
                graph=graph, busbars_helper_nodes=busbars_helper_nodes, asset_node=edge_id[0]
            )
        if edge_id[1] in busbars_helper_nodes:
            # the edge is directly connected to the busbar
            shortest_path_to_busbar_side2_dict = {edge_id[1]: [edge_id[1]]}
        else:
            shortest_path_to_busbar_side2_dict = calculate_asset_bay_for_node_assets(
                graph=graph, busbars_helper_nodes=busbars_helper_nodes, asset_node=edge_id[1]
            )
        # sort out paths with multiple busbars
        shortest_path_to_busbar_side1_dict = remove_path_multiple_busbars(
            path_dict=shortest_path_to_busbar_side1_dict, busbars=busbars_helper_nodes
        )
        shortest_path_to_busbar_side2_dict = remove_path_multiple_busbars(
            path_dict=shortest_path_to_busbar_side2_dict, busbars=busbars_helper_nodes
        )

        if len(shortest_path_to_busbar_side1_dict) == 0 or len(shortest_path_to_busbar_side2_dict) == 0:
            # switch is part of a previous found path
            # -> skip
            set_single_bay_weight(graph=graph, edge_id=edge_id, bay_weight=WeightValues.low.value)
        else:
            # current path is not a sub path of a previous found path
            # -> save path
            asset_bay_edge_id_update_dict[edge_id] = (shortest_path_to_busbar_side1_dict, shortest_path_to_busbar_side2_dict)
    return asset_bay_edge_id_update_dict

get_busbar_sides_of_coupler #

get_busbar_sides_of_coupler(
    graph, asset_bay_edge_id_update_dict
)

Get the sides of the coupler.

A coupler has two sides, the from and to side. Both sides can connect to multiple busbars. This function gets the busbar ids for the busbar_helper_node ids.

PARAMETER DESCRIPTION
graph

The network graph.

TYPE: Graph

asset_bay_edge_id_update_dict

A dictionary containing the found busbars. key: EDGE_ID (a tuple of node_ids) value: list of busbar_helper_node ids.

TYPE: dict[EDGE_ID, tuple[dict[int, list[int]], dict[int, list[int]]]]

RETURNS DESCRIPTION
sides_of_coupler

A dictionary containing the sides of the coupler. key: edge_id (a tuple of node_ids) value: tuple of two lists of busbar ids Which side is from and which side is to is not defined. The order of the tuple is defined by the edge_id order. Note: the busbar_helper_node are transformed to busbar ids.

TYPE: dict[EDGE_ID, tuple[list[int], list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def get_busbar_sides_of_coupler(
    graph: nx.Graph,
    asset_bay_edge_id_update_dict: dict[EDGE_ID, tuple[dict[int, list[int]], dict[int, list[int]]]],
) -> dict[EDGE_ID, tuple[list[int], list[int]]]:
    """Get the sides of the coupler.

    A coupler has two sides, the from and to side. Both sides can connect to
    multiple busbars.
    This function gets the busbar ids for the busbar_helper_node ids.

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
    asset_bay_edge_id_update_dict : dict[EDGE_ID, tuple[dict[int, list[int]], dict[int, list[int]]]]
        A dictionary containing the found busbars.
        key: EDGE_ID (a tuple of node_ids)
        value: list of busbar_helper_node ids.

    Returns
    -------
    sides_of_coupler : dict[EDGE_ID, tuple[list[int], list[int]]]
        A dictionary containing the sides of the coupler.
        key: edge_id (a tuple of node_ids)
        value: tuple of two lists of busbar ids
        Which side is from and which side is to is not defined.
        The order of the tuple is defined by the edge_id order.
        Note: the busbar_helper_node are transformed to busbar ids.
    """
    busbars, busbars_helper_nodes = get_busbar_true_nodes(graph=graph)
    sides_of_coupler = {}

    for edge_id, side_paths in asset_bay_edge_id_update_dict.items():
        side1_path = side_paths[0]
        side2_path = side_paths[1]
        side1 = [find_matching_node_in_list(node_id, busbars_helper_nodes, busbars) for node_id in side1_path.keys()]
        side2 = [find_matching_node_in_list(node_id, busbars_helper_nodes, busbars) for node_id in side2_path.keys()]
        sides_of_coupler[edge_id] = (side1, side2)

    return sides_of_coupler

set_coupler_busbar_sides #

set_coupler_busbar_sides(graph, busbar_sides_of_coupler)

Set the coupler busbar side in the nx.Graph (based on NetworkGraphData model).

PARAMETER DESCRIPTION
graph

The network graph. Note: The graph is modified in place.

TYPE: Graph

busbar_sides_of_coupler

A dictionary containing the sides of the coupler. key: EDGE_ID (a tuple of node_ids) value: tuple of two lists of busbar ids

TYPE: dict[EDGE_ID, tuple[list[int], list[int]]]

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def set_coupler_busbar_sides(
    graph: nx.Graph,
    busbar_sides_of_coupler: dict[EDGE_ID, tuple[list[int], list[int]]],
) -> None:
    """Set the coupler busbar side in the nx.Graph (based on NetworkGraphData model).

    Parameters
    ----------
    graph : nx.Graph
        The network graph.
        Note: The graph is modified in place.
    busbar_sides_of_coupler : dict[EDGE_ID, tuple[list[int], list[int]]]
        A dictionary containing the sides of the coupler.
        key: EDGE_ID (a tuple of node_ids)
        value: tuple of two lists of busbar ids
    """
    update_edge_dict = {}
    for edge_id, (from_busbars, to_busbars) in busbar_sides_of_coupler.items():
        from_busbar_grid_model_ids = [graph.nodes[busbar_id]["grid_model_id"] for busbar_id in from_busbars]
        to_busbar_grid_model_ids = [graph.nodes[busbar_id]["grid_model_id"] for busbar_id in to_busbars]
        # set the coupler side
        update_edge_dict[edge_id] = {
            "from_busbar_grid_model_ids": from_busbar_grid_model_ids,
            "to_busbar_grid_model_ids": to_busbar_grid_model_ids,
        }

    update_edge_connection_info(graph=graph, update_edge_dict=update_edge_dict)

get_asset_bay_id_grid_model_update_dict #

get_asset_bay_id_grid_model_update_dict(
    asset_bay_edge_id_update_dict,
)

Get the asset bay id grid model update dict for set_asset_bay_edge_attr.

Replaces the edge ids with the grid model ids. Adds the coupler itself to the dict if missing.

PARAMETER DESCRIPTION
asset_bay_edge_id_update_dict

A dictionary containing the found busbars. key: bay_edge_id value: tuple of dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: dict[tuple[int, int], tuple[dict[int, list[int]], dict[int, list[int]]]]

RETURNS DESCRIPTION
asset_bay_id_grid_model_update_dict
1
2
3
4
                                    dict[EDGE_ID, dict[int, list[int]]],
                                    dict[EDGE_ID, dict[int, list[int]]],
                                    dict[EDGE_ID, dict[int, list[int]]]
                                    ]

A tuple (coupler, side1, side2) of dictionary containing the found busbars. key: bay_id (an EDGE_ID) value: dictionary with key: busbar_id and value: list of node_ids from the asset_node the key (a busbars_helper_node)

TYPE: tuple[

Source code in packages/importer_pkg/src/toop_engine_importer/network_graph/filter_strategy/switches.py
def get_asset_bay_id_grid_model_update_dict(
    asset_bay_edge_id_update_dict: dict[EDGE_ID, tuple[dict[int, list[int]], dict[int, list[int]]]],
) -> tuple[
    dict[EDGE_ID, dict[int, list[int]]],
    dict[EDGE_ID, dict[int, list[int]]],
    dict[EDGE_ID, dict[int, list[int]]],
]:
    """Get the asset bay id grid model update dict for set_asset_bay_edge_attr.

    Replaces the edge ids with the grid model ids.
    Adds the coupler itself to the dict if missing.

    Parameters
    ----------
    asset_bay_edge_id_update_dict : dict[tuple[int,int], tuple[dict[int, list[int]], dict[int, list[int]]]]
        A dictionary containing the found busbars.
        key: bay_edge_id
        value: tuple of dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)

    Returns
    -------
    asset_bay_id_grid_model_update_dict : tuple[
                                                dict[EDGE_ID, dict[int, list[int]]],
                                                dict[EDGE_ID, dict[int, list[int]]],
                                                dict[EDGE_ID, dict[int, list[int]]]
                                                ]
        A tuple (coupler, side1, side2) of dictionary containing the found busbars.
        key: bay_id (an EDGE_ID)
        value: dictionary with
            key: busbar_id and
            value: list of node_ids from the asset_node the key (a busbars_helper_node)
    """
    asset_bay_id_side1_update_dict = {}
    asset_bay_id_side2_update_dict = {}
    asset_bay_id_coupler_update_dict = {}
    for edge_id, shortest_path_to_busbar_dict in asset_bay_edge_id_update_dict.items():
        # get coupler update dict, enforces that the coupler itself gets updated
        asset_bay_id_coupler_update_dict[edge_id] = {edge_id[0]: [edge_id[0], edge_id[1]]}
        asset_bay_id_side1_update_dict[edge_id] = shortest_path_to_busbar_dict[0]
        asset_bay_id_side2_update_dict[edge_id] = shortest_path_to_busbar_dict[1]

    return asset_bay_id_coupler_update_dict, asset_bay_id_side1_update_dict, asset_bay_id_side2_update_dict

Exporter#

toop_engine_importer.exporter #

Export data from the AICoE_HPC_RL_Optimizer back to the original format.

  • asset_topology_to_dgs.py: Translate asset topology model to a DGS file (PowerFactory).
  • asset_topology_to_ucte.py: Translate asset topology model to a UCTE file.
  • uct_exporter.py: Translate a RealizedTopology json file to a UCTE file.

__all__ module-attribute #

__all__ = [
    "asset_topo_to_uct",
    "load_ucte",
    "process_file",
    "validate_ucte_changes",
]

asset_topo_to_uct #

asset_topo_to_uct(
    asset_topology,
    grid_model_file_output,
    grid_model_file_input=None,
    station_list=None,
)

Translate asset topology model to UCT and saves the model.

PARAMETER DESCRIPTION
asset_topology

Asset topology model to be translated. Based on the pydantic model from interfaces.asset_topology

TYPE: Topology

grid_model_file_output

Path to save the UCTE file.

TYPE: Path

grid_model_file_input

Path to the grid model file. If not provided, the Topology.grid_model_file will be used.

TYPE: Optional[Path] DEFAULT: None

station_list

List of station grid_model_ids to be translated. If not provided, all stations in the asset_topology will be translated.

TYPE: Optional[str] DEFAULT: None

RAISES DESCRIPTION
NotImplementedError

If asset_topology.asset_setpoints is not None.

Source code in packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py
def asset_topo_to_uct(
    asset_topology: Topology,
    grid_model_file_output: Path,
    grid_model_file_input: Optional[Path] = None,
    station_list: Optional[str] = None,
) -> None:
    """Translate asset topology model to UCT and saves the model.

    Parameters
    ----------
    asset_topology : Topology
        Asset topology model to be translated.
        Based on the pydantic model from interfaces.asset_topology
    grid_model_file_output : Path
        Path to save the UCTE file.
    grid_model_file_input : Optional[Path]
        Path to the grid model file. If not provided, the Topology.grid_model_file will be used.
    station_list : Optional[str]
        List of station grid_model_ids to be translated.
        If not provided, all stations in the asset_topology will be translated.

    Raises
    ------
    NotImplementedError
        If asset_topology.asset_setpoints is not None.

    """
    if asset_topology.asset_setpoints is not None:
        raise NotImplementedError("Asset setpoints are not supported yet.")
    if grid_model_file_input is None:
        grid_model_file_input = asset_topology.grid_model_file
    preamble, nodes, lines, trafos, trafo_reg, postamble = load_ucte(grid_model_file_input)
    for station in asset_topology.stations:
        if station_list is not None and station.grid_model_id not in station_list:
            continue
        asset_change_df = pd.DataFrame(get_changes_from_switching_table(station))
        if len(asset_change_df) > 0:
            change_trafos_lines_in_ucte(trafos, asset_change_df)
            change_trafos_lines_in_ucte(lines, asset_change_df)
        coupler_state_df = pd.DataFrame(get_coupler_state_ucte(station.couplers))
        change_busbar_coupler_state(lines, coupler_state_df)

    # handle order of elements in the ucte file
    handle_duplicated_grid_ids(trafos)
    handle_duplicated_grid_ids(lines)

    output_ucte_str = make_ucte(preamble, nodes, lines, trafos, trafo_reg, postamble)
    with open(grid_model_file_output, "w") as f:
        f.write(output_ucte_str)

load_ucte #

load_ucte(input_uct)

Load UCTE file and return its contents as separate dataframes.

PARAMETER DESCRIPTION
input_uct

Path to the UCTE file.

TYPE: Path

RETURNS DESCRIPTION
preamble

Preamble of the UCTE file.

TYPE: str

nodes

Nodes of the UCTE file.

TYPE: DataFrame

lines

Lines of the UCTE file.

TYPE: DataFrame

trafos

Transformers of the UCTE file.

TYPE: DataFrame

trafo_reg

Transformer regulation of the UCTE file.

TYPE: DataFrame

postamble

Postamble of the UCTE file.

TYPE: str

Source code in packages/importer_pkg/src/toop_engine_importer/exporter/asset_topology_to_ucte.py
def load_ucte(
    input_uct: Path,
) -> tuple[str, pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame, str]:
    """Load UCTE file and return its contents as separate dataframes.

    Parameters
    ----------
    input_uct : Path
        Path to the UCTE file.

    Returns
    -------
    preamble : str
        Preamble of the UCTE file.
    nodes : pd.DataFrame
        Nodes of the UCTE file.
    lines : pd.DataFrame
        Lines of the UCTE file.
    trafos : pd.DataFrame
        Transformers of the UCTE file.
    trafo_reg : pd.DataFrame
        Transformer regulation of the UCTE file.
    postamble : str
        Postamble of the UCTE file.
    """
    with open(input_uct, "r") as f:
        ucte_contents = f.read()
    preamble, nodes, lines, trafos, trafo_reg, postamble = parse_ucte(ucte_contents)

    return preamble, nodes, lines, trafos, trafo_reg, postamble

process_file #

process_file(
    input_uct,
    input_json,
    output_uct,
    topo_id=0,
    reassign_branches=True,
    reassign_injections=False,
)

Process a UCTE file and a preprocessed json file to include split substations.

PARAMETER DESCRIPTION
input_uct

The path to the input UCTE file, the original UCTE

TYPE: Path

input_json

The preprocessed json holding the split substations and information, use the loadflowsolver's preprocessing notebook to generate this

TYPE: Path

output_uct

The path to the output UCTE file, will be overwritten

TYPE: Path

topo_id

The id of the topology to use in the json file

TYPE: int DEFAULT: 0

reassign_branches

If True, reassign branches to the new busbars

TYPE: bool DEFAULT: True

reassign_injections

If True, reassign injections to the new busbars Note: not implemented yet

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
list[str]

The codes of the fake busbars that were inserted

Source code in packages/importer_pkg/src/toop_engine_importer/exporter/uct_exporter.py
def process_file(
    input_uct: Path,
    input_json: Path,
    output_uct: Path,
    topo_id: int = 0,
    reassign_branches: bool = True,
    reassign_injections: bool = False,
) -> dict:
    """Process a UCTE file and a preprocessed json file to include split substations.

    Parameters
    ----------
    input_uct : Path
        The path to the input UCTE file, the original UCTE
    input_json : Path
        The preprocessed json holding the split substations and information, use the loadflowsolver's
        preprocessing notebook to generate this
    output_uct : Path
        The path to the output UCTE file, will be overwritten
    topo_id : int
        The id of the topology to use in the json file
    reassign_branches : bool
        If True, reassign branches to the new busbars
    reassign_injections : bool
        If True, reassign injections to the new busbars
        Note: not implemented yet

    Returns
    -------
    list[str]
        The codes of the fake busbars that were inserted
    """
    if reassign_injections:
        raise NotImplementedError("Reassigning injections is not implemented yet.")

    with open(input_uct, "r") as f:
        ucte_contents = f.read()
    with open(input_json, "r") as f:
        json_contents = json.load(f)
    topo = json_contents[topo_id]["topology"]["substation_info"]
    split_subs = [s for s in topo if is_split(s)]

    preamble, nodes, lines, trafos, trafo_reg, postamble = parse_ucte(ucte_contents)

    statistics = {"changed_ids": {}}  # type: dict

    for topo_element in split_subs:
        statistics["changed_ids"][topo_element["id"]] = {}
        statistics["changed_ids"][topo_element["id"]]["branches"] = {}
        statistics["changed_ids"][topo_element["id"]]["injections"] = {}

        code = topo_element["id"][0:7]
        switches = find_switches(lines, code)
        switches_dict = group_switches(switches)
        if len(switches_dict) == 0:
            raise ValueError(f"No switches found for substation {code}")
        switch_group_id = get_switch_group_number(switches_dict)
        bus_a, bus_b = get_bus_a_b(switches_dict[switch_group_id])

        if (topo_element["branch_assignments"] is not None) and reassign_branches:
            statistics["changed_ids"][topo_element["id"]]["branches"] = apply_branch_assignment(
                topo_element,
                lines,
                trafos,
                trafo_reg,
                bus_a,
                bus_b,
                statistics["changed_ids"],
            )

        statistics["changed_ids"][topo_element["id"]]["switches"] = open_switches(lines, switches_dict[switch_group_id])

    new_ucte = make_ucte(preamble, nodes, lines, trafos, trafo_reg, postamble)

    with open(output_uct, "w") as f:
        f.write(new_ucte)

    validate_ucte_changes(ucte_contents, new_ucte)

    return statistics

validate_ucte_changes #

validate_ucte_changes(ucte_contents, ucte_contents_out)

Validate the changes made to the UCTE file.

PARAMETER DESCRIPTION
ucte_contents

The original UCTE file

TYPE: str

ucte_contents_out

The modified UCTE file

TYPE: str

RAISES DESCRIPTION
RuntimeError

If the changes are not as expected

Source code in packages/importer_pkg/src/toop_engine_importer/exporter/uct_exporter.py
def validate_ucte_changes(ucte_contents: str, ucte_contents_out: str) -> None:
    """Validate the changes made to the UCTE file.

    Parameters
    ----------
    ucte_contents : str
        The original UCTE file
    ucte_contents_out : str
        The modified UCTE file

    Raises
    ------
    RuntimeError
        If the changes are not as expected

    """
    if len(ucte_contents) != len(ucte_contents_out):
        raise RuntimeError(
            f"File sizes are different -> error in applying topology. "
            f"Length of original UCTE: {len(ucte_contents)}, Length of modified UCTE: {len(ucte_contents_out)}. "
            + "Length should not change, due to renaming of branches and opening switches."
        )