# `Monty.Frameworks.Models.EvidenceMatching.LearningModule.EvidenceGraphLM`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/frameworks/models/evidence_matching/learning_module/evidence_graph_lm.ex#L1)

The learning module that recognises objects by accumulating evidence.

Mirrors `...evidence_matching/learning_module.py::EvidenceGraphLM`, on top of
the graph-matching base class (`graph_matching.py::GraphLM`). Monty inherits
the base class's buffer bookkeeping, terminal conditions and vote handling;
Elixir has no inheritance, so those live here too, in the same order the
Python reads.

## What it does with a step

1. **Percepts arrive.** Sensor percepts carry features and a location;
   learning-module percepts carry a hypothesis from below. A step with no
   features at all only *moves*: the sensor travelled, so every hypothesis
   travels with it.
2. **Every graph's hypotheses are displaced** by the sensor's movement, as
   each hypothesis' own orientation sees it, and then **updated**: the
   observation is compared against the nodes around each hypothesis' search
   location, and the ones near the best evidence earn or lose evidence.
   (The comparison itself is `...HypothesesDisplacer`; which hypotheses get
   compared is `...HypothesesUpdater`.)
3. **Possible matches are thresholded.** An object stays possible if its best
   hypothesis is within `x_percent_threshold` per cent of the best object.
   When nothing stands out — the evidence is flat — every object stays
   possible rather than none.
4. **The terminal condition is checked** between steps: recognise when one
   object is left and its pose is narrowed down, give up when nothing is
   left, otherwise keep going.

## Why evidence is unbounded

`past_weight` and `present_weight` both default to `1`, so evidence is a
running sum rather than a moving average: a hypothesis that has matched four
times is four times as convincing as one that has matched once, which is what
makes "within 20% of the leader" a sensible rule. Monty notes a bounded
version would be nicer to configure but performs worse, because it cannot
remember how often it has been right.

## What is short-term and what is long-term

The buffer, the hypotheses, the MLH and the terminal state belong to one
episode and are thrown away by `reset_stm/1`. Graph memory is long-term and
survives; `update_ltm_from_stm/1` is the only thing that writes to it, and
only after a training episode.

## Differences from Monty, on purpose

  * **A learning module returns a new module** instead of mutating itself, so
    a step is a value: a test can hold two versions of the same module and see
    what one step changed.
  * **Evidence updates run for one graph at a time.** Monty's
    `use_multithreading` switch threads the per-object updates; each is
    independent, so the result is identical either way, and parallelism
    belongs to whoever calls the steps rather than to the algorithm.
  * **Object ids come back sorted** (`GraphMemory`'s map order) rather than in
    the order models were learned. Only exact ties can tell the difference,
    and a tie is broken by id, which is at least stable across runs.
  * **A module that cannot read its own sensed pose does not vote** instead of
    raising: the pose is needed to translate votes into another sensor's
    frame, and a vote round is not a place to end an episode.

# `graph_id`

```elixir
@type graph_id() :: String.t()
```

# `mlh`

```elixir
@type mlh() :: %{
  graph_id: graph_id(),
  mlh_id: non_neg_integer() | nil,
  location: [float()],
  rotation: Monty.Geometry.Rotation.t(),
  scale: number(),
  evidence: number()
}
```

The best hypothesis of a graph, or of everything, as one record.

`mlh_id` indexes into that graph's hypothesis space — `nil` until a space
exists, which is why `current_mlh/1` has a location to refresh only when
there is an id to refresh it from.

# `mode`

```elixir
@type mode() :: :train | :eval | nil
```

Monty's `ExperimentMode`: training writes memory, evaluation does not.

# `t`

```elixir
@type t() :: %Monty.Frameworks.Models.EvidenceMatching.LearningModule.EvidenceGraphLM{
  buffer: term(),
  current_mlh: term(),
  detected_object: term(),
  detected_pose: term(),
  detected_rotation_r: term(),
  feature_evidence_increment: term(),
  feature_weights: term(),
  graph_id_to_target: term(),
  graph_memory: term(),
  gsg: term(),
  has_detailed_logger: term(),
  hypotheses: term(),
  hypotheses_updater: term(),
  hypotheses_updater_module: term(),
  hypotheses_updater_options: term(),
  hypotheses_updater_telemetry: term(),
  learning_module_id: term(),
  max_match_distance: term(),
  max_nneighbors: term(),
  mode: term(),
  object_evidence_threshold: term(),
  past_weight: term(),
  path_similarity_threshold: term(),
  pose_similarity_threshold: term(),
  possible_matches: term(),
  present_weight: term(),
  previous_mlh: term(),
  primary_target: term(),
  primary_target_rotation_quat: term(),
  required_symmetry_evidence: term(),
  stepwise_target_object: term(),
  stepwise_targets_list: term(),
  symmetry_evidence: term(),
  target_to_graph_id: term(),
  terminal_state: term(),
  threshold_config: term(),
  tolerances: term(),
  vote_evidence_threshold: term(),
  vote_weight: term(),
  x_percent_threshold: term()
}
```

# `vote`

```elixir
@type vote() :: %{
  possible_states: %{optional(graph_id()) =&gt; [Monty.Cmp.Message.t()]},
  sensed_pose_rel_body: [[float()]]
}
```

What this module tells the vote round: for each object, the hypotheses worth
voting for, plus where this module's own sensor is.

# `add_displacements`

```elixir
@spec add_displacements(t(), [Monty.Cmp.Message.t()]) ::
  {:ok, [Monty.Cmp.Message.t()]} | {:error, term()}
```

Records how far the sensor travelled, on every percept of the step.

The displacement is the same for the whole step — one movement of one sensor —
and is written onto the learning-module percepts too, because a learning
module higher in the hierarchy needs it to place what it is told.

`{:error, :no_sensor_module_location}` when no sensor module reported a
location: a step with no place in it cannot be indexed against the episode.

# `add_displacements`

```elixir
@spec add_displacements(t(), [Monty.Cmp.Message.t()], [float()] | nil) ::
  {:ok, [Monty.Cmp.Message.t()]} | {:error, term()}
```

The same, from a location the caller remembers rather than the buffer.

Monty's `GraphLM._add_displacements` reads `self.buffer.last_location`, and
`NoResetEvidenceGraphLM` overrides it to read its own field — because the model
clears the buffer when the object under the sensor changes, while where the
sensor was has to survive
(`.../no_reset_evidence_matching.py::NoResetEvidenceGraphLM._add_displacements`).
The location is an argument so that each class states where its own is.

# `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 learning module.

# `check_for_symmetry`

```elixir
@spec check_for_symmetry(t(), [non_neg_integer()], [non_neg_integer()], boolean()) ::
  {t(), boolean()}
```

Counts how long the same hypotheses have stayed possible.

A symmetric object cannot be told from a rotated copy of itself, so its
alternatives survive every step. Counting consecutive steps where at least
90% of the possible hypotheses were possible last step is how Monty decides
that this is symmetry rather than slowness, and after
`required_symmetry_evidence` such steps it accepts the pose it has.

# `clear_buffers`

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

Drops the buffer, and keeps everything else.

Monty's `MontyForNoResetEvidenceGraphMatching._reset_modules_buffers` calls
`lm.buffer.reset()` directly, between objects it has no signal for. The buffer
is what an episode is written from and what a training episode builds a graph
out of; the hypotheses, the MLH and the terminal state are what the run
believes, and a run without episode boundaries keeps those.

# `clear_conclusion`

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

Forgets the episode's answer and keeps the hypothesis space.

Monty's `set_individual_ts(None)`: the conclusion, the object it named and the
pose that came with it go; the hypotheses, their evidence and the buffer's
statistics stay.

# `collect_stats_to_save`

```elixir
@spec collect_stats_to_save(t()) :: {t(), map()}
```

The statistics of this step, and the module with the buffer that holds them.

`mlh_prediction_error` is how far the prediction was from the observation, so
it is recorded against the *previous* MLH — the buffer's copy is what a later
step's error is measured from.

# `collect_stats_to_save`

```elixir
@spec collect_stats_to_save(t(), (t(), map() -&gt; map())) :: {t(), map()}
```

The same, with the detailed fields supplied by whoever owns the module.

Monty adds the whole hypothesis space to the step's statistics only for a
module with a detailed logger, through `_add_detailed_stats` — a method a
mixin may *replace*: `TheoreticalLimitLMLoggingMixin` states its own and does
not call `super()`, so the learning module that mixes it in logs its four
fields and not this module's four. Elixir has no inheritance, so the replacing
function is passed in and the rest of the step stays here
(`docs/monty-differences.md` §2.9).

# `compute_possible_matches`

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

Monty's `GraphLM._compute_possible_matches`: the query this step's observation
makes of memory, and the hypothesis space it leaves possible.

`moved?` decides whether the observation carries a displacement with it. It is
false for a step that has not moved yet — the first one — where a displacement
of zero would be a measurement nobody made.

Public because `NoResetEvidenceGraphLM` assembles Monty's own `matching_step`
from the same steps this module's does, and states for itself when the agent
has moved.

# `conclusion`

```elixir
@spec conclusion(t()) ::
  Monty.Frameworks.Models.AbstractMontyClasses.LearningModule.terminal_state()
```

What the module has concluded so far, read and not recomputed.

Monty's `lm.terminal_state`, which this module's own `matching_step` writes
the moment nothing is possible. The model decides the step from it; deciding
what to conclude is `update_terminal_condition/1`.

# `current_mlh`

```elixir
@spec current_mlh(t()) :: mlh()
```

The current most likely hypothesis, with its location refreshed.

The location is read back from the hypothesis space so that it reflects any
displacement since the MLH was computed — a location-only step moves the
hypothesis but not its identity, so the stored index still points at it.

# `displace_all`

```elixir
@spec displace_all(t(), [Monty.Cmp.Message.t()]) :: t()
```

Moves every hypothesis of every object by the sensor's movement.

Used on location-only steps: the sensor travelled without looking, so the
hypotheses must keep up while their evidence stays exactly as it was. The
hypothesis ids do not change, which is why the MLH still points at the same
hypothesis afterwards.

# `displace_hypotheses`

```elixir
@spec displace_hypotheses(t(), [Monty.Cmp.Message.t()], [float()] | nil) :: t()
```

The same, by a displacement the caller works out from its own memory of where
the sensor was.

`NoResetEvidenceGraphLM` overrides Monty's `_displace_hypotheses` for the same
reason it overrides `_add_displacements`: the buffer is cleared between objects
and the sensor's place is not. A module with no previous place has nothing to
displace by and says so by returning itself.

# `enough_symmetry_evidence?`

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

Whether the same alternatives have survived long enough to call it symmetry.

# `evidence_for_each_graph`

```elixir
@spec evidence_for_each_graph(t()) :: {[graph_id()], [number()]}
```

The highest evidence of each object that has any hypotheses, alongside the
ids those belong to.

A module that has not started matching yet has no hypothesis space at all, and
answers with the sentinel `{"patch_off_object", [0.0]}` — Monty's way of
saying "nothing was observed" so that callers do not treat "no candidate" as
"the candidate is the empty string".

# `exploratory_step`

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

A step of learning without trying to recognise: buffer the step, move on.

Monty still displaces here for the same reason it does in a location-only
matching step — hypotheses must stay aligned with the sensor — but never
updates evidence, because in exploration nothing is being tested.

# `fixme_update_ground_truth`

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

`GraphLM.fixme_update_ground_truth`: records which object the graph this
episode built came from.

Monty's name, and its comment: this is the ground truth an evaluation is
scored against, not something the model worked out. It is written after a
training episode, from the object the module detected and the object it was
told to expect, and it is what lets a later run answer "was `mug` recognised as
`mug`" without the training run's target being available.

A graph can come from more than one target and a target can give more than one
graph, so both directions are sets, exactly as Monty keeps them.

# `from_config`

A module built from an experiment's learning-module configuration.

Monty has no such function: Hydra instantiates the config's `_target_` with
the group's parameters, which arrive already composed. This reads the same
mapping — what an experiment writes under
`monty_config.learning_modules.learning_module_0` — and builds both the module
and the memory it learns into, so that an experiment's own numbers decide what
runs rather than a test's.

A target or an updater this port has not written is an error naming it, not a
module that would answer with something else's numbers.

# `get_all_known_object_ids`

```elixir
@spec get_all_known_object_ids(t()) :: [graph_id()]
```

Every object the memory knows about.

# `get_graph`

```elixir
@spec get_graph(t(), graph_id(), String.t() | :first | nil) ::
  %{optional(String.t()) =&gt; Monty.Frameworks.Models.ObjectModel.model()}
  | Monty.Frameworks.Models.ObjectModel.model()
```

The model stored for `model_id`, or one input channel of it.

`nil` returns everything memory holds for the object, keyed by channel; a
channel returns just that channel's model. Monty names this after graphs
because that is all memory used to hold, and keeps the name because the
callers read it as "the thing I compare against".

# `get_mlh_for_object`

```elixir
@spec get_mlh_for_object(t(), graph_id()) :: mlh()
```

The most likely hypothesis of one object, whether or not it is the best overall.

# `get_object_scale`

```elixir
@spec get_object_scale(t(), graph_id()) :: 1
```

Monty has no scale detection yet; every object is at scale one.

# `get_output`

```elixir
@spec get_output(t()) :: {Monty.Cmp.Message.t() | nil, t()}
```

This module's hypothesis, as a message to the module above it.

The pose is the object's orientation, inverted because the message describes
how the *object* sits rather than how the sensor sees it, and
`pass_message` is only true once the object is recognised — a guess is not
something to build on. `object_id` is a stable number derived from the id, so
the module above can match on it as a categorical feature.

Monty's returns the `Message` alone; this comes back with the module, like
every other step in this port (`docs/monty-differences.md` §2.4).

# `get_possible_hypothesis_ids`

```elixir
@spec get_possible_hypothesis_ids(t(), graph_id()) :: [non_neg_integer()]
```

The indices of the hypotheses of `object_id` whose evidence is within
`x_percent_threshold` per cent of the best one, once the best is convincing.

Nothing is possible while the best hypothesis is still below
`object_evidence_threshold`: an object whose every explanation is weak has
not been recognised, whatever the spread of its hypotheses.

# `get_possible_matches`

```elixir
@spec get_possible_matches(t()) :: [graph_id()]
```

The objects currently possible, in memory order.

# `get_possible_poses`

```elixir
@spec get_possible_poses(t(), boolean()) :: %{optional(graph_id()) =&gt; list()}
```

Poses per object, narrowed to nothing here but reported for logging.

Evidence matching narrows hypotheses by evidence rather than by keeping a
path per object, so this is the whole hypothesis space — with euler angles
for a reader, or the raw matrices for a chart.

# `get_top_two_mlh_ids`

```elixir
@spec get_top_two_mlh_ids(t()) :: {graph_id() | nil, graph_id() | nil}
```

The two most likely objects, for a goal generator deciding whether to
discriminate between objects or narrow down one object's pose.

`{nil, nil}` when there is nothing to discriminate yet, and `{id, nil}` when
memory holds a single object — the second element is what tells a goal
generator to work on pose.

Monty's `argsort` gives the same answer, and then corrects for ties against
the current MLH, because a goal generator expects `argmax` and `argsort` to
agree on which object is on top. With three objects tied, the current MLH is
reported as the leader and the runner-up stays whoever it was.

# `get_top_two_pose_hypotheses_for_graph_id`

```elixir
@spec get_top_two_pose_hypotheses_for_graph_id(t(), graph_id()) ::
  {mlh(), mlh() | nil}
```

The best and second-best hypotheses of one object.

`{best, nil}` when the object has a single hypothesis: there is no second
best, and Monty's `argsort(...)[-2]` would index past the end of a one-element
array. Monty raises there, so this is a deviation as well as a defect —
`docs/monty-differences.md` §1.4 and §2.6.

# `get_unique_pose_if_available`

```elixir
@spec get_unique_pose_if_available(t(), graph_id()) :: {t(), [number()] | nil}
```

The chosen pose of `object_id`, if the hypotheses have narrowed to one.

Two ways to be finished: every possible hypothesis agrees on where the object
is and how it is turned, or the same alternatives have survived long enough
that the object is symmetric and any of them will do. Both settle
`detected_pose`, which is the seven numbers a training episode stores and an
evaluation scores.

Returns `{lm, nil}` while the pose is still ambiguous — the hypotheses stay
possible, and a later step may narrow them.

# `graph_memory`

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

The graphs this module matches against, and writes to after a training episode.

# `load_state_dict`

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

Resumes from `state_dict/1` and re-derives everything that depends on memory.

Memory that changes has to be re-read: the feature arrays are rebuilt and the
comparison is rebuilt with them, or the module would keep matching against
the memory it was constructed with.

# `matching_step`

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

One step of trying to recognise an object.

A location-only step moves the hypotheses and stops there; anything else is
buffered, matched, thresholded, and ends in either a recognition, a
no-match, or neither.

# `most_likely_hypothesis`

```elixir
@spec most_likely_hypothesis(t(), graph_id() | nil) :: mlh()
```

The most likely hypothesis, of one object or of all of memory.

With no hypothesis space anywhere, the answer is the current MLH with the
graph id `"new_object0"` — Monty's way of saying "there are no observations"
without inventing an object that was seen.

# `new`

```elixir
@spec new(Monty.Frameworks.Models.GraphMatching.GraphMemory.t(), keyword()) ::
  {:ok, t()} | {:error, term()}
```

A learning module over `memory`.

`:max_match_distance` is how far a hypothesis' search location may sit from a
stored node and still be compared against it; `:tolerances` and
`:feature_weights` are keyed by input channel and then by feature.

The memory is handed in rather than built here: what an object model can
represent — grid resolution, node budget, size limit — is decided where
memory is built, and a learning module reads memory rather than owning it.

`:hypotheses_updater` names the strategy and `:hypotheses_updater_options`
are passed to it; `:displacer` and `:scorer` are built here from the
configuration above, so that every strategy sees the same comparison.

`:gsg` is the goal generator this module will drive, and is `nil` unless an
experiment supplies one — the same default Monty's evidence learning module
has, which takes a generator as an argument rather than creating one.

Pose tolerances are added if the configuration does not name them: matching
never runs without comparing poses, so a missing pose tolerance would silently
mean "any pose matches".

Returns `{:error, %InvalidEvidenceThresholdConfig{}}` for an
`:evidence_threshold_config` Monty does not define.

# `num_hypotheses`

```elixir
@spec num_hypotheses(t(), graph_id()) :: non_neg_integer()
```

How many hypotheses the module holds for one object.

Monty's goal generator reads `_hypotheses[graph_id].evidence` for this, and
its own comment marks the reach as a temporary measure. Here it is a question
the module answers: one hypothesis cannot be separated from itself, which is
the whole reason the goal generator asks.

# `observation_processed?`

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

Whether the last buffered observation was one this module processed.

# `propose_goals`

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

The goals this module's goal generator proposed, and none before it has one.

Monty only proposes after a step that processed an observation — a module
given nothing has nothing new to look at — so this repeats the goals only
when the step that produced them is the last one.

# `receive_votes`

```elixir
@spec receive_votes(%{optional(graph_id()) =&gt; [Monty.Cmp.Message.t()]}, t()) ::
  {:ok, t()}
```

Weighs another module's hypotheses into this module's evidence.

Each incoming hypothesis lands near the hypotheses it agrees with: for every
hypothesis of ours, the closest votes within `max_match_distance` are
considered, and the best of them is averaged in (or added, when evidence is
unbounded). A vote for somewhere we are not is simply not for us — the
hypothesis keeps the evidence it had.

Monty finds the closest votes with a KD-tree; this compares every hypothesis
against every vote, which gives the same neighbours for the handful of votes
a round carries.

# `reset_stm`

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

Clears short-term memory: the buffer, the hypotheses, the MLH, the terminal
state, and the evidence slope a burst was watching.

Long-term memory is untouched, which is why an episode can end mid-object and
the next one still knows what a mug looks like. Monty re-reads the feature
arrays out of memory here, so a graph learned in an earlier episode — or
loaded from a pretrained library — is matchable from the first step.

# `select_features_to_use`

```elixir
@spec select_features_to_use(t(), [Monty.Cmp.Message.t()]) :: %{
  optional(String.t()) =&gt; map()
}
```

The observed features this module matches on, by channel.

Monty's `GraphLM._select_features_to_use`. A feature is kept when the
configuration gives its channel a tolerance for it — an experiment that does
not want to match on `hsv` simply does not configure an `hsv` tolerance.
`pose_fully_defined` is the exception: it is always kept, because it decides
how many hypotheses a node can start, and dropping it would silently halve
the hypothesis space of every node.

Pose vectors arrive as a matrix and are read as nine numbers, which is the
shape the comparison works on.

# `send_out_vote`

```elixir
@spec send_out_vote(t()) :: {vote() | nil, t()}
```

What this module has to say in a vote round, or `nil`.

Only hypotheses whose *scaled* evidence clears `vote_evidence_threshold` are
sent: a module that is unsure must not drag another module's evidence down.
Scaling puts every module's evidence in `[-1, 1]` so that a module which has
matched for twenty steps does not drown out one which has matched for three.

A module that has seen nothing this step stays silent — Monty's rule, and the
reason a vote is a statement about *this* observation.

# `set_detected_object`

```elixir
@spec set_detected_object(t(), :match | :no_match | :time_out | :pose_time_out | nil) ::
  t()
```

Names the object this episode is about, from the conclusion and the memory.

A match names the single possible object; a no-match names a new object, so a
training episode has somewhere to store what it saw; a time-out names
nothing, because a guess is not worth storing. In evaluation a time-out names
the most likely object when its evidence is convincing — an evaluation wants
to know what the module believed when the clock ran out, while training must
not learn from it.

# `set_experiment_mode`

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

Monty's `set_experiment_mode`: decides whether episodes write to memory.

# `set_individual_ts`

```elixir
@spec set_individual_ts(t(), :match | :no_match | :time_out | :pose_time_out | nil) ::
  t()
```

Records that this module has concluded something, and what it concluded.

Monty's `set_individual_ts`: a match also settles the detected pose, which is
what a training episode stores its graph against. Any other conclusion clears
the pose, so a module that takes a match back does not keep claiming it.

# `set_primary_target`

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

Names this episode's object without emptying `stepwise_targets_list`.

The stepwise list is the record of what the sensor was on at every step, and a
run without episode boundaries keeps it across an object change — it is the
only place that record exists.

# `set_stepwise_target`

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

`GraphLM`'s stepwise target: the object under the module's sensor this step.

Set by the model from the percept's semantic id, and kept as a list because a
run's log says what the module was looking at on every step — the stepwise
performance of an episode is a statement about that list, and a graph built
from a patch that wandered off the object is explained by it.

# `set_target`

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

`GraphLM._set_target`: names the object this episode is about.

The target is what makes a performance measurable at all — "correct" is
"recognised the object it was asked about", and the rotation error is measured
against the pose the object was placed with. A new episode clears the object it
was on last, which Monty does here as well: a stale stepwise target would make
the next episode's stepwise performance a statement about the previous one.

`nil` leaves the target alone, which is Monty's guard and its reason: a caller
that has no target for this episode (an unsupervised run) must not erase the
one the module learned from.

# `set_terminal_state`

```elixir
@spec set_terminal_state(
  t(),
  Monty.Frameworks.Models.AbstractMontyClasses.LearningModule.terminal_state()
) :: t()
```

Takes the episode's ending as this module's, without asking it to reconsider.

Monty writes the module's `terminal_state` when the system has decided the
episode is over: a module that never concluded claims nothing, but it is not
left open either, so an episode has one answer per module.

# `state_dict`

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

What has to be kept to resume this module: its memory, and how much detail it
was asked to log.

# `threshold_possible_matches`

```elixir
@spec threshold_possible_matches(t(), number()) :: [graph_id()]
```

The objects still worth considering: those whose best hypothesis is within
`x_percent_threshold` per cent of the best object's.

Three regimes, and the middle one is the interesting one. When the evidence
spreads out (`std > 0.1`) or is all negative, the threshold spans the objects.
When everything is flat *and* memory holds a single object, that object stays
possible at zero evidence — otherwise a one-object memory could never be
recognised, which is the situation of every first training run. With several
objects and flat evidence, all of them stay possible: nothing yet
distinguishes them.

# `update_ltm_from_stm`

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

Folds what this episode saw into long-term memory, if it was a training
episode and if it saw anything.

The graph is stored against `detected_object` and in the object's own frame:
`detected_rotation_r` describes how the object sits, so its inverse is what
turns observations into model coordinates. When nothing was detected in time,
`detected_object` is `nil` and the memory is left alone.

# `update_stats_after_vote`

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

What the vote round left this module believing.

Monty's `update_stats_after_vote`: the step's statistics are written into the
last entry the buffer holds, and then the module's own conclusion is settled.
Nothing possible is `:no_match`; a module that was `:no_match` is let back
when other modules' votes have given it possibilities again.

The second half is the one that cannot be left to the terminal check: that
check clears `:match` when a module loses its object, never `:no_match`, so a
module that lost its hypotheses once would stay concluded for the episode.

# `update_terminal_condition`

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

Decides what this episode has concluded, if anything.

Recognition needs one object left *and* a pose narrow enough to name, or
enough repeated evidence that the object is symmetric — in which case no
single rotation is right and every one of them is. Anything else keeps the
episode going; a module that had concluded "match" and no longer can takes
that back.

---

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