# `Monty.Frameworks.Models.FeatureLocationMatching.FeatureGraphLM`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/frameworks/models/feature_location_matching/feature_graph_lm.ex#L1)

A learning module that recognises objects by the features it senses at
locations: Monty's `FeatureGraphLM`.

This is the first learning module Monty had, and it is the one to read to see
what recognising an object out of a graph means. A step carries a location and
the features there. Memory is a graph of locations, each with the features
sensed at it. The module asks two questions per graph:

  1. **Which nodes could I be standing on?** Every node whose stored features
     are within tolerance of what was just sensed, ignoring where they are.
  2. **Does the move agree with a pose I could be in?** For every path still
     possible and every rotation still possible on it, turn the observed step
     into the model's frame by that rotation, land on a place, and keep the
     path if a node near that place carries the pose the rotation predicted.

A path that survives is where the sensor would be if that pose were right.
Recognition is one object left whose path is unique and whose rotation is
unique — or, for an object that looks the same from several sides, the same
set of poses lasting `required_symmetry_evidence` steps.

## What it composes

`%Monty.Frameworks.Models.GraphMatching.GraphLM{}` is the graph module
underneath: the buffer, the goal generator, the votes, the conclusion, the
memory write. This adds what only a feature match has — the poses still
possible on each path, and the symmetry evidence that lets a symmetric object
be recognised at all. The paths themselves stay in the graph module's
`possible_paths`, because they are what its conclusion reads.

## Differences from Monty, on purpose

  * **A step returns a new module** rather than mutating one, so the callbacks
    that narrow the pose hand back the module they changed.
  * **The nearest matching node is found by sorting the candidate nodes**, not
    by asking a k-d tree for them in order. Monty's tree is faster on a graph
    of tens of thousands of nodes; this module's experiments are graphs of
    hundreds, and a sort is the same answer with nothing to keep consistent.
  * **Poses are not jittered.** Monty samples `n_samples` extra rotations
    around each aligned pose with `ctx.rng.vonmises`, and its only caller
    passes zero, so the sampling is not ported; a config asking for it would
    need the RNG threaded in, and no config does.
  * **Votes are negative only.** Monty's feature module also votes on the
    locations and rotations it still considers, and uses the other modules'
    such votes to prune paths. This port's model routes one kind of vote — the
    objects a module has ruled out — so a feature module votes like the rest
    and does no location pruning from other modules' votes. Monty's own
    comment on that path says it is not being developed further.

# `t`

```elixir
@type t() :: %Monty.Frameworks.Models.FeatureLocationMatching.FeatureGraphLM{
  graph_lm: term(),
  initial_possible_poses: term(),
  last_num_unique_locations: term(),
  last_unique_poses: term(),
  max_match_distance: term(),
  path_similarity_threshold: term(),
  pose_similarity_threshold: term(),
  possible_poses: term(),
  required_symmetry_evidence: term(),
  symmetry_evidence: term(),
  tolerances: term(),
  umbilical_num_poses: term()
}
```

# `add_detailed_stats`

```elixir
@spec add_detailed_stats(t(), map()) :: map()
```

Monty's `_add_detailed_stats`: the paths, the poses and the symmetry count.

# `add_lm_processing_to_buffer_stats`

```elixir
@spec add_lm_processing_to_buffer_stats(t(), boolean()) :: t()
```

Records whether this step's observation reached the module.

# `clear_buffers`

```elixir
@spec clear_buffers(t()) :: t()
```

Monty's `_reset_modules_buffers`: drop the observations, keep the memory.

# `clear_conclusion`

```elixir
@spec clear_conclusion(t()) :: t()
```

Forgets what the module concluded, keeping what it believes.

# `clear_hypotheses`

```elixir
@spec clear_hypotheses(t()) :: t()
```

Drops the hypotheses an unsupervised module never earned.

# `compute_possible_matches`

```elixir
@spec compute_possible_matches(
  t(),
  Monty.Context.t(),
  [Monty.Cmp.Message.t()],
  boolean()
) :: t()
```

Monty's `_compute_possible_matches` for this class: features, and the move.

The move is `nil` on the first observation of an episode — there is nothing
behind it to have moved from — and that `nil` is what tells the narrowing to
look for starting places rather than to extend paths.

# `conclusion`

```elixir
@spec conclusion(t()) ::
  Monty.Frameworks.Models.GraphMatching.GraphLM.terminal_state()
```

What this module concluded, as the model reads it.

# `exploratory_step`

```elixir
@spec exploratory_step([Monty.Cmp.Message.t()], Monty.Context.t(), t()) :: {:ok, t()}
```

Monty's `exploratory_step`: keep the observation, match nothing yet.

# `fixme_update_ground_truth`

```elixir
@spec fixme_update_ground_truth(t()) :: t()
```

Monty's `fixme_update_ground_truth`: which objects the graph was built from.

# `from_config`

```elixir
@spec from_config(map(), keyword()) :: {:ok, t()}
```

The module a config asks for, as Monty's `__init__` reads its arguments.

`:memory` is the graphs the module starts from: Monty loads a state dict into
a module it has just built, which is the same thing one step earlier.

# `get_output`

```elixir
@spec get_output(t()) :: {nil, t()}
```

Monty's `get_output`: a feature module publishes no hypothesis upward.

# `get_possible_poses`

```elixir
@spec get_possible_poses(t(), boolean()) :: %{
  optional(Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id()) =&gt;
    list()
}
```

The rotations still possible on each object, one list per path.

Monty's `get_possible_poses`: as Euler angles in degrees, each the *inverse* of
the stored rotation — the turn that takes the sensed frame onto the model's —
which is the form a reader of a log or a chart expects to compare against the
object's pose.

# `get_unique_pose_if_available`

```elixir
@spec get_unique_pose_if_available(
  t(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id()
) ::
  {t(), {:ok, map()} | :not_narrowed}
```

Monty's `get_unique_pose_if_available`: where the object is, once the paths and
the rotations have stopped multiplying.

`[x, y, z, euler_x, euler_y, euler_z, scale]`: the last location of the one
path left, the rotation that turns the model into what the sensor saw, and a
scale — which a feature module does not measure, so it is always 1.

The symmetry counter is evidence gathered across steps, so it comes back with
the answer rather than being kept to one side.

# `graph_memory`

```elixir
@spec graph_memory(t()) ::
  Monty.Frameworks.Models.GraphMatching.GraphMemory.t() | :none
```

The graph memory this module holds, passed to the next episode.

# `init_from_ltm`

```elixir
@spec init_from_ltm(t()) :: t()
```

Monty's `init_from_ltm`: the hypotheses a memory gives a module at the start.

Every object in memory is possible, every node of it is a place to have
started, and no pose is ruled out — and the symmetry counter starts again,
because evidence about *this* episode's poses says nothing about an episode
that has not begun.

# `load_state_dict`

```elixir
@spec load_state_dict(t(), map()) :: t()
```

Monty's `load_state_dict`: a memory, and the module state that follows it.

# `matching_step`

```elixir
@spec matching_step([Monty.Cmp.Message.t()], Monty.Context.t(), t()) :: {:ok, t()}
```

Monty's `matching_step`: what this step's features and move leave possible.

A step that carried only a location is nothing to match; the rest narrow the
hypotheses, and a module nothing is possible for any more concludes
`:no_match`.

# `new`

```elixir
@spec new(keyword()) :: t()
```

A module with an empty memory.

`:max_match_distance` is how far from a place a node may be and still be the
node the sensor arrived at. `:tolerances` is per input channel and per feature,
in the units that feature is sensed in — except pose vectors, which are
degrees in a config and radians everywhere in the matching, so they are
converted here, once.

`:initial_possible_poses` is `"uniform"` (try every rotation in 45° steps),
`"informed"` (start from the pose the sensed pose vectors imply — Monty's
default), or a list of Euler poses to try exactly.

# `observation_processed?`

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

Whether the last buffered observation was one this module processed.

# `pose_features_match?`

```elixir
@spec pose_features_match?(
  t(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.input_channel(),
  non_neg_integer(),
  Monty.Geometry.SurfacePoseVectors.t(),
  boolean()
) :: boolean()
```

Monty's `_match_pose_dependent_features`: does the node's pose agree.

Two comparisons, both angles: the surface normal, and — when the sensor's pose
is fully defined — the first curvature direction, whose sign is not
meaningful, so an angle past a quarter turn is folded back (`π/2 - |θ - π/2|`).

# `propose_goals`

```elixir
@spec propose_goals(t()) :: {[Monty.Cmp.Goal.t()], t()}
```

Monty's `propose_goals`: what the goal generator last asked for.

# `propose_region`

```elixir
@spec propose_region(t()) :: {Monty.Cmp.AttentionRegion.t(), t()}
```

Monty's `propose_region`: the region its goals are in.

# `receive_votes`

```elixir
@spec receive_votes(term(), t()) :: {:ok, t()}
```

Monty's `receive_votes`: another module ruling an object out.

# `reset_stm`

```elixir
@spec reset_stm(t()) :: t()
```

The same, with the buffer and the hypotheses emptied first — Monty's `reset_stm`.

# `send_out_vote`

```elixir
@spec send_out_vote(t()) :: {term(), t()}
```

Monty's `send_out_vote`: the objects this module knows this one is not.

# `set_experiment_mode`

```elixir
@spec set_experiment_mode(
  t(),
  Monty.Frameworks.Models.AbstractMontyClasses.LearningModule.mode()
) ::
  t()
```

Tells the module whether this episode trains or evaluates.

# `set_stepwise_target`

```elixir
@spec set_stepwise_target(t(), String.t() | nil) :: t()
```

Records which object the module's sensor is on this step.

# `set_target`

```elixir
@spec set_target(t(), map() | nil) :: t()
```

Monty's `fixme_reset_ground_truth`: the object this episode is about.

# `set_terminal_state`

```elixir
@spec set_terminal_state(
  t(),
  Monty.Frameworks.Models.GraphMatching.GraphLM.terminal_state()
) :: t()
```

Sets the conclusion without asking the module to reconsider.

# `state_dict`

```elixir
@spec state_dict(t()) :: map()
```

Monty's `state_dict`: the memory, and which objects it was built from.

# `supervise`

```elixir
@spec supervise(t(), map(), [number()]) :: t()
```

The supervised conclusion an experiment states, in place of matching one.

# `update_ltm_from_stm`

```elixir
@spec update_ltm_from_stm(t()) :: t()
```

Monty's `update_ltm_from_stm`: a training episode's observations, into memory.

# `update_stats_after_vote`

```elixir
@spec update_stats_after_vote(t()) :: t()
```

Monty's `update_stats_after_vote`: what a vote round left it believing.

# `update_terminal_condition`

```elixir
@spec update_terminal_condition(t()) ::
  {t(), Monty.Frameworks.Models.GraphMatching.GraphLM.terminal_state()}
```

Monty's `update_terminal_condition`: has this module concluded?

The decision is `GraphLM`'s; what a feature module adds is the pose, which
Monty records as a side effect of being asked. So it is worked out first, here,
and handed to the base as the answer to "is the pose narrowed down".

---

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