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

The slope of each hypothesis's evidence over a sliding window.

Mirrors `tbp/monty/frameworks/models/evidence_matching/evidence_slope_tracker.py`.
A learning module uses it to drop hypotheses whose evidence is not merely low
but *falling*: a hypothesis that arrived late has not had time to prove
itself, so only ones older than `min_age` updates can be removed.

Slopes are per channel per step, so they stay comparable when the number of
input channels active at a step changes. That is the whole reason the channel
counts are buffered alongside the evidence: a step fed by three channels
accumulates evidence three times as fast, and a slope that ignored it would
make hypotheses look better simply because more sensors were on them.

The evidence is a ring of bytes shared by every hypothesis: `evidence` is
`hypotheses * window_size` f64, `cursor` names the slot the next update
overwrites — which is also the oldest value in every window — and a window in
order is `cursor, cursor + 1, … wrap`. Every hypothesis' window slides by one
slot on the same update, so recording a step writes one value per hypothesis
and moves the cursor; no window is rebuilt. Sliding ten-slot lists a step was
the single largest thing a matching step allocated.

An unfilled slot is a NaN — Monty's `nil`, which a flat window's slope of zero
must not be mistaken for — so a window cannot be read as floats directly, and
`calculate_slopes/1` is a kernel over these bytes.

# `t`

```elixir
@type t() :: %Monty.Frameworks.Models.EvidenceMatching.EvidenceSlopeTracker{
  ages: [non_neg_integer()],
  channels: [pos_integer()],
  cursor: non_neg_integer(),
  evidence: binary(),
  min_age: non_neg_integer(),
  window_size: pos_integer()
}
```

`evidence` is `hypotheses * window_size` f64 in ring order, `channels` the
channel count at each window slot oldest first, and `cursor` the slot the next
update writes.

# `value`

```elixir
@type value() :: float() | nil
```

# `add_hyp`

```elixir
@spec add_hyp(t(), non_neg_integer()) :: t()
```

Adds hypotheses, each with an empty window and an age of zero.

A hypothesis that has never been updated is all padding: it has no slope, and
`calculate_slopes/1` says so with `nil` rather than with zero.

# `calculate_slopes`

```elixir
@spec calculate_slopes(t()) :: [float() | nil]
```

The average per-channel slope of every hypothesis, or `nil` for one whose
window holds no two consecutive observations.

Each step's difference is divided by the number of channels that contributed
at the *later* slot of the pair, then averaged over the window's valid steps.

# `clear_hyp`

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

Forgets every hypothesis, keeping the window size and minimum age.

# `hyp_ages`

```elixir
@spec hyp_ages(t()) :: [non_neg_integer()]
```

The age of every tracked hypothesis, in updates.

# `new`

```elixir
@spec new(keyword()) :: t()
```

A tracker for `window_size` evidence points per hypothesis, allowing removal
once a hypothesis is `min_age` updates old.

# `removable_indices_mask`

```elixir
@spec removable_indices_mask(t()) :: [boolean()]
```

Which hypotheses are old enough to be considered for removal.

# `remove_hyp`

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

Removes the hypotheses at the given indices, keeping the rest in order.

# `select_hypotheses`

```elixir
@spec select_hypotheses(t(), float()) ::
  Monty.Frameworks.Models.EvidenceMatching.EvidenceSlopeTracker.HypothesesSelection.t()
```

Which hypotheses to keep, given a slope threshold.

A hypothesis is kept when its slope is at least the threshold *or* when it is
too young to remove: the second half is what stops a module from throwing away
a hypothesis in the same step it was proposed.

# `total_size`

```elixir
@spec total_size(t()) :: non_neg_integer()
```

How many hypotheses are tracked.

# `update`

```elixir
@spec update(t(), [float()] | binary(), keyword()) ::
  {:ok, t()} | {:error, :wrong_number_of_values}
```

Records this step's evidence and moves every window on by one slot.

`values` is one evidence value per hypothesis — all of them, every step:
a hypothesis that skipped a step would have a slope over a different number of
steps from its neighbours, which is how slopes stop being comparable.

Returns `{:error, :wrong_number_of_values}` rather than raising, because the
caller is a learning module mid-episode: Monty raises `ValueError`, and the
reason is the same either way.

---

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