Monty.Frameworks.Models.EvidenceMatching.LearningModule.EvidenceGraphLM (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

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.

Summary

Types

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

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

t()

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

Functions

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

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

Records whether this step's observation reached the learning module.

Counts how long the same hypotheses have stayed possible.

Drops the buffer, and keeps everything else.

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

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

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

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

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

The current most likely hypothesis, with its location refreshed.

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

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

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

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

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

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

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

Every object the memory knows about.

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

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

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

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

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.

The objects currently possible, in memory order.

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

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

The best and second-best hypotheses of one object.

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

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

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

One step of trying to recognise an object.

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

A learning module over memory.

How many hypotheses the module holds for one object.

Whether the last buffered observation was one this module processed.

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

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

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

The observed features this module matches on, by channel.

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

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

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

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

Names this episode's object without emptying stepwise_targets_list.

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

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

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

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

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

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

What the vote round left this module believing.

Decides what this episode has concluded, if anything.

Types

graph_id()

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

mlh()

@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()

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

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

t()

@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()

@type vote() :: %{
  possible_states: %{optional(graph_id()) => [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.

Functions

add_displacements(lm, percepts)

@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(lm, percepts, previous_location)

@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(lm, processed)

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

Records whether this step's observation reached the learning module.

check_for_symmetry(lm, last_possible_ids, possible_ids, increment_evidence?)

@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(state)

@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(state)

@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(lm)

@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(lm, detailed)

@spec collect_stats_to_save(t(), (t(), map() -> 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(lm, observations, moved?)

@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(lm)

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(lm)

@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(lm, percepts)

@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(lm, percepts, previous_location)

@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?(lm)

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

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

evidence_for_each_graph(lm)

@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(percepts, ctx, lm)

@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(lm)

@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(config, options \\ [])

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(lm)

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

Every object the memory knows about.

get_graph(lm, model_id, input_channel \\ nil)

@spec get_graph(t(), graph_id(), String.t() | :first | nil) ::
  %{optional(String.t()) => 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(lm, graph_id)

@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(evidence_graph_lm, graph_id)

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

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

get_output(state)

@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(lm, object_id)

@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(lm)

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

The objects currently possible, in memory order.

get_possible_poses(lm, as_euler \\ true)

@spec get_possible_poses(t(), boolean()) :: %{optional(graph_id()) => 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(lm)

@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(lm, graph_id)

@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(lm, object_id)

@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(arg1)

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

load_state_dict(lm, memento)

@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(percepts, ctx, lm)

@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(lm, graph_id \\ nil)

@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(memory, options)

@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(lm, graph_id)

@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?(lm)

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

Whether the last buffered observation was one this module processed.

propose_goals(state)

@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(votes, lm)

@spec receive_votes(%{optional(graph_id()) => [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(lm)

@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(lm, percepts)

@spec select_features_to_use(t(), [Monty.Cmp.Message.t()]) :: %{
  optional(String.t()) => 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(lm)

@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(lm, terminal_state)

@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(lm, mode)

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

set_individual_ts(lm, terminal_state)

@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(state, primary_target)

@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(lm, label)

@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(lm, primary_target)

@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(lm, terminal_state)

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(lm)

@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(lm, x_percent_scale_factor \\ 1.0)

@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(lm)

@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(state)

@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(lm)

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.