Architecture & Internals¶
This page covers the internals you need when extending neurospatial — writing a custom layout engine, or reading the raw graph metadata directly. Most users never need this; the mental model in Core Concepts is enough for day-to-day analysis.
Mandatory Graph Metadata¶
Every environment carries a connectivity graph (a NetworkX Graph) whose
nodes are bins and whose edges connect neighboring bins. neurospatial enforces
a set of mandatory attributes on that graph so that spatial queries
(shortest paths, geodesic distances, neighbor finding) are always well-defined.
Node attributes:
'pos': Tuple of N-D coordinates (the bin center)'source_grid_flat_index': Flat index in the original grid'original_grid_nd_index': N-D grid index tuple
Edge attributes:
'distance': Positive navigation path length; native grids use the distance between centers, while explicit bins can declare longer paths'vector': Displacement vector (as a tuple), from the lower to the higher bin ID'edge_id': The edge's row inenv.edge_pairs'angle_2d': Angle in 2D (optional, present for 2D layouts)
You can read these attributes directly off env.connectivity:
import numpy as np
from neurospatial import Environment
positions = np.random.default_rng(0).uniform(0, 50, size=(500, 2))
env = Environment.from_samples(positions, bin_size=5.0, units="cm")
G = env.connectivity
print(f"Number of nodes: {G.number_of_nodes()}")
print(f"Number of edges: {G.number_of_edges()}")
# Node attributes (mandatory)
node_0_attrs = G.nodes[0]
print(f"Node 0 position: {node_0_attrs['pos']}")
print(f"Node 0 grid index: {node_0_attrs['original_grid_nd_index']}")
# Edge attributes (mandatory) — pick any existing edge
u, v = next(iter(G.edges))
edge_attrs = G.edges[u, v]
print(f"Edge ({u},{v}) distance: {edge_attrs['distance']}")
print(f"Edge ({u},{v}) vector: {edge_attrs['vector']}")
These attributes enable geodesic distances and shortest-path queries. Diffusion
uses the owned cell faces and volumes in env.binning, rather than treating
every navigation edge as a physical interface. A navigation connection can carry
a path without permitting diffusion across it.
Package responsibilities¶
The arrows show permitted dependencies between these packages:
flowchart TD
Environment[environment: coordination and caches] --> Layout[layout: construction]
Environment --> Ops[ops: numerical algorithms]
Environment --> Geometry[geometry: owned data and codecs]
Layout --> Geometry
Ops --> Geometry
geometry/ holds immutable bins, locators, arena evidence, coordinate semantics,
validation and codecs. It does not import builders, numerical solvers, storage
backends or plotting libraries. layout/ and ops/ are independent peers.
The only upward imports are the Environment methods that hand off to storage (to_file, to_nwb, ...) or
rendering (animate_fields), which import those packages when called.
An environment captures construction output once and owns its geometry, which
never changes afterwards. Relabelled copies (with_metadata, with_regions)
share its analysis cache; derived and restored environments start with an
empty one. Public methods delegate to domain functions with the components
they read (bins, graph, arena, coordinate descriptor and the analysis cache),
and those functions do not call back into the environment. Numerical builders take explicit data and arrays; callers use
public operations for cached analyses. Packages that consume an environment,
such as ops, declare small consumer protocols of the operations they need.
Custom construction¶
Use Environment.from_bins(...) for explicit Cartesian Binning records, or
Environment.from_layout(engine) for a built layout engine: a built-in engine
from neurospatial.layout.create_layout(...) or your own engine after its
build(...). A custom engine needs no registration. Every layout engine,
built-in or custom, hands its geometry to the environment through binning(),
which returns a Binning built from the engine's own state: bins, neighbor
pairs with their distances (their order becomes env.edge_pairs), point
lookup, lazily computed faces and any grid, track, shape or outline records.
A built-in engine's connectivity graph is derived from that record, so
editing it does not change the bins. The environment keeps that record, not the engine, so live lookup or
track-conversion callbacks do not remain attached. It derives the
connectivity graph, grid_edges, grid_shape, active_mask and
dimension_ranges from the record, so build(...) and binning() are all
an engine needs.
External objects without binning() use registered adapters to supported
built-in snapshots.
Centers and a graph alone establish neither exact cell ownership nor diffusion
faces. Supply a supported locator and explicit KnownFaces or MissingFaces;
optional grid, track and drawing components establish their own capabilities.
See geometry access and cache ownership
for snapshot reuse and the numerical API.
See the layout engines guide for the built-in engines and their factory methods.