Project Configs

Solarian uses Hydra for configuration. All YAML lives under your project's conf/ directory and is composed by the CLI based on which subcommand you run. The naming and layout follow simple conventions that bind YAML files to Python attributes.

The conf/ folder

conf/ is organized by who reads each file. Only the three process configs sit at the root; every other kind of config has a folder, and a plugin's templates/ mirrors those folders so solarian install seeds each file where its consumer looks:

conf/
├── robot.yaml              # used by `solarian run-robot`
├── controller.yaml         # used by `solarian run-controller`
├── server.yaml             # used by `solarian run-server`
├── modules/<module>.yaml   # a module, named by the robot's attribute
├── skills/<skill>.yaml     # a skill's config
├── services/<service>.yaml # a service or processor config (bbox_tracker)
├── commands/
│   ├── record.yaml         # used by `solarian record`
│   └── replay.yaml         # used by `solarian replay`
├── training/<run>.yaml     # one complete training run, for `solarian train`
├── calibrations/           # calibration results; cameras/<serial>.json
└── deployment/
    └── localhost.yaml      # relay address and device ports for one host layout

A config left at the root of conf/ is not read: looking for conf/modules/right_arm.yaml while conf/right_arm.yaml exists stops with Move conf/right_arm.yaml to conf/modules/right_arm.yaml.

Every CLI subcommand declares a config_name in solarian/cli/commands/__init__.py. When you run the command, Hydra loads conf/<config_name>.yaml; a name with a folder, such as commands/record, is a file in that folder. --conf conf/<folder>/<name> loads another file instead.

CommandConfig file
run-robotconf/robot.yaml
run-controllerconf/controller.yaml
run-serverconf/server.yaml
recordconf/commands/record.yaml
replayconf/commands/replay.yaml
trainnone: --conf conf/training/<name>

What is a config?

A config is a Hydra-composed YAML document validated by a Pydantic model. Composition lets you split shared values (like ZMQ port numbers) into reusable groups; validation catches typos before the robot starts moving.

For example, conf/robot.yaml:

connection: ${deployment.connection}

img_compression_quality: 70
fps: 30

${deployment.connection} pulls the connection: block out of the active deployment. After resolution the runtime config looks like:

img_compression_quality: 70
fps: 30
connection:
  ip: 127.0.0.1
  action_sender_port: 5555
  action_receiver_port: 5556
  state_sender_port: 5557
  state_receiver_port: 5558
  clock_sync_port: 5559

The result is validated against RobotConfig. If a field is missing or has the wrong type, Solarian prints a friendly error and refuses to start.

Module configs and the naming rule

Each Module attached to your Robot reads its own YAML file under conf/. The YAML filename must match the attribute name on your Robot subclass.

class MyRobot(Robot):
    arm = ArmModule()              # → conf/modules/arm.yaml
    gripper = GripperModule()      # → conf/modules/gripper.yaml
    head_camera = CameraModule()   # → conf/modules/head_camera.yaml

Each file is parsed against the module's config_class (default ModuleConfig):

# conf/modules/arm.yaml
actuator_port: 10.0.0.92
frequency: 120
actuators:
  shoulder_pan:
    id: 0
    port: ${actuator_port}
  shoulder_lift:
    id: 1
    port: ${actuator_port}
  # ...

This same naming convention drives Observations and Actions: MyObservation.arm must be a ModuleObs whose fields reference MyRobot.arm.*.

Variable interpolation

Hydra supports ${var} interpolation. A common pattern is to declare a shared port at the top of a module config and reuse it for every actuator:

actuator_port: /dev/ttyUSB0
actuators:
  joint_1:
    port: ${actuator_port}
  joint_2:
    port: ${actuator_port}

Every file in conf/ can also reference ${deployment.*}, which is how host-specific values stay in one place. See below.

Deployments

A deployment is everything that changes with where your processes run: the relay's address and the device paths as seen from the host that owns the hardware. It lives in conf/deployment/<name>.yaml, and the scaffold ships with localhost.yaml:

connection:
  ip: 127.0.0.1
  action_sender_port: 5555
  action_receiver_port: 5556
  state_sender_port: 5557
  state_receiver_port: 5558
  clock_sync_port: 5559

actuator_port_arm: /dev/ttyUSB0
camera_port_wrist_camera: '0'

Every other file in conf/ reads these through ${deployment.*}, so moving a robot between machines means editing one file rather than duplicating the directory:

# conf/robot.yaml
connection: ${deployment.connection}

# conf/modules/arm.yaml
actuator_port: ${deployment.actuator_port_arm}

# conf/modules/wrist_camera.yaml
camera_port: ${deployment.camera_port_wrist_camera}

This works in all three kinds of config file — command, module and skill — even though only command configs go through Hydra composition.

Selecting a deployment

Add conf/deployment/pi.yaml with the Pi's device paths, then pick it per run or once per machine:

solarian run-robot --deployment pi     # or -d pi
export SOLARIAN_DEPLOYMENT=pi          # e.g. in the Pi's shell profile

The default is localhost. A name with no matching file is an error listing the available deployments.

Extending a deployment

A deployment can name another with extends: and set only what differs; its keys are merged over the other's, recursively:

# conf/deployment/solarian_base.yaml
extends: turing
bbox_tracker_url: https://acme--sb-1a2b3c4d5e6f-bbox-tracker-url.modal.run

The connection file Solarian Base writes works this way: it extends the profile the robot already uses with Base's relay and the URLs of the project's endpoints.

Server URLs and tokens

A conf that calls a server reads its URL from a deployment key, so one profile can point the project at servers on the local network and another at hosted ones:

# conf/services/bbox_tracker.yaml
server_url: ${deployment.bbox_tracker_url}
token: ${oc.select:deployment.solarian_base_endpoint_token,null}
server:
  service: sam2-tracker
  port: 8765

# conf/skills/umi_target.yaml
policy_server_url: ${deployment.umi_target_policy_server_url}
policy_server_token: ${oc.select:deployment.solarian_base_endpoint_token,null}
server:
  service: diffusion-policy
  checkpoint: ${oc.select:deployment.umi_target_policy_checkpoint,null}
  port: 8003

${oc.select:deployment.KEY,null} falls back to null when the deployment does not set the key, here the bearer token only hosted endpoints require. The server: block says what solarian run-service and Solarian Base run for the conf; see Services.

Hydra defaults composition

Hydra's defaults list determines the order in which configs are merged. record.yaml builds on the controller config this way:

defaults:
  - controller                # merge conf/controller.yaml
  - _self_                    # then apply the keys defined inline below

_self_ puts the inline keys last, so they override anything pulled in from groups.

Command-line overrides

Anything in the composed config can be overridden on the command line, no quoting required for top-level keys:

solarian run-robot fps=60
solarian record num_episodes=5 episode_time_s=15

Deployment values are not part of the composed config, so change the relay address by editing conf/deployment/<name>.yaml or pointing at another deployment with --deployment.

Overrides also work for +adding keys not present in the base config:

solarian train +precision.amp_enabled=true

These are forwarded straight to Hydra's compose(overrides=...).

Picking a different config file

The --conf flag chooses the config directory and (optionally) the config name within it:

# Use a different conf directory
solarian run-robot --conf conf_experiment

# Use a different config name (conf/robot_v2.yaml) in the default directory
solarian run-robot --conf conf/robot_v2

To run the same configs against different hardware, reach for --deployment rather than a second conf/ directory.

The CLI splits dir/name so the second form maps to conf/robot_v2.yaml.

Custom command configs

A custom CLI command registered via CommandRegistry.register_lazy(..., config_name="commands/my_command") automatically picks up conf/commands/my_command.yaml. If the command's get_config_class() returns a Pydantic model, the loaded config is validated against it. See CLI / Authoring a custom command.

See also

  • Project Settings — the Python-side registry that complements the YAML side.
  • Modules — the consumer of conf/<module_name>.yaml.
  • Server — the connection profile used by all three nodes.
  • CLI — the command/config-file mapping.

Was this page helpful?