Skip to content

← Back to ParallelLocal_SequentialController documentation

ParallelLocal_SequentialController - Source Code

File: Distributed_Design_Optimizer/coordination/innerloop_iterationscheme/ParallelLocal_SequentialController.py

# Copyright (C) The DistributedDesignOptimizer Contributors
# Licensed under the GNU General Public License v3.0. See LICENSE file for details.
"""Parallel local, sequential controller iteration scheme module.

This module provides an iteration scheme where local subsystems execute
in parallel while the controller subsystem executes sequentially.
"""

import multiprocessing
from typing import List, Tuple
from Distributed_Design_Optimizer.postprocess.terminal_print_tools import DDO_Color, Reset, ddo_print, ddo_print_border
from Distributed_Design_Optimizer.subsystem import SubSystemInterface
from Distributed_Design_Optimizer.subsystem import ControllerSubSystemBasis
from Distributed_Design_Optimizer.coordination.innerloop_iterationscheme import IterationSchemeBasis


class ParallelLocal_SequentialController(IterationSchemeBasis):
    """Iteration scheme with parallel local subsystems and sequential controller.

    Executes local subsystems in parallel while running the controller
    subsystem sequentially to ensure proper coordination.
    """

    def __init__(self) -> None:
        """Initialize the iteration scheme."""

    def run_innerloop_jobs_multiprocessing(self, subsystemsIn: List[SubSystemInterface]) -> Tuple[List[SubSystemInterface], List[int]]:
        """Execute inner loop jobs using multiprocessing.

        Args:
            subsystemsIn: List of subsystems to execute.

        Returns:
            Tuple containing the list of executed subsystems and their original indices.
        """
        # Keep track of all executed subsystems and their indices
        all_executed_subsystems: List[SubSystemInterface] = []
        all_subsystem_indices: List[int] = []

        # Separate local subsystems from controller, tracking original indices
        local_subsystems: List[SubSystemInterface] = []
        local_subsystem_indices: List[int] = []
        controller_subsystem: SubSystemInterface | None = None
        controller_subsystem_index: int | None = None

        for i in range(len(subsystemsIn)):
            if isinstance(subsystemsIn[i], ControllerSubSystemBasis):
                controller_subsystem = subsystemsIn[i]
                controller_subsystem_index = i
            else:
                local_subsystems.append(subsystemsIn[i])
                local_subsystem_indices.append(i)

        # Execute all local subsystems in parallel using Pool
        if len(local_subsystems) > 0:
            if len(local_subsystems) > multiprocessing.cpu_count():
                raise ValueError(f"{DDO_Color}ERROR: To run subsystems in parallel, you need to have at least as many cpu cores as there are subsystems to be executed in parallel{Reset}")
            pool = multiprocessing.Pool(processes=min(len(local_subsystems), multiprocessing.cpu_count()),
                                        maxtasksperchild=1)
            executed_subsystems = pool.map(self.run_subsystem, local_subsystems)
            pool.close()
            pool.join()

            # Store executed subsystems and their original indices
            all_executed_subsystems.extend(executed_subsystems)
            all_subsystem_indices.extend(local_subsystem_indices)

        # If there is a controller, execute it sequentially after local subsystems
        if controller_subsystem is not None:
            controller_subsystem = self.run_subsystem(subsystem=controller_subsystem)

            # Store executed controller subsystem and its original index
            all_executed_subsystems.append(controller_subsystem)
            all_subsystem_indices.append(controller_subsystem_index)

        return all_executed_subsystems, all_subsystem_indices

    def print_startup_summary(self) -> None:
        """Nothing to print."""
        pass

    def print_termination_summary(self) -> None:
        """Nothing to print."""
        pass

    def print_beginning_of_run_innerloop_jobs_multiprocessing(self) -> None:
        """Print a banner before executing inner loop jobs with multiprocessing."""
        name = f"{type(self).__name__}:"
        pad = " " * len(name)
        ddo_print_border()
        ddo_print(f"{name} Starting InnerLoop Jobs Execution using Multiprocessing ...")
        ddo_print(f"{pad} Phase 1: All local subsystems in parallel, Phase 2: Controller subsystem sequentially")
        ddo_print_border()
        print()