Skip to content

Layout Engines

Layout engines define how continuous space is discretized into bins. This page helps you choose the right engine for your analysis.

Quick Decision Guide

Most common scenarios:

  1. Standard open field experiment → Use Environment.from_samples() (defaults to RegularGridLayout)
  2. Circular arena → Use Environment.from_polygon() (ShapelyPolygonLayout)
  3. T-maze or track → Use Environment.from_graph() (GraphLayout)
  4. Need uniform neighbor distances → Use layout="Hexagonal" (HexagonalLayout)

Available Engines

RegularGridLayout

Standard rectangular/cuboid grids - fastest and most common.

When to use: Default choice for rectangular environments Performance: ⭐⭐⭐⭐⭐ (fastest) Memory: ⭐⭐⭐⭐⭐ (most efficient) Setup complexity: ⭐⭐⭐⭐⭐ (easiest)

env = Environment.from_samples(positions, bin_size=2.5)
# or explicitly:
env = Environment.from_samples(positions, bin_size=2.5, layout="RegularGrid")

HexagonalLayout

Hexagonal tessellation with uniform neighbor distances.

When to use: When analyzing directional patterns or need isotropic representation Performance: ⭐⭐⭐⭐ (very fast) Memory: ⭐⭐⭐⭐ (efficient, ~15% more bins) Setup complexity: ⭐⭐⭐⭐ (easy)

env = Environment.from_samples(positions, bin_size=2.5, layout="Hexagonal")

Trade-off: All 6 neighbors equidistant, but requires 15% more bins for same coverage.

GraphLayout

1D linearized track for mazes and structured environments.

When to use: T-maze, plus maze, linear track experiments Performance: ⭐⭐ (slower lookups) Memory: ⭐⭐ (higher usage) Setup complexity: ⭐⭐ (requires graph definition)

import networkx as nx

G = nx.Graph()
G.add_node(0, pos=(0, 0))
G.add_node(1, pos=(50, 0))
G.add_edge(0, 1, edge_id=0, distance=50.0)

env = Environment.from_graph(
    graph=G,
    edge_order=[(0, 1)],
    edge_spacing=0.0,
    bin_size=2.0,
    track_width=10.0,  # full corridor width
)

Key feature: Converts 2D positions to 1D linear coordinates with env.to_linear(). Each segment is split into equal bins no longer than bin_size (a 50 cm segment with bin_size=3.0 gets 17 bins of about 2.94 cm), so bin lengths can differ between segments. Where segments meet at a node, the end bins of every two of them are neighbors, and the distance between them is measured through the node. Pass the corridor's measured track_width so that bin_at, occupancy and the rate maps can tell positions on the track from positions off it (bin -1); without it they refuse.

MaskedGridLayout

Grid with active/inactive cells that you supply as a boolean mask, built by Environment.from_grid_mask().

When to use: You already know which cells are usable, for example a map of the arena with walls or obstacles removed Performance: ⭐⭐⭐⭐ (fast) Memory: ⭐⭐⭐⭐⭐ (very efficient for sparse environments) Setup complexity: ⭐⭐⭐ (need a mask and its grid edges)

import numpy as np
from neurospatial import Environment

mask = np.ones((20, 20), dtype=bool)
mask[8:12, 8:12] = False  # a central pillar
edges = np.arange(0.0, 50.5, 2.5)  # 20 cells of 2.5 cm
env = Environment.from_grid_mask(mask, grid_edges=(edges, edges), units="cm")
assert env.n_bins == mask.sum()

Environment.from_samples() does not use this engine: with infer_active_bins=True (the default) it builds a RegularGridLayout whose active cells are the visited ones. On 2-D and higher grids, close_gaps, fill_holes and dilate then refine that mask, in that order.

ShapelyPolygonLayout

Grid bounded by polygon for geometric arenas.

When to use: Circular, elliptical, or custom-shaped arenas Performance: ⭐⭐⭐ (moderate) Memory: ⭐⭐⭐⭐ (efficient) Setup complexity: ⭐⭐⭐ (need polygon definition)

from shapely.geometry import Point
import numpy as np

# Circular arena
theta = np.linspace(0, 2*np.pi, 100)
boundary = np.column_stack([50*np.cos(theta), 50*np.sin(theta)])
from shapely.geometry import Polygon
arena = Polygon(boundary)

env = Environment.from_polygon(arena, bin_size=2.5)

TriangularMeshLayout

Triangular tessellation - alternative grid structure.

When to use: Alternative to hexagonal, specific geometric needs Performance: ⭐⭐⭐ (moderate) Memory: ⭐⭐⭐⭐ (efficient) Setup complexity: ⭐⭐⭐⭐ (easy)

from shapely.geometry import Polygon

from neurospatial import Environment
from neurospatial.layout import create_layout

arena = Polygon([(0, 0), (100, 0), (100, 100), (0, 100)])

# Triangular meshes have no dedicated factory: build the layout engine,
# then pass it to from_layout().
mesh = create_layout("TriangularMesh", boundary_polygon=arena, point_spacing=2.5)
env = Environment.from_layout(mesh, units="cm")

Environment.from_layout takes any built layout engine, including one you write yourself (see Custom construction).

ImageMaskLayout

Binary image-based boundaries from video analysis.

When to use: Extracting arena from video frames Performance: ⭐⭐⭐ (moderate) Memory: ⭐⭐⭐ (moderate) Setup complexity: ⭐⭐ (requires image preprocessing)

import numpy as np

image_mask = np.zeros((480, 640), dtype=bool)
image_mask[100:400, 150:500] = True

env = Environment.from_pixel_mask(
    image_mask=image_mask,
    pixel_size=2.5,
)

Performance Comparison

Benchmark: 100x100 cm arena, 2.5 cm bins, 10,000 position lookups

Engine Setup (ms) Lookups/sec Memory (MB) Bins
Regular 5 1,000,000 0.5 1,600
Hexagonal 8 900,000 0.6 1,840
Masked 15 800,000 0.3 ~1,100
Polygon 50 600,000 0.6 ~1,250
Triangular 10 700,000 0.6 ~1,700
Graph 100 100,000 1.0 50 nodes

Practical impact: For typical experiments (10k-100k positions), all engines complete in <1 second. Choose based on spatial structure needs, not performance.

Choosing Bin Size

Bin size has more impact than engine choice for most analyses.

Guidelines by Arena Size

Rat (100x100 cm arena): - Coarse: 5-10 cm → 100-400 bins - Standard: 2-5 cm → 400-2,500 bins - Fine: 1-2 cm → 2,500-10,000 bins

Mouse (40x40 cm arena): - Coarse: 2-4 cm → 100-400 bins - Standard: 1-2 cm → 400-1,600 bins - Fine: 0.5-1 cm → 1,600-6,400 bins

Rule of Thumb

Target 10-100 samples per bin for stable statistics:

# Estimate good bin_size
n_samples = len(positions)
arena_area = 100 * 100  # cm²
target_samples_per_bin = 50

target_bins = n_samples / target_samples_per_bin
bin_size = np.sqrt(arena_area / target_bins)

Decision Tree

What's your environment?
│
├─ Rectangular/square arena
│  ├─ Need direction-independent analysis? → Hexagonal
│  └─ Standard analysis → Regular (default)
│
├─ Circular/elliptical arena → Polygon
│
├─ Track/maze with branches → Graph
│
└─ Sparse data with walls/obstacles
   ├─ Have binary image → ImageMask
   ├─ Have polygon boundary → Polygon
   └─ Just position data → Regular (infer_active_bins=True, the default)

Common Patterns

Pattern 1: Let neurospatial choose

# Simplest - a regular grid whose active bins are the visited ones
env = Environment.from_samples(positions, bin_size=2.5)

Pattern 2: Explicit control

# Choose specific engine when you know what you need
env = Environment.from_samples(
    positions,
    bin_size=2.5,
    layout="Hexagonal"  # Override default
)

Pattern 3: Complex boundaries

# Use factory method for specific geometry
env = Environment.from_polygon(polygon, bin_size=2.5)
# or
env = Environment.from_pixel_mask(image_mask, pixel_size=2.5)

Summary

For most users: - Start with Environment.from_samples() and default settings - Adjust bin_size based on your data density - Only change engine if you have specific geometric or analytical needs

Key factors: 1. Environment shape (rectangular vs. circular vs. track) 2. Data density (affects bin size, not engine) 3. Analysis needs (directional uniformity, 1D linearization)

See Also