Bayesian network inference over geographic space
Project description
geobn
Bayesian network inference over geospatial data.
geobn lets you turn heterogeneous data sources (offline and real-time) into insight over geographical areas by using techniques in probabilistic AI. The library is domain-agnostic, and may be used for, e.g., environmental risk assessment and risk‑informed route planning.
This is achieved by wiring different data sources — rasters, remote APIs, or plain scalars — directly into a Bayesian network, and run pixel-wise inference, producing posterior probability maps and entropy rasters. Under the hood it groups pixels by unique evidence combinations, so each inference query is solved once per combination instead of once per pixel, keeping computations of large areas computationally tractable. Static sources can be disk-cached to avoid redundant network fetches, and bn.precompute() can pre-solve all evidence combinations into a lookup table, reducing repeated inference calls to pure array indexing. The table can be saved with bn.save_precomputed() and loaded on any machine with bn.load_precomputed() — no pgmpy required at runtime.
Full docs (API reference, concepts, examples) are hosted at: https://jensbremnes.github.io/geobn
Install
pip install geobn
To also run the bundled examples, clone the repo instead:
git clone https://github.com/jensbremnes/geobn.git
cd geobn
pip install -e ".[dev]"
Data sources
| Class | Use case |
|---|---|
ArraySource(array, crs, transform) |
In-memory numpy array |
ConstantSource(value) |
Broadcast a scalar over the entire grid |
RasterSource(path) |
Local GeoTIFF / any rasterio-readable file |
URLSource(url, timeout, cache_dir) |
Remote Cloud-Optimised GeoTIFF |
WCSSource(url, layer, valid_range=...) |
Generic OGC WCS endpoint (terrain, bathymetry, …) |
PointGridSource(fn, sample_points, delay) |
Sample any fn(lat, lon) -> float over the bounding box with user-defined resolution |
How it works
DataSources → align to grid → discretize → BN inference → InferenceResult
- Load a BN —
geobn.load("model.bif")reads a standard.biffile via pgmpy. - Attach sources — each evidence node gets a
DataSource. All sources are reprojected and resampled to a common grid at inference time (the finest-resolution georeferenced source sets the grid automatically, or callbn.set_grid()explicitly). - Discretize —
set_discretization(node, breakpoints)bins continuous values into the discrete states your BN expects. - Infer — unique evidence combinations are batched; pgmpy
VariableEliminationruns once per unique combo, not once per pixel. - Export —
InferenceResultgives you a numpy array, an xarray Dataset, or a multi-band GeoTIFF (N probability bands + entropy).
Usage
The examples below use the bundled Lyngen Alps avalanche risk model (see examples/lyngen_alps/) and demonstrate all six source types.
Loading a network
import geobn
bn = geobn.load("avalanche_risk.bif")
bn.set_grid("EPSG:4326", resolution=0.005, extent=(19.8, 69.35, 21.0, 69.75))
Connecting data sources
Attach a DataSource to each evidence node. Sources can be remote services, local files, derived arrays, or plain scalars — they are all reprojected and aligned to a common grid at inference time. DataSource objects are declarative — constructing one performs no I/O. Data is fetched lazily when you call bn.infer() (or bn.fetch_raw() for manual extraction).
# WCSSource — fetch data (e.g., terrain) from WVS server
dtm = geobn.WCSSource(
url="https://hoydedata.no/arcgis/services/las_dtm_somlos/ImageServer/WCSServer",
layer="las_dtm",
version="1.0.0",
valid_range=(-500, 9000), # replaces out-of-range sentinel values with NaN
cache_dir="cache/",
)
# Also possible to extract data as raw numpy array, and do own processing
dtm_array = bn.fetch_raw(geobn.WCSSource(...))
slope_deg, sun_exposure = my_custom_function(dtm_array)
# ArraySource (with no CRS) - wire pre-aligned numpy arrays directly
bn.set_input("slope_angle", geobn.ArraySource(slope_deg))
bn.set_input("sun_exposure", geobn.ArraySource(sun_exposure))
# RasterSource — Reads local GeoTIFF from disk
bn.set_input("forest_cover", geobn.RasterSource("forest_cover.tif"))
# URLSource — remote Cloud-Optimised GeoTIFF
bn.set_input("recent_snow", geobn.URLSource("https://example.com/recent_snow.tif"))
# PointGridSource — sample any fn(lat, lon) -> float over the bounding box
# Useful for point weather APIs (MET Norway Frost, Open-Meteo, etc.)
import requests
def fetch_wind_speed(lat, lon):
r = requests.get(f"https://api.example.com/wind?lat={lat}&lon={lon}")
return r.json()["wind_speed_ms"]
bn.set_input("wind_load", geobn.PointGridSource(fetch_wind_speed, sample_points=20))
# ConstantSource — broadcast a single scalar over the entire grid
bn.set_input("temperature", geobn.ConstantSource(-5.0)) # °C
Discretizing continuous inputs
Breakpoints map continuous raster values into the discrete states your BN expects. The number of intervals must match the number of states for that node.
bn.set_discretization("slope_angle", [0, 5, 25, 40, 90]) # degrees
bn.set_discretization("sun_exposure", [-0.5, 0.5, 1.5, 2.5, 3.5]) # N/E/W/S
bn.set_discretization("forest_cover", [-0.5, 0.5, 1.5, 2.5]) # sparse/moderate/dense
bn.set_discretization("wind_load", [0, 5, 15, 50]) # m/s
bn.set_discretization("recent_snow", [0, 10, 25, 150]) # cm
bn.set_discretization("temperature", [-40, -8, -2, 15]) # °C
Running inference
result = bn.infer(query=["avalanche_risk"])
infer() returns an InferenceResult with a posterior probability array and entropy map for each queried node.
probs = result.probabilities["avalanche_risk"] # (H, W, n_states) — one band per state
ent = result.entropy("avalanche_risk") # (H, W) — Shannon entropy in bits
# State names come directly from the .bif file
for i, state in enumerate(result.state_names["avalanche_risk"]):
print(f"P({state}) mean: {probs[..., i].mean():.3f}")
Exporting results
result.to_xarray() # xarray Dataset
result.to_geotiff("out/") # multi-band GeoTIFF: N probability bands + entropy
result.show_map("out/") # interactive Leaflet map
Caching remote data to disk
URLSource and WCSSource accept a cache_dir argument. When set, fetched data is written to disk as .npy files and reused on subsequent runs — including across Python sessions and script restarts. No network request is made if a matching cache file already exists.
The cache key is a SHA-256 hash of the URL and request parameters (bounding box, resolution, layer), so changing the grid or source automatically triggers a fresh fetch.
dtm = geobn.WCSSource(
url="https://hoydedata.no/arcgis/services/las_dtm_somlos/ImageServer/WCSServer",
layer="las_dtm",
version="1.0.0",
valid_range=(-500, 9000),
cache_dir="cache/", # survives process restarts
)
snow = geobn.URLSource("https://example.com/recent_snow.tif", cache_dir="cache/")
This is particularly useful when iterating on discretization rules or BN structure — fetch the terrain data once, then experiment freely without waiting for remote requests on every run.
Repeated inference with changing inputs
When static inputs (terrain) are mixed with inputs that change between runs (weather), freeze the static nodes so their arrays are fetched and discretized only once:
# Terrain nodes are frozen: fetched and cached on the first infer() call
bn.freeze("slope_angle", "sun_exposure", "forest_cover")
# Sweep over wind scenarios without re-fetching or re-discretizing terrain
for wind_ms in [3, 8, 20]:
bn.set_input("wind_load", geobn.ConstantSource(wind_ms))
result = bn.infer(query=["avalanche_risk"])
result.to_geotiff(f"out/wind_{wind_ms}ms/")
For maximum throughput, pre-run all evidence combinations once and reduce subsequent calls to a numpy index lookup. bn.precompute() exhausts every combination of discrete evidence states, stores the results in an in-memory lookup table, and subsequent bn.infer() calls resolve each pixel by indexing into that table — no pgmpy inference at runtime.
bn.precompute(query=["avalanche_risk"]) # one-time cost: runs all state combinations
result = bn.infer(query=["avalanche_risk"]) # O(H×W) array indexing — no pgmpy at runtime
To persist the table for offline deployment, save it after precompute() and load it on the target machine — no pgmpy inference runs at load or infer time:
# Workstation: build and save
bn.precompute(query=["avalanche_risk"])
bn.save_precomputed("avalanche_table.npz")
# Robot / edge device: load and infer
bn.load_precomputed("avalanche_table.npz")
result = bn.infer(query=["avalanche_risk"]) # pure numpy, no pgmpy
Examples
| Example | Description |
|---|---|
examples/lyngen_alps/ |
Avalanche risk: Kartverket DTM via WCSSource + configurable weather, Lyngen Alps, Norway |
Run from the repo root:
python examples/lyngen_alps/run_example.py
Contributing
Contributions are welcome! Whether it's a bug report, new data source, documentation fix, or feature idea — feel free to open an issue or pull request.
See CONTRIBUTING.md for setup instructions and guidelines.
Academic foundation
geobn is a software realisation of ideas developed during the author's PhD research. If you use this library in academic work, please consider citing the following paper:
J. E. Bremnes, I. B. Utne, T. R. Krogstad, and A. J. Sørensen, "Holistic Risk Modeling and Path Planning for Marine Robotics," IEEE Journal of Oceanic Engineering, vol. 50, no. 1, pp. 252–275, 2025. DOI: 10.1109/JOE.2024.3432935
Declaration of AI use
This library was written with the assistance of Claude (Anthropic). All concepts, design decisions, and research ideas originate with the author.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file geobn-0.1.1.tar.gz.
File metadata
- Download URL: geobn-0.1.1.tar.gz
- Upload date:
- Size: 807.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7a2415b4f1d83376263d5c42f122becc9ed5fcc261b018b4b03999333610c43
|
|
| MD5 |
1d494db922681b81db2e90f07e74ed76
|
|
| BLAKE2b-256 |
89b4d098ff5bc225f5c50e195d5ce2e69e20e968f2af3bbdde3b3a41a6cb6ddd
|
File details
Details for the file geobn-0.1.1-py3-none-any.whl.
File metadata
- Download URL: geobn-0.1.1-py3-none-any.whl
- Upload date:
- Size: 39.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cea633d2ae5365ece0eb28fdfe0b2453de666887fab4c33419b111ca64e38e69
|
|
| MD5 |
81dbb0ea0846209ffcecc02ec106a4e6
|
|
| BLAKE2b-256 |
9dd829451fb15ff0dbb7a0d2d8133e34897753177b1916f86b498502259d044e
|