Module: simulate
File: src/spatial_graph_algorithms/simulate/
Status: Stable.
Purpose
Generate synthetic SpatialGraph objects with known ground truth positions.
Used for algorithm benchmarking, testing, and method development.
The public entry point is generate(). Everything else is internal.
Internal Structure
simulate/
├── __init__.py — generate() + list_shapes() + _validate_inputs()
├── generation.py — point cloud sampling per shape
├── graphs.py — edge construction per mode
└── false_edges.py — noise injection
generate() orchestrates three steps:
generate_points(shape) → build_edges(mode) → inject_false_edges(fraction)
↓ ↓ ↓
positions array set of edges all_edges + false_edges set
↓
SpatialGraph(adjacency, positions, edge_metadata)
Shapes
| Name | Dim | How it works |
|---|---|---|
circle |
2D | Rejection sampling inside unit disk |
square |
2D | Uniform random in [0, scale]² |
sphere |
3D | Rejection sampling inside unit sphere |
cube |
3D | Uniform random in [0, scale]³ |
image / image_2d |
2D | Sample from black pixels of a binary image (requires image_path) |
any name in list_shapes() |
2D | Auto-resolved bundled image (e.g. "star", "ring", "triangle") |
Built-in named shapes
The package ships 18 shape images bundled as package data (in
src/spatial_graph_algorithms/data/shapes/). Pass any name directly as the
shape argument — no image_path needed:
from spatial_graph_algorithms.simulate import generate, list_shapes
print(list_shapes())
# ['0', '1', '2', ..., 'circle', 'hole', 'porous', 'ring',
# 'spiral', 'square', 'star', 'triangle']
sg = generate(n=500, shape="star", seed=42)
sg = generate(n=500, shape="ring", seed=42)
Named shapes sample from the black pixels of the corresponding PNG, exactly
like shape="image". They require dim=2.
Connectivity Modes
| Mode | Algorithm | When to use |
|---|---|---|
delaunay_corrected |
Delaunay + prune top 5% longest edges + reconnect | Default — most realistic |
delaunay |
Standard Delaunay | Mathematically clean, may have long outlier edges |
knn |
k nearest neighbours | Controlled average degree |
epsilon / epsilon-ball |
All pairs within radius | Controlled local neighbourhood |
distance_decay |
Probabilistic power-law decay | Soft spatial structure |
knn_bipartite |
k-NN between two partitions | Two-population experiments |
epsilon_bipartite |
Epsilon-ball between two partitions | Two-population experiments |
lattice |
Regular grid (4-conn 2D, 6-conn 3D) | Baseline / sanity checks |
False Edge Injection
inject_false_edges samples random non-existing edges and adds them to the graph.
Each injected edge is labelled is_false=True in sg.edge_metadata.
The candidates pool respects bipartite structure: for bipartite graphs, false edges are only drawn between the two partitions. For large sparse graphs, candidates are sampled directly instead of materializing the full non-edge set, so false-edge injection scales to large visualization and denoising fixtures.
Either false_edges_number (exact count) or false_edges_fraction (fraction of true
edges) can be set, but not both.
How to Add a New Connectivity Mode
- Add a function
mode_<name>(positions, ...) -> Set[Edge]insimulate/graphs.py. Return a set of(min(i,j), max(i,j))tuples. - Add a branch in
build_edges()in the same file. - Add the string name to
SUPPORTED_MODESinsimulate/__init__.py. - Add at least one test in
tests/test_simulate_modes.py.
Example skeleton:
def mode_gabriel(positions: np.ndarray) -> Set[Edge]:
"""Return edges from the Gabriel graph."""
# implement here
...
Constraints:
- The function must be deterministic given the same positions.
- For stochastic modes, accept an rng: np.random.Generator parameter.
- Modes must handle n=2 without crashing.
- Never add to the edge set after the function returns — inject_false_edges handles noise.
How to Add a New Shape
Option A — Bundled image shape (recommended for 2-D silhouettes)
- Add a
<name>.pngbinary image tosrc/spatial_graph_algorithms/data/shapes/. Black pixels are the valid sampling region. - That's it —
generate(shape="<name>")will auto-resolve it. - Add a test in
tests/test_simulate.py.
Option B — Programmatic shape
- Add a
_generate_<name>helper function insimulate/generation.py. - Add a branch in
generate_points(). - Validate dimension compatibility inside the branch.
Tests
tests/test_simulate.py — generate() basic contracts
tests/test_simulate_modes.py — every mode produces valid edges
tests/test_simulate_parameterized_modes.py — modes across n, dim, seed
tests/test_simulate_false_edges.py — noise injection count and labelling
tests/test_simulate_integration.py — simulate through full pipeline