# `Monty.Attention.VoxelGrid`
[🔗](https://github.com/d-led/ex.tbp.monty/blob/v0.1.0/packages/ex_tbp_monty_core/lib/monty/attention/voxel_grid.ex#L1)

A sparse grid of per-voxel weights over 3-D space: Monty's
`attention/voxel_grid.py::VoxelGrid`.

An attention system remembers where it has looked, and it remembers it in
voxels: space is cut into cubes `voxel_size` on a side, and each cube that has
been looked at holds one weight. Space is mostly empty, so the grid is sparse —
Monty backs it with a pandas `DataFrame` whose index is the occupied voxels'
coordinates; here it is a map from a voxel to its weight, which is the same
thing with less machinery.

## Which voxel a point is in

Voxels are **half-open**: a point on the face shared by two voxels belongs to
the one with the larger coordinate, so voxel `v` spans `[v, v + 1)` in units of
`voxel_size`. That is `floor(point / voxel_size)` and it is why a point at
exactly `0.0` is in voxel `0` while one at `-0.0001` is in voxel `-1`.

## What is not here

Monty's representation is public, so its tests can build a malformed
`DataFrame` and check that `validate_dataframe` refuses it: no `(x, y, z)`
index, a duplicated index, no `weight` column, a weight column that is not one
dimension. A map cannot be malformed in those ways — a duplicate voxel is
simply one entry — so what survives of those four cases here is the one rule a
map *can* break: `new/3` refuses two weights for one voxel rather than letting
the last one win. `from_pandas/2` and `to_pandas/1` have no counterpart.

# `t`

```elixir
@type t() :: %Monty.Attention.VoxelGrid{
  voxel_size: float(),
  weights: %{optional(voxel()) =&gt; number()}
}
```

# `voxel`

```elixir
@type voxel() :: {integer(), integer(), integer()}
```

A voxel's lower corner, in voxels.

# `empty`

```elixir
@spec empty(float()) :: t()
```

A grid with no occupied voxels.

# `encode`

```elixir
@spec encode(t()) :: %{optional(String.t()) =&gt; term()}
```

The grid as JSON-encodable data — Monty's `encode_voxel_grid`.

`voxels` and `weight` are parallel lists, which is how Monty writes the
MultiIndex it cannot serialise.

# `new`

```elixir
@spec new(float(), [voxel()], [number()]) :: t()
```

A grid over the given voxels.

Refuses two weights for one voxel: a voxel holds one weight, and silently
keeping the last would turn a caller's bookkeeping bug into a grid that looks
fine.

# `size`

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

The number of occupied voxels.

# `voxel_size`

```elixir
@spec voxel_size(t()) :: float()
```

Edge length of a voxel.

# `voxelize_and_bin_points`

```elixir
@spec voxelize_and_bin_points(float(), [[number()]], [number()]) :: [
  {voxel(), number()}
]
```

Each point's voxel alongside its weight — Monty's `voxelize_and_bin_points`.

The rows come back in point order, which is what a caller that wants to group
them by voxel needs to keep the point indices.

# `voxelize_points`

```elixir
@spec voxelize_points(float(), [[number()]]) :: [voxel()]
```

Which voxel each point is in — Monty's `voxelize_points`.

`floor` rather than `trunc`: the voxels are half-open, so the point at `-0.5`
is in voxel `-1`, not in voxel `0`.

# `voxels`

```elixir
@spec voxels(t()) :: [voxel()]
```

The occupied voxels, in a stable order (the tuples sort).

# `weights`

```elixir
@spec weights(t()) :: [number()]
```

The weights of the occupied voxels, in the same order as `voxels/1`.

# `weights_at_points`

```elixir
@spec weights_at_points(t(), [[number()]], keyword()) :: [number() | nil]
```

The grid's weight at each point.

`:fill` is what an unoccupied voxel answers — `nil` by default, the NaN of
this port, which says "no weight here" rather than "a weight of zero".

---

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