1
Fork 0

feat(config): introduce polymorphic architecture configs

centralized/decentralized
This commit is contained in:
Tibo De Peuter 2026-04-14 22:06:15 +02:00
parent 157e061f86
commit 82dfcabede
Signed by: tdpeuter
SSH key fingerprint: SHA256:u/h/LVoqKF1Iz02uOyxe6hcjmoZASCGV2HM0TG9ZMoU
6 changed files with 163 additions and 32 deletions

View file

@ -0,0 +1,21 @@
# Centralized Actor-Critic Architecture
# Baseline configuration with a single global sensor and motor.
# Default values are defined in CentralizedConfig dataclass.
# Use this configuration for standard PPO experiments.
sensor:
hidden_dims: [64, 64]
activation: "tanh"
motor:
hidden_dims: []
activation: "tanh"
feature_extractor:
hidden_dims: [64, 64]
activation: "tanh"
critic:
hidden_dims: []
activation: "tanh"

View file

@ -0,0 +1,31 @@
# Decentralized Actor Architecture (NerveNet-MLP variant)
# Multi-agent/distributed configuration using local sensors, propagators, and motors.
# Default values are defined in DecentralizedConfig dataclass.
# Use this configuration for decentralized execution experiments.
sensor:
hidden_dims: [64, 64]
activation: "tanh"
propagator:
hidden_dims: [64, 64]
activation: "tanh"
motor:
hidden_dims: []
activation: "tanh"
feature_extractor:
hidden_dims: [64, 64]
activation: "tanh"
critic:
hidden_dims: []
activation: "tanh"
# Synchronous message-passing rounds per control step
message_passing_steps: 1
# Connectivity topology (e.g., ring, fully_connected)
topology_type: "ring"

View file

@ -0,0 +1,84 @@
from dataclasses import dataclass, field
from typing import List
@dataclass
class LayerConfig:
hidden_dims: List[int] = field(default_factory=lambda: [64, 64])
activation: str = "tanh"
@dataclass
class ArchitectureConfig:
"""Base class for actor-critic network configurations.
Both centralized and decentralized architectures share a centralized critic
composed of a feature extractor followed by a shallow output layer.
See docs/design/actor-critic.md for the full design rationale.
"""
# Input network: maps global observation to a hidden representation.
feature_extractor: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[64, 64], activation="tanh")
)
# Output network: maps the hidden representation to a scalar value estimate.
critic: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[], activation="tanh")
)
@dataclass
class CentralizedConfig(ArchitectureConfig):
"""Centralized actor-critic architecture (baseline).
The actor is a single global policy composed of a sensor (input network)
and a motor (output network). The sensor receives the full concatenated
global observation; the motor projects the hidden state to all joint actions.
See docs/design/actor-critic.md for the full design rationale.
"""
# Input network: maps the global observation to a hidden state.
sensor: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[64, 64], activation="tanh")
)
# Output network: projects hidden state to (mean, log_std) over all joints.
motor: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[], activation="tanh")
)
@dataclass
class DecentralizedConfig(ArchitectureConfig):
"""Decentralized actor architecture (NerveNet-MLP variant).
Each node runs a local sensor, exchanges messages with neighbours via a
propagator for a fixed number of steps, and then a local motor produces
the joint offset for that node only.
The critic remains centralized (shared with the base class): it receives the
full concatenated global observation and outputs a single scalar.
See docs/design/actor-critic.md and docs/design/communication.md for the
full design rationale.
"""
# Input network: local observation -> initial hidden state per node.
sensor: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[64, 64], activation="tanh")
)
# Message-passing network: aggregates neighbour messages and updates hidden state.
propagator: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[64, 64], activation="tanh")
)
# Output network: final hidden state -> joint offset for this node only.
motor: LayerConfig = field(
default_factory=lambda: LayerConfig(hidden_dims=[], activation="tanh")
)
# Number of synchronous message-passing rounds per control step.
message_passing_steps: int = 1
# Graph topology used for neighbour connections.
# Supported values: "ring", "fully_connected".
topology_type: str = "ring"

View file

@ -1,23 +0,0 @@
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class LayerConfig:
hidden_dims: List[int] = field(default_factory=lambda: [64, 64])
activation: str = "relu"
@dataclass
class MLPConfig:
actor: LayerConfig = field(default_factory=LayerConfig)
critic: LayerConfig = field(default_factory=LayerConfig)
sensor: LayerConfig = field(default_factory=LayerConfig)
feature_extractor: LayerConfig = field(default_factory=LayerConfig)
@dataclass
class NetworksConfig:
mlp: MLPConfig = field(default_factory=MLPConfig)
# Future placeholder for message_passing
# message_passing: Optional[MessagePassingConfig] = None

View file

@ -3,16 +3,23 @@ from dataclasses import dataclass, field
from experiment_logger.config_logger import LoggingConfig
from brittle_star_project.configs.config_experiment import ExperimentConfig
from brittle_star_project.configs.config_ppo import PPOConfig
from brittle_star_project.configs.config_networks import NetworksConfig
from brittle_star_project.configs.config_architecture import ArchitectureConfig, CentralizedConfig
from brittle_star_project.environment.env_config import MorphologyConfig, ArenaConfig, EnvConfig
@dataclass
class BrittleStarConfig:
"""Root configuration for a brittle star training run.
Composed of strictly separated sub-configs. Each sub-config can be swapped
independently via CLI or a different YAML file. See configs/README.md.
"""
experiment: ExperimentConfig = field(default_factory=ExperimentConfig)
logging: LoggingConfig = field(default_factory=LoggingConfig)
ppo: PPOConfig = field(default_factory=PPOConfig)
networks: NetworksConfig = field(default_factory=NetworksConfig)
# Default to centralized; swap with architecture=decentralized on the CLI.
architecture: ArchitectureConfig = field(default_factory=CentralizedConfig)
morphology: MorphologyConfig = field(default_factory=MorphologyConfig)
arena: ArenaConfig = field(default_factory=ArenaConfig)
environment: EnvConfig = field(default_factory=EnvConfig)

View file

@ -3,25 +3,36 @@ from hydra.core.config_store import ConfigStore
from experiment_logger.config_logger import LoggingConfig
from brittle_star_project.configs.config_experiment import ExperimentConfig
from brittle_star_project.configs.config_ppo import PPOConfig
from brittle_star_project.configs.config_networks import NetworksConfig, MLPConfig, LayerConfig
from brittle_star_project.configs.config_architecture import (
CentralizedConfig,
DecentralizedConfig,
)
from brittle_star_project.environment.env_config import MorphologyConfig, ArenaConfig, EnvConfig
from brittle_star_project.configs.main_config import BrittleStarConfig
def register_configs():
"""Register dataclasses with Hydra's ConfigStore."""
def register_configs() -> None:
"""Register all dataclasses with Hydra's ConfigStore.
This must be called before hydra.main() processes the config, ensuring
every structured config is validated against its Python schema. Typos in
YAML keys will raise ConfigAttributeError at startup.
"""
cs = ConfigStore.instance()
# Store the main config schema
# Root schema
cs.store(name="brittle_star_config", node=BrittleStarConfig)
# Store individual structured configs for validation
# Sub-config groups — each group corresponds to a configs/ subdirectory.
cs.store(group="experiment", name="base_experiment", node=ExperimentConfig)
cs.store(group="logging", name="base_logging", node=LoggingConfig)
cs.store(group="ppo", name="base_ppo", node=PPOConfig)
cs.store(group="networks", name="base_networks", node=NetworksConfig)
# Environment configs (reusing existing environment config objects)
# Architecture variants — swap via CLI: architecture=decentralized
cs.store(group="architecture", name="centralized", node=CentralizedConfig)
cs.store(group="architecture", name="decentralized", node=DecentralizedConfig)
# Environment configs
cs.store(group="morphology", name="base_morphology", node=MorphologyConfig)
cs.store(group="arena", name="base_arena", node=ArenaConfig)
cs.store(group="environment", name="base_environment", node=EnvConfig)