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:
- Standard open field experiment → Use
Environment.from_samples()(defaults to RegularGridLayout) - Circular arena → Use
Environment.from_polygon()(ShapelyPolygonLayout) - T-maze or track → Use
Environment.from_graph()(GraphLayout) - 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)
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¶
- Environments Guide: Using different factory methods
- Examples: Visual comparisons
- API Reference