# `Monty.Frameworks.Models.FeatureLocationMatching.FeatureGraphMemory`
[🔗](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_memory.ex#L1)

Monty's `FeatureGraphMemory`: the same library, read by feature.

The feature-at-location module matches an observation in two questions. Which
nodes could I be standing on — that is this module's `match_all_node_features`
— and, once a pose is being tested, does the pose agree. The first question is
what narrows a graph of twenty thousand nodes to the handful worth walking
from, so it is asked of the whole graph at once.

## Why this is a module of functions and not a second struct

Monty spells the difference as a subclass, and the difference is four
functions: hypotheses with poses in them, a node's frame, the matching nodes,
and the comparison behind them. Everything else — the models, the feature rows,
the learning — is `GraphMemory`'s, and a second struct would be the same fields
with a different name. So `new/1` builds a `GraphMemory` and these functions
read it, which is the same shape `Monty.Frameworks.Models.GraphMatching.GraphMemory`
already has for the evidence module's `EvidenceGraphMemory`.

## The comparison

Monty builds two arrays per column — the observed value plus and minus its
tolerance — and asks numpy whether each node's value lies between them. A
feature that is *circular* (hue, the first of `hsv`'s three) wraps through 1,
so a tolerance of 0.1 around 0.05 covers `[0.95, 1]` and `[0, 0.15]` and the
test becomes "outside" rather than "inside". A node with no stored value has no
evidence for that column and does not match, which is what Monty's NaN
pre-fill does.

## Where the tolerances come from

Monty's memory holds `features_to_use` *and* the tolerances, because in Python
the module assigns its own tolerances to it. This port's memory holds the
feature **names** it keeps rows for, which is what the evidence kernel reads,
so the tolerances are passed in by the learning module that owns them — the
same numbers, in the same place as the config that stated them.

# `get_initial_hypotheses`

```elixir
@spec get_initial_hypotheses(Monty.Frameworks.Models.GraphMatching.GraphMemory.t()) ::
  {%{
     optional(Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id()) =&gt;
       map()
   },
   %{
     optional(Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id()) =&gt; [
       [[float()]]
     ]
   },
   %{
     optional(Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id()) =&gt;
       []
   }}
```

Monty's `FeatureGraphMemory.get_initial_hypotheses`.

Three answers, not two: every object is possible, every node of it is a place
to have started — as a *path* of one location, because that is what the next
step extends — and no pose is ruled out yet.

# `get_nodes_with_matching_features`

```elixir
@spec get_nodes_with_matching_features(
  Monty.Frameworks.Models.GraphMatching.GraphMemory.t(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id(),
  map(),
  map(),
  keyword()
) :: {[non_neg_integer()], [[float()]] | [[[float()]]]}
```

Monty's `get_nodes_with_matching_features`: the nodes worth walking from.

`:list_of_lists` wraps every location in a list of its own, which is how Monty
hands back a path that starts on a node — the first observation has no
displacement behind it, so a path *is* one location.

Only the first input channel is matched: this module is not used in a
heterarchy, and Monty says the same in the same place.

# `get_rotation_features_at_node`

```elixir
@spec get_rotation_features_at_node(
  Monty.Frameworks.Models.GraphMatching.GraphMemory.t(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id(),
  non_neg_integer(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.input_channel()
) :: [[float()]]
```

Monty's `get_rotation_features_at_node`: a node's frame, as three directions.

The stored pose vectors of one node, shaped as the surface normal and the two
principal curvature directions — what `align_orthonormal_vectors/2` expects.

# `match_all_node_features`

```elixir
@spec match_all_node_features(
  Monty.Frameworks.Models.GraphMatching.GraphMemory.t(),
  map(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.input_channel(),
  Monty.Frameworks.Models.GraphMatching.GraphMemory.graph_id(),
  map()
) :: [non_neg_integer()]
```

Monty's `_match_all_node_features`: which nodes could be where I am.

The answer is the indices of the nodes whose every stored feature lies within
the observed value's tolerance — the row layout and its column names being the
memory's own (`feature_order`), which is why the observation is walked in that
order rather than in the order its features arrive.

A graph being matched always has rows: they are built when the episode starts,
which is what `FeatureGraphLM.init_from_ltm/1` does before the first step.

# `new`

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

A memory that can answer which nodes have which features.

Monty's `FeatureGraphMemory.__init__` takes only the thresholds that decide
when two observations are the same node; the memory it returns is the base
one, because there is nothing else to store.

---

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