Monty.Frameworks.Models.ObjectModel behaviour (ExTbpMonty.Core v0.1.0)

Copy Markdown View Source

How a learned object is stored — the object-model family.

Mirrors tbp/monty/frameworks/models/object_model.py, which holds three things: the graph model, the grid model that oversamples a surface into voxels before thinning it into a graph, and the error the grid raises when it is asked to hold more space than it was given.

  • Monty.Frameworks.Models.ObjectModel.GraphObjectModel — ported: nodes, their features by column, and edges to the nearest neighbours;
  • ...ObjectModel.GridObjectModel — ported: the voxel grid that averages what was seen in each voxel and thins the busiest ones into a graph, so that one model is bounded in size and in resolution at once;
  • ...ObjectModel.GridTooSmallError — ported: the grid's "this object does not fit" answer.

What Monty keeps in abstract_monty_classes.ObjectModel is an empty base class, so there is nothing to mirror for it here beyond this namespace.

What every object model answers

GridObjectModel is a GraphObjectModel in Python — same interface, a grid in front of the graph. Memory only ever asks a model these questions, so in Elixir, where there is no inheritance to lean on, the same set is written out as a behaviour and both models implement it:

  • new/2 — an empty model for an object id, told its options;
  • build_model/3 and update_model/6 — add an episode's observations, or extend what is there; both can refuse, because a grid can be too small;
  • set_graph/2 — take a pretrained graph as it was stored;
  • graph/1, positions/1, num_nodes/1, get_values_for_feature/2 — what matching reads.

Refusals are {:error, reason} rather than exceptions, since "the object is bigger than this model" is an answer a caller acts on (learn it anyway, log it, model bigger next time) and not an accident.

Summary

Callbacks

The model built from one episode's observations.

The same ids, left as the bytes the index wrote, for a native kernel to read.

The ids of the count nodes nearest each of locations.

The columns holding one feature, as one row per node.

The graph matching reads, or nil before there is one.

Whether the model stores a value for feature at every node.

An empty model for object_id.

The number of nodes.

One [x, y, z] per node.

The model holding a graph that was stored rather than built.

The model extended with one episode's observations.

Types

features()

@type features() :: %{optional(String.t()) => list()}

graph()

@type graph() :: map()

location()

model()

@type model() :: struct()

Callbacks

build_model(model, list, features)

@callback build_model(model(), [location()], features()) ::
  {:ok, model()} | {:error, term()}

The model built from one episode's observations.

find_nearest_neighbor_ids(model, list, pos_integer)

@callback find_nearest_neighbor_ids(model(), [location()], pos_integer()) ::
  {binary(), pos_integer()} | nil

The same ids, left as the bytes the index wrote, for a native kernel to read.

{ids, count}: locations * count little-endian u32 ids, row after row, with 4_294_967_295 where a query found fewer than count. The decoded form above is what the rest of the port compares and reports; this one exists because decoding twenty thousand of them into lists of lists cost more than the search did, and the evidence kernel can read the marker itself.

nil when this model's index has no bytes to offer — a model that walks distances instead of a tree, or one whose index is the Elixir tree.

find_nearest_neighbors(model, list, pos_integer, keyword)

@callback find_nearest_neighbors(model(), [location()], pos_integer(), keyword()) ::
  [[non_neg_integer()]] | [[{float(), non_neg_integer()}]]

The ids of the count nodes nearest each of locations.

Displacing asks this to find which stored nodes an unobserved pose would have produced evidence from. Monty queries a KD-tree built over the graph; a grid model's graph is a point cloud with no edges, so it answers the same question by comparing distances.

With distance: true each answer comes as {distance, id} instead of an id. That is Monty's return_distance flag, and it exists for goal generation: deciding where two objects most disagree needs the distances, not the ids.

get_values_for_feature(model, t)

@callback get_values_for_feature(model(), String.t()) :: [[float()]]

The columns holding one feature, as one row per node.

graph(model)

@callback graph(model()) :: graph() | nil

The graph matching reads, or nil before there is one.

has_feature?(model, t)

@callback has_feature?(model(), String.t()) :: boolean()

Whether the model stores a value for feature at every node.

A caller that reads a whole column has to know whether there is one: a graph built from a recording that carried only some of the features has no column to read, and asking for it is an error rather than an empty answer.

new(object_id, options)

@callback new(object_id :: String.t(), options :: keyword()) :: model()

An empty model for object_id.

num_nodes(model)

@callback num_nodes(model()) :: non_neg_integer()

The number of nodes.

positions(model)

@callback positions(model()) :: [location()]

One [x, y, z] per node.

set_graph(model, graph)

@callback set_graph(model(), graph()) :: {:ok, model()} | {:error, term()}

The model holding a graph that was stored rather than built.

update_model(model, list, features, location, location, t)

@callback update_model(
  model(),
  [location()],
  features(),
  location(),
  location(),
  Monty.Geometry.Rotation.t()
) :: {:ok, model()} | {:error, term()}

The model extended with one episode's observations.