Here’s a concise, GrowNet-native view of **2D bins**—what they are, how they’re computed, and how you’d actually use them without introducing "DL pooling."

This document is about structural 2D binning. For serial focus selection, active anchors, and anchor maps, see [FOCUS_AND_ANCHORING.md](C:/Development/Projects/GrowNet/docs/FOCUS_AND_ANCHORING.md).

------

## TL;DR

- A **2D bin** is a **deterministic spatial bucket** on the input image plane (or any 2‑D lattice) that groups nearby pixels/neurons.
- Bins are **topological** (geometry only). They don’t learn; the **neurons you connect into each bin do the learning** via their normal slot/weight dynamics.
- You can use bins two ways:
  1. **Structural binning (preferred):** wire many fine‑grid inputs into a smaller set of **bin neurons** (coarse layer).
  2. **Readout binning (optional):** summarize the current 2‑D frame for logging/visualization (max/avg/sum) — this does **not** affect learning.

------

## What a 2D bin is (in GrowNet terms)

- Given an **InputLayer2D(H×W)**, choose **bin height** `Bh` and **bin width** `Bw` (and optionally stride `Sh`, `Sw`).

- Each pixel `(r, c)` maps to a **bin index**:

  ```
  bin_rows = ceil(H / Bh)
  bin_cols = ceil(W / Bw)
  bin_id(r, c) = (r // Bh) * bin_cols + (c // Bw)     # non-overlapping, Sh=Bh, Sw=Bw
  ```

- The **bin neurons** live in a normal `Layer` (no special class needed). Their IDs or names can encode `(bin_row, bin_col)`.

- **Learning stays local**: the receiving bin neuron’s slots/weights “summarize” its neighborhood through repeated spikes from all members mapped into that bin.

> **Difference from pooling:** bins aren’t an operator inside the tick; they’re just **wiring topology**. Any “averaging” emerges from neuron thresholds, modulation, and repeated inputs—not from a fixed reduction function.

------

## Two usage patterns

### 1) Structural binning (recommended)

Create a coarser layer with one neuron per bin and **connect** each input pixel’s neuron to its bin neuron.

**Sketch (Python)** — uses existing primitives only:

```python
from region import Region
from input_layer_2d import InputLayer2D

H, W = 64, 64
Bh, Bw = 8, 8                        # bin size
bin_rows = (H + Bh - 1) // Bh
bin_cols = (W + Bw - 1) // Bw
num_bins = bin_rows * bin_cols

region = Region("spatial_bins")
in_idx = region.add_input_layer_2d(H, W, gain=1.0, epsilon_fire=0.01)
bin_layer_idx = region.add_layer(excitatory_count=num_bins, inhibitory_count=0, modulatory_count=0)

src = region.layers[in_idx]
dst = region.layers[bin_layer_idx]

# Deterministic wiring: many-to-one, pixel -> its bin neuron
for r in range(H):
    for c in range(W):
        src_idx = src.index(r, c)
        bin_id = (r // Bh) * bin_cols + (c // Bw)
        src_neuron = src.get_neurons()[src_idx]
        dst_neuron = dst.get_neurons()[bin_id]
        src_neuron.connect(dst_neuron, feedback=False)   # local fan-in, neuron learns as usual

region.bind_input("pixels", [in_idx])
# Now when you tick_2d, each bin neuron aggregates its neighborhood via normal slot/weight updates.
```

**Why this fits GrowNet:**

- No special layers.
- **Aggregation is learned** (thresholds, slot selection, weight reinforcement).
- You can make multiple bin layers at different scales (a *pyramid*) and wire them in parallel.

### 2) Readout binning (optional utility)

Sometimes you just want a coarse **summary of the current output frame** (for metrics or UI). This is a **non‑learning** operation you compute *after* the tick on `OutputLayer2D.get_frame()` (or similar).

- Example operations: max/avg/sum within each bin window.
- This doesn’t change connectivity or training; it’s a convenience for monitoring.

------

## Design knobs & trade‑offs

- **Bin size (`Bh`, `Bw`)**
   Bigger bins → fewer neurons downstream (cheaper), but coarser spatial locality.
   Smaller bins → more neurons, sharper locality.
- **Stride (`Sh`, `Sw`)**
  - **Non‑overlapping** (`Sh=Bh`, `Sw=Bw`): each pixel goes to exactly one bin (simpler).
  - **Overlapping** (`Sh < Bh` or `Sw < Bw`): a pixel can drive multiple bin neurons (robust but denser wiring).
- **Edge handling**
   If `H` or `W` isn’t divisible by `Bh`, `Bw`, the bottom/right bins are smaller. The mapping above already handles that via `ceil`.
- **Multi‑scale bins**
   Build several bin layers with different `(Bh, Bw)` and wire them to different downstream modules. This gives you scale sensitivity without any “pooling layer.”
- **Sparsity & gating**
   You can gate wiring per bin (e.g., only connect bins whose recent activity exceeds a threshold) to save compute or adapt focus dynamically.

------

## Metrics & invariants

- With **non‑overlapping** bins, each source neuron maps to exactly one bin.
- Wiring cost scales as `O(H×W)` connections into `O(num_bins)` targets, instead of `O(H×W×all_targets)`.
- **RegionMetrics** (slots/synapses) will reflect the coarser fan‑in; `deliveredEvents` is unchanged (edge‑driven), because binning is structural, not a tick‑time operator.

------

## When to use 2D bins

- You want **locality‑preserving reduction** (fewer neurons downstream) while keeping a purely **neuro‑local**, slot‑based learning story.
- You want **pyramids/multi‑scale** features without introducing convolution or pooling layers.
- You want **interpretable topology**: each bin neuron owns a clear spatial neighborhood.

------

### Quick checklist

-  Pick `Bh`, `Bw` (and optional `Sh`, `Sw`).
-  Create a bin layer with `bin_rows * bin_cols` excitatory neurons.
-  Wire each pixel’s input neuron → its bin neuron (deterministic mapping).
-  (Optional) Add a second/third bin layer at larger scales.
-  Bind input and tick as usual; learning happens in the bin neurons’ slots/weights.

If you’d like, I can turn this into a small helper (e.g., `wire_bins_2d(region, src_layer_idx, Bh, Bw, Sh=None, Sw=None)`) that does the deterministic mapping/wiring for you while keeping everything else as-is.
