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

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.

# `features`

```elixir
@type features() :: %{optional(String.t()) =&gt; list()}
```

# `graph`

```elixir
@type graph() :: map()
```

# `location`

```elixir
@type location() :: Monty.Frameworks.Utils.SpatialArithmetics.vector()
```

# `model`

```elixir
@type model() :: struct()
```

# `build_model`

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

The model built from one episode's observations.

# `find_nearest_neighbor_ids`

```elixir
@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`

```elixir
@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`

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

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

# `graph`

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

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

# `has_feature?`

```elixir
@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`

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

An empty model for `object_id`.

# `num_nodes`

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

The number of nodes.

# `positions`

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

One `[x, y, z]` per node.

# `set_graph`

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

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

# `update_model`

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

The model extended with one episode's observations.

---

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