diff --git a/configs/architecture/centralized.yaml b/configs/architecture/centralized.yaml new file mode 100644 index 0000000..62ff16d --- /dev/null +++ b/configs/architecture/centralized.yaml @@ -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" diff --git a/configs/architecture/decentralized.yaml b/configs/architecture/decentralized.yaml new file mode 100644 index 0000000..d87e144 --- /dev/null +++ b/configs/architecture/decentralized.yaml @@ -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" diff --git a/src/brittle_star_project/configs/config_architecture.py b/src/brittle_star_project/configs/config_architecture.py new file mode 100644 index 0000000..bc140dc --- /dev/null +++ b/src/brittle_star_project/configs/config_architecture.py @@ -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" diff --git a/src/brittle_star_project/configs/config_networks.py b/src/brittle_star_project/configs/config_networks.py deleted file mode 100644 index 347936b..0000000 --- a/src/brittle_star_project/configs/config_networks.py +++ /dev/null @@ -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 diff --git a/src/brittle_star_project/configs/main_config.py b/src/brittle_star_project/configs/main_config.py index 941a5f2..aab864b 100644 --- a/src/brittle_star_project/configs/main_config.py +++ b/src/brittle_star_project/configs/main_config.py @@ -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) diff --git a/src/brittle_star_project/configs/register_configs.py b/src/brittle_star_project/configs/register_configs.py index 22d93ca..697eda9 100644 --- a/src/brittle_star_project/configs/register_configs.py +++ b/src/brittle_star_project/configs/register_configs.py @@ -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)