Deep Dive: Create and Publish Controllers
What Is a Controller?
1
Understand the Controller Methods
Required Methods
Method
What it does
Method Signatures
from typing import Dict, List
from amesa_core import AgentController
class MyController(AgentController):
def __init__(self):
# Must accept zero required arguments
# All self.* state is wiped at every episode reset
pass
async def filtered_sensor_space(self, obs_spec) -> List[str]:
# Declare which sensors this controller observes
# Called once at startup; result is cached
...
async def compute_action(self, obs_spec: Dict, action) -> any:
# Core control logic — called every timestep
# obs_spec: filtered sensor dict (keys match filtered_sensor_space)
# action: previous action sent to the sim (None on first step)
# Returns: the action to send to the simulator
...
async def compute_success_criteria(self, obs_spec: Dict, action) -> bool:
# Return True when the episode should end as a success
...
async def compute_termination(self, obs_spec: Dict, action) -> bool:
# Return True to end the episode early as a failure
...compute_action Parameters
Parameter
Type
Description
2
Write Your Controller
# fermentation_controller/controller.py
from typing import Dict, List
from amesa_core import AgentController
class FermentationController(AgentController):
"""
Proportional controller for dissolved oxygen in a fermentation reactor.
Adjusts agitation speed based on deviation from target DO setpoint.
"""
TARGET_DO = 0.65 # target dissolved oxygen fraction
TOLERANCE = 0.03 # success band: ±0.03
FAIL_DO_LOW = 0.10 # terminate if DO drops this low
KP = 2.5 # proportional gain
def __init__(self):
# No required arguments — called with no args at every episode reset
self.prev_error = 0.0
async def filtered_sensor_space(self, obs_spec) -> List[str]:
# Expose only the sensors this controller actually reads
return ["dissolved_oxygen", "agitation_rpm"]
async def compute_action(self, obs_spec: Dict, action) -> List[float]:
do_level = obs_spec["dissolved_oxygen"]
error = self.TARGET_DO - do_level
# Proportional control: positive error → increase agitation
delta_rpm = self.KP * error
self.prev_error = error
return [float(delta_rpm)]
async def compute_success_criteria(self, obs_spec: Dict, action) -> bool:
do_level = obs_spec["dissolved_oxygen"]
return abs(do_level - self.TARGET_DO) <= self.TOLERANCE
async def compute_termination(self, obs_spec: Dict, action) -> bool:
do_level = obs_spec["dissolved_oxygen"]
return do_level < self.FAIL_DO_LOWAccessing Full Sensor List
async def filtered_sensor_space(self, obs_spec) -> List[str]:
return obs_spec # pass through — observe everything3
Create the Artifact Directory
Using the CLI Scaffold (Recommended)
amesa agent new \
--name fermentation-controller \
--type controller \
--description "Proportional DO controller for fermentation reactor" \
--location ./fermentation-controller/
fermentation_controller/
__init__.py ← empty; required
controller.py ← your AgentController subclass goes here
pyproject.tomlCreating Manually
mkdir -p fermentation-controller/fermentation_controller
touch fermentation-controller/fermentation_controller/__init__.py
touch fermentation-controller/fermentation_controller/controller.py
touch fermentation-controller/pyproject.tomlNaming Rules
Layer
Convention
Example
4
Configure pyproject.toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "fermentation-controller"
version = "0.1.0"
description = "Proportional DO controller for fermentation reactor"
dependencies = [
"amesa-core",
]
[amesa]
type = "agent-controller"
entrypoint = "fermentation_controller.controller:FermentationController"Field Reference
Field
Where
Required
Description
Entrypoint Format
Part
Value in this example
5
6
Using a Controller in an Orchestration
from amesa_core import Orchestration, Agent, Sensor
from fermentation_controller.controller import FermentationController
orchestration = Orchestration()
orchestration.add_sensors([
Sensor("dissolved_oxygen", "Dissolved oxygen fraction [0..1]"),
Sensor("agitation_rpm", "Agitator speed in RPM"),
])
# Pass the CLASS — not an instance
agent = Agent("fermentation-ctrl", FermentationController)
orchestration.add_agent(agent)