11. Introduce ControllerVector for Indexed Sub-Controllers#

Date: 2025-11-10

Related: PR #192

Status#

Accepted

Context#

Many devices have multiple identical components that need individual control: multi-axis motion stages, multi-channel power supplies, multiple file writers, etc. Developers must manually register each indexed controller with string-based names and check for this pattern throughout the code to perform different logic

for i in range(num_axes):
    motor = MotorController()
    self.add_sub_controller(f"Axis{i}", motor)  # String-based naming

This approach lacks collection semantics. Accessing controllers requires string manipulation (controller.sub_controllers["Axis0"]) and heuristics to test if an attribute is numerical, rather than natural indexing (controller.axes[0]).

Decision#

Introduce ControllerVector, a specialized controller type for managing collections of indexed sub-controllers with dict-like semantics.

ControllerVector implements MutableMapping with integer-only keys, providing natural indexing, iteration, and length operations. It supports non-contiguous indices and can have shared attributes alongside the indexed sub-controllers.

Key architectural changes:

  • ControllerVector implements __getitem__, __setitem__, __iter__, __len__

  • Keys enforced to be integers only

  • Supports sparse indexing: {1: motor1, 5: motor5, 10: motor10}

  • Can be subclassed to add shared attributes

  • Inherits from BaseController for full integration with controller hierarchy

Consequences#

Benefits#

  • Natural Collection Semantics: Dict-like interface provides familiar indexing and iteration

  • Consistency: Integer-only keys prevent naming inconsistencies

  • Clear Intent: ControllerVector explicitly signals “collection of identical components”

  • Sparse Collections: Non-contiguous indices support flexible numbering schemes

  • Type Safety: Integer indexing enforced by type hints and runtime checks

  • Shared Attributes: Can add attributes to the vector itself, separate from indexed components

Migration Pattern#

Before (Manual registration):

class StageController(Controller):
    def __init__(self, num_axes: int):
        super().__init__()
        for i in range(num_axes):
            motor = MotorController()
            self.add_sub_controller(f"Axis{i}", motor)

        # Access via string keys
        first = self.sub_controllers["Axis0"]

After (ControllerVector):

class StageController(Controller):
    def __init__(self, num_axes: int):
        super().__init__()
        self.axes = ControllerVector(
            {i: MotorController() for i in range(num_axes)},
            description="Motor axes"
        )

        # Natural dict-like access
        first = self.axes[0]
        for i, motor in self.axes.items():
            print(f"Motor {i}: {motor}")