What this port says to a simulator, and what it expects back.
Monty's world is a Python process: MuJoCo or Habitat, driven by an action
vocabulary (Monty.Frameworks.Actions) and answering observations plus
proprioceptive state — the two payloads MontyBase.step is given. This
module is that contract, in both directions, so the Elixir side can be tested
without a simulator and a simulator can be written against it without reading
any Elixir.
Framing
Length-prefixed JSON: a four-byte big-endian length, then that many bytes of UTF-8 JSON. Not newline-delimited, because a simulator that logs to stdout would then break the stream, and not Erlang's external term format, because the peer is not an Erlang node.
The prefix is written and stripped by Erlang's {:packet, 4} port option, so
this module deals in bodies only; the peer writes the same four bytes itself.
Messages
One request, one reply, both carrying an id so a late reply to a request
that already timed out is recognisable rather than mistaken for this one.
→ {"id": 1, "command": "reset", "sensor_ids": ["patch"]}
← {"id": 1, "ok": true}
→ {"id": 2, "command": "step",
"actions": [{"action": "move_forward", "agent_id": "agent_id_0",
"distance": 0.004}]}
← {"id": 2,
"observations": {"agent_id_0": {"patch": {"location": [...], ...}}},
"proprioceptive_state": {"agent_id_0": {"position": [...],
"rotation": [...],
"sensors": {...}}}}
← {"id": 2, "error": "the sensor id is not known"}The action maps are exactly Monty.Frameworks.Actions.ActionJSONEncoder's —
Monty's own dict(action) — so the peer parses what Monty's own simulator
parses.
Summary
Functions
Proprioceptive state, as the port works with it.
What a reply means.
The body of a request, as JSON-encodable data.
Types
@type request() :: {:reset, [String.t()]} | {:step, [Monty.Frameworks.Actions.Action.t()]}
Functions
@spec proprioceptive_state(map()) :: Monty.Frameworks.Models.MotorSystemState.t()
Proprioceptive state, as the port works with it.
A reply carries Monty's own shape — per agent its position, its rotation as
a wxyz quaternion, and its sensors the same way — while the motor system
works in AgentState and SensorState. The order is Monty's, not JSON's:
quaternion.quaternion writes wxyz and the port's own quaternions are
xyzw, so it is named here rather than guessed at by a caller.
A reply that leaves a position or a rotation out is a simulator that has drifted from this contract, and says so with the agent it happened for.
What a reply means.
A reply that is not JSON, or that answers a different request, is reported rather than guessed at: a simulator that has drifted from this contract is a bug to fix, not a case to handle.
The fourth element is what the world said about the episode, which only a reset
answer has: %{"primary_target" => target} and the scene's
"semantic_id_to_label". It is a map rather than an option so a world that says
nothing costs nobody a clause, and one that has more to say one day does not
need a new arity.
@spec request(non_neg_integer(), request()) :: map()
The body of a request, as JSON-encodable data.