Builds spatial metapopulation ODE graphs for plague simulation. Two graph types:
- Medieval venue graph — SGU + venue nodes from a MAY world state HDF5
- HLC landscape graph — 500m×500m grid nodes from the National Historic Landscape Characterisation geodatabase
- Combined graph — HLC grid with SGU venue sub-networks embedded inside it
Outputs HDF5 consumed by a C++ ODE solver. Standalone — no MAY framework dependency at runtime.
Each metacompartment is a node running an independent ODE. Nodes are connected by weighted edges encoding migration probability. The C++ solver defines equations and compartment count; this module only builds the spatial graph and venue labels.
Node types (distinction is Python-only — identical to the solver):
- HLC node — one per 500m×500m HLC grid cell (DominantType in whitelist), at the cell centroid
- Venue node — one per selected venue, scattered fictitiously within the SGU's bounding square
Edge types (all coexist in one CSR):
- HLC ↔ HLC: undirected rook adjacency (N/S/E/W), w = 1.0
- venue ↔ venue: Delaunay triangulation within each SGU, max distance configurable, w = 1.0
- venue → HLC: each convex-hull venue connects outward to the nearest HLC node, w = 1.0
Rat_network_builder/
├── explore_db.py # extract unique landscape types from HLC geodatabase
├── build_hlc_graph.py # HLC rook-adjacency grid → HDF5
├── build_combined_graph.py # combined HLC grid + SGU venue nodes → HDF5
├── build_medieval_graph.py # medieval venue-only graph (SGU + venue nodes) → HDF5
├── requirements.txt
├── data/
│ └── unique_types/ # CSVs of unique HLC type values (from explore_db.py)
│ ├── allowed_types.csv # whitelist for HLC node inclusion
│ ├── dominant_type.csv
│ ├── dominant_hlc_legend.csv
│ ├── all_hlc_types.csv
│ └── all_nhlc_types.csv
└── src/
└── compartment_model_network_builder/
├── world_reader.py # reads SGU + venue records from world HDF5
├── graph_builder.py # MetapopulationGraphBuilder — accumulates nodes/edges → CSR
├── node_sources.py # NodeSet dataclass; load_sgu_nodes(); load_venue_nodes()
├── venue_positioner.py
├── edge_builders.py # register_edge_builder decorator + radius_ball, delaunay
└── hdf5_export.py # export_hdf5() — writes HDF5; always includes n_nodes attr
Extracts unique landscape character type values from an Esri SQLite Mobile Geodatabase
(.geodatabase). Outputs CSVs to data/unique_types/.
python explore_db.py [--db DATA.geodatabase] [--output-dir data/unique_types/]Reads four columns: DominantType, DominantHLCLegend, AllHLCTypes, AllNHLCTypes.
AllHLCTypes / AllNHLCTypes are semicolon-delimited compound fields; percentage suffixes
are stripped automatically.
Nodes = HLC 500m×500m cells whose DominantType is in allowed_types.csv.
Edges = undirected rook adjacency (N/S/E/W), w = 1.0. Isolated nodes included.
python build_hlc_graph.py [--db DATA.geodatabase] [--types allowed_types.csv] [--out hlc_rook_graph.h5]Coordinates converted from OSGB36 BNG (EPSG:27700) → WGS84 (EPSG:4326) via pyproj.
venue_id_data stores the OBJECTID of each HLC cell for downstream traceability.
Embeds SGU venue sub-networks from a MAY world state into the HLC rook grid.
Algorithm:
- Load HLC cells and SGU/venue data
- For each SGU: scatter
n_total//2venues uniformly in a square of side10√n_totalmetres (BNG), centred on the SGU centroid - Delete HLC cells whose centroid falls strictly inside any SGU's scatter square
- Build rook graph on surviving HLC cells
- Per SGU: Delaunay-triangulate venues (max distance
MAX_VENUE_DIST_M); connect convex-hull vertices to nearest HLC node in the outward cone (max distanceMAX_SCATTER_DIST_M) - Write HDF5 with
/nodes/node_type(0=HLC, 1=venue)
python build_combined_graph.py \
--world /path/to/world_state_medieval.h5 \
[--db DATA.geodatabase] [--types allowed_types.csv] [--out combined_graph.h5]| Parameter | Default | Meaning |
|---|---|---|
MAX_VENUE_DIST_M |
100.0 | Max venue-venue Delaunay edge (m) |
MAX_SCATTER_DIST_M |
500.0 | Max edge-venue → HLC edge (m) |
SEED |
42 | RNG seed for venue scatter |
Legacy entry point: SGU centroid nodes + scattered venue nodes, no HLC grid.
python build_medieval_graph.py --world /path/to/world_state_medieval.h5 [--out rat_ode_medieval.h5]| Constant | Default | Meaning |
|---|---|---|
MAX_SGU_DIST_KM |
15.0 | SGU-SGU Delaunay pruning distance |
SGU_LAMBDA_KM |
5.0 | λ for SGU-SGU weights |
VENUE_CONNECT_M |
50.0 | Venue-venue connection radius (m) |
VENUE_LAMBDA_M |
10.0 | λ for venue-venue weights (m) |
EDGE_MARGIN_M |
10.0 | Boundary margin for venue-SGU edges |
VENUE_SGU_LAMBDA_KM |
2.0 | λ for venue-SGU weights |
/metadata/
n_nodes int — always present; total node count
+ any kwargs passed to export_hdf5()
/nodes/
latitudes float32 (n,)
longitudes float32 (n,)
venue_id_ptr int32 (n+1,) — CSR row pointers into venue_id_data
venue_id_data int32 (total,) — flat venue/cell IDs; empty slice = no link
node_type int8 (n,) — 0=HLC, 1=venue [combined graph only]
/graph/
csr_row_ptr int32 (n+1,)
csr_col_idx int32 (nnz,)
edge_weights float32 (nnz,)
Central accumulator. All node types are structurally identical to the solver.
b = MetapopulationGraphBuilder()
b.add_nodes(nodeset_a)
b.add_nodes(nodeset_b)
b.add_edges(method='delaunay', max_dist_km=15.0, lambda_km=5.0)
b.add_edges(method='radius_ball', max_dist_km=1.0, lambda_km=0.5)
b.add_edge(u, v, weight) # single manual edge
row_ptr, col_idx, weights = b.build_csr()Edges deduplicated by (u,v) key; higher weight wins on collision. Graph is always symmetric.
export_hdf5(builder, output_path, **metadata_attrs)Always writes metadata/n_nodes. Additional kwargs (e.g. coord_system='WGS84',
n_hlc_nodes=N) are stored as further attributes on /metadata.
world_data = load_world_data("world_state_medieval.h5")
# world_data.sgus : list[SguRecord] — id, name, latitude, longitude
# world_data.venues : list[VenueRecord] — id, latitude, longitude, sgu_name, venue_typeSGUs are the finest geographic level in the file. No MAY framework required.
@register_edge_builder('my_method')
def _my(lats, lons, node_indices, **kwargs):
return [(u, v, weight), ...] # both (u,v) and (v,u) required
edges = build_edges(lats, lons, idx, method='my_method', max_dist_km=5.0, lambda_km=2.0)Built-in: 'radius_ball' (cKDTree), 'delaunay' (scipy Delaunay + haversine prune).