# `Monty.Cmp.Message`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/cmp/message.ex#L1)

A message in Monty's cortical messaging protocol (CMP).

Mirrors `tbp/monty/cmp.py::Message`. The same payload represents an observed
state (from a sensor module), a hypothesized state (from a learning module)
or a goal (from a goal generator) — the receiver decides how to read it.

Differences from the Python class, on purpose:

  * `sender_type` is an atom (`:sm | :lm`) here and the strings `"SM"`/`"LM"`
    on the wire (`Monty.Cmp.encode/1`), so a typo is a compile-time warning
    rather than a runtime assertion.
  * Construction returns `{:ok, message} | {:error, reason}` instead of
    raising, because messages are built inside supervised processes whose job
    is to keep the run alive.
  * Vectors are plain lists (`[x, y, z]`, `[[x, y, z], ...]`), the shape Nx
    and JSON both read without conversion.
  * `semantic_id` is a field here and a private attribute in Python
    (`percept._semantic_id`). Monty sets it "just for logging" — its own
    words, on the line that sets it — and nothing in matching reads it; here
    a private field does not exist, so the annotation is a field and the
    promise is the same one Monty makes in a comment.

# `pose_vectors`

```elixir
@type pose_vectors() ::
  Monty.Geometry.SurfacePoseVectors.t()
  | Monty.Geometry.OrientationMatrix.t()
  | Monty.Geometry.GazeDirection.t()
```

# `sender_type`

```elixir
@type sender_type() :: :sm | :lm | :gsg
```

Where a message came from. `:gsg` only appears on `Monty.Cmp.Goal`.

# `t`

```elixir
@type t() :: %Monty.Cmp.Message{
  confidence: float(),
  displacement: %{optional(String.t()) =&gt; term()} | nil,
  location: vector() | nil,
  morphological_features: %{optional(String.t()) =&gt; term()},
  non_morphological_features: %{optional(String.t()) =&gt; term()},
  pass_message: boolean(),
  process_features_in_lm: boolean(),
  semantic_id: integer() | nil,
  sender_id: String.t(),
  sender_type: sender_type()
}
```

# `vector`

```elixir
@type vector() :: [float()]
```

# `curvature_directions`

```elixir
@spec curvature_directions(t()) ::
  {:ok, {vector(), vector()}} | {:error, :no_surface_pose_vectors}
```

The two principal curvature directions, for `SurfacePoseVectors`.

# `feature`

```elixir
@spec feature(t(), String.t()) ::
  {:ok, term()} | {:error, {:feature_not_found, String.t()}}
```

Looks a feature up among the morphological and then the non-morphological
features.

# `from_sensor_module?`

```elixir
@spec from_sensor_module?(t()) :: boolean()
```

True when the message was produced by a sensor module.

# `new`

```elixir
@spec new(map() | keyword()) :: {:ok, t()} | {:error, term()}
```

Builds a message, validating the fields the Python class asserts on.

A location-only message (`process_features_in_lm: false`) may omit the pose
features; it only synchronises the receiver's sense of where the sensor is.

# `on_object?`

```elixir
@spec on_object?(t()) :: boolean()
```

Whether the sender believes the sensor is on an object.

Monty's default is `true` when the feature is absent, because a message that
says nothing about it came from a sensor that could not tell. The feature
itself is a number — a sensor module writes `1.0` or `0.0`, a goal generator
`1` — which Monty's `get_on_object` hands on and every caller only ever asks
as a question, so the answer here is the question's answer.

# `set_displacement`

```elixir
@spec set_displacement(t(), vector(), term() | nil) :: t()
```

Records how the sensor moved since the previous message, and optionally the
point-pair feature that goes with it.

# `surface_normal`

```elixir
@spec surface_normal(t()) :: {:ok, vector()} | {:error, :no_surface_pose_vectors}
```

The surface normal, for messages carrying `SurfacePoseVectors`.

# `transform_morphological_features`

```elixir
@spec transform_morphological_features(t(), vector() | nil, [[float()]] | nil) ::
  {:ok, t()} | {:error, :gaze_direction_not_transformable}
```

Applies a translation and/or rotation to the message's pose.

Mirrors `Message.transform_morphological_features`: the location moves by the
translation, and the pose vectors are rotated by `rotation * pose_vectors`.
A `GazeDirection` cannot be rotated — it is a goal direction, not a frame.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
