Metadata-Version: 2.4
Name: burin-core
Version: 0.3.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Dist: numpy>=1.23
License-File: LICENSE
Summary: Deterministic, equal-area coverage commitments: polygon → rHEALPix cells → 32-byte root, with openings and set algebra.
Keywords: dggs,rhealpix,geospatial,merkle,commitment,equal-area,stac
Author-email: Cameron Sajedi <cameronsajedi@gmail.com>
License-Expression: Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Repository, https://github.com/starling-foundries/burin-core

# burin-core

**A deterministic, equal-area fingerprint of a geographic region.** Give it a polygon and a
resolution; it returns a 32-byte root that anyone, in any language, recomputes from the same
polygon and the same profile, down to the last bit. Two encodings of the same footprint (a COG,
a Zarr, a different tiling) produce one root. Two roots can be unioned, intersected, differenced
and compared without exchanging the geometry, and a cell can be proven inside or outside a root
with a short opening.

The ground is cut into the **rHEALPix** discrete global grid (the OGC API-DGGS Annex B
registered grid, `lon_0 = 50`, WGS84), which is exactly equal-area *and* exactly hierarchical:
nine children tile their parent, so a region held as one cell and the same region spelled out to
the leaves hash identically (the roll-up). Hashing is SHA-256 with domain-tagged nodes. The
hash, the grid parameters, and the time convention are bound into a **profile** whose identity
is derived from its integers, never transmitted.

Apache-2.0. Rust core with a Python wheel (not yet on PyPI: build it with `maturin build --release`, or point uv at this checkout with `burin-core = { path = "../burin-core" }` under `[tool.uv.sources]`); a JavaScript build is
planned. The vocabulary of OGC API - DGGS (zone, refinement level, compact zone list, DGGRS) maps onto the spec one to one; see SPEC.md §7. The projection is a port of the reference implementation
[rhealpixdggs-py](https://github.com/manaakiwhenua/rhealpixdggs-py) (MIT), kept in bit-level
parity by the fixtures under `crates/burin-core/tests/fixtures`.

## Python

Publish a fingerprint; answer questions against it with a proof:

```python
import burin

park = burin.Tree.from_geojson(outline, 11)         # a place, as equal-area cells about 50 m across
print(park.fingerprint)                             # burin:ogc-rhealpix:space:11:f0a5… — publish this line

proof = park.prove_point(lon, lat)                  # made by whoever holds the set
fp = burin.Fingerprint.parse(published_line)        # held by whoever checks
fp.check_point(proof, lon, lat)                     # True / False, or raises burin.InvalidProof
```

`examples/central_park.py` does this for a place and its opening hours together. What a check
establishes, and what it does not, is in [THREAT_MODEL.md](THREAT_MODEL.md).

The rest of the interface:

```python
import burin as bc

aoi = {"type": "Polygon", "coordinates": [[[-0.13, 51.50], [-0.10, 51.50], [-0.10, 51.52], [-0.13, 51.52], [-0.13, 51.50]]]}

bc.fingerprint_polygon(aoi, resolution=10)          # '…' 64 hex chars; same polygon → same bytes, anywhere
bc.OGC_RHEALPIX.id_hex()                            # the profile identity the root is read against

t = bc.Tree.from_geojson(aoi, 10)                   # the canonical coverage
t.cells()                                           # coarsest covered cell ids; t.leaves() expands them
u = t.union(bc.Tree.from_geojson(other, 10))        # set algebra over coverages
proof = t.prove(u, "union"); bc.verify_setop(proof) # a transcript a verifier replays with no geometry
op = t.open(bc.suid_to_cid("Q453")); bc.verify_opening(op)   # membership / non-membership
bc.subzones(bc.suid_to_cid("Q4"), 2)              # sub-zones in the OGC scanline order (SPEC §8)
bc.neighbours(bc.suid_to_cid("Q453"))             # {'up': …, 'right': …, 'down': …, 'left': …}
bc.Tree.from_geojson(aoi, 6).raster()              # uint8 bytes, shape (6, 3**6, 3**6); halo_index(6) pads it
t.to_dggs_json(bc.suid_to_cid("Q"))                 # OGC DGGS-JSON zone data; bc.Tree.from_dggs_json reads it back
```

Arrays in, arrays out, spread over threads (`nthreads=0` uses them all):

```python
import numpy as np

cells = bc.cells_from_lonlat(lon, lat, level=9)     # uint64, the shape of lon/lat (SPEC §8 "Points")
lon_c, lat_c = bc.cells_to_lonlat(cells)            # each cell's nucleus
coords, offsets = bc.cell_boundaries(cells)         # ragged rings: cell i is coords[offsets[i]:offsets[i+1]]
bc.cell_neighbours(cells)                           # (..., 4): up, right, down, left
bc.zoom_to(cells, 9, 7)                             # ancestors; to a finer level, (..., 9**k) descendants
bc.full_domain(3)                                   # every cell at a level: cids fill [9**(r+1), 15*9**r)
bc.Tree.from_cells(cells, 9).root_hex               # the fingerprint of any set of cells
```

Time has the same program (SPEC §9–§12): instants in, integer ticks and time cells out, exactly.

```python
from burin import time as bt

bt.ticks(times)                                     # datetime64 of any unit → uint64 ticks (1 µs by default)
bt.cells_from_times(times, level=40)                # the time cell containing each instant
start, end = bt.times_of_cells(cells_t)             # each cell's [start, end) as datetime64[us]
day = bt.tree([("2013-07-15", "2013-07-16")])       # a time set; day.root_hex is its fingerprint
bt.allen(bt.interval("2013-07-01", "2013-08-01"), bt.interval("2013-07-15", "2013-07-16"))   # 'contains'
t.holds(u, "within"); bc.verify_relation("within", t.root_hex, u.root_hex, t.relation_evidence(u, "within"))
```

## Rust

```rust
use burin_core::{fingerprint_geojson, Profile};
let root = fingerprint_geojson(&geojson, 10, &Profile::ogc())?;
```

## The rule, in one paragraph

A polygon's cells at resolution R are the depth-R rHEALPix cells whose **nucleus** (the inverse
projection of the planar cell centre) the polygon contains, boundary excluded. The set is held
in its coarsest form and hashed bottom-up: `leaf_empty = H(0x00)`, `leaf_full = H(0x01)`,
`node = H(0x02 ‖ c₀‖…‖c₈)`, `root = H(0x03 ‖ b₀‖…‖b₅)`; a constant subtree hashes to its ladder
value, so tiling does not matter. The profile identity is
`SHA-256("BURIN-PROFILE-1" ‖ hash id ‖ A ‖ B ‖ lon_0 in µdeg ‖ polar squares ‖ a in µm ‖ 1/f in nano ‖ tick ‖ epoch)`.
`N^-r` is the reciprocal of the exact integer `N^r`. See `SPEC.md`.

## Development

```bash
cargo test -p burin-core                      # parity fixtures + structural properties
uv run --no-project --with rhealpixdggs==0.8.6 --with shapely python tools/gen_fixtures.py   # regenerate set 1
cargo run -p burin-core --example freeze     # regenerate set 2 (golden roots; a wire-format bump)
uv venv && uv pip install maturin pytest && uv run maturin develop && uv run pytest
cd fuzz && cargo +nightly fuzz run opening_record corpus/opening_record seeds/opening_record   # any target in fuzz/
```

Which outputs are frozen and how versions move are in [STABILITY.md](STABILITY.md); what changed is in [CHANGELOG.md](CHANGELOG.md).
Space-time is drafted in [SPEC-time.md](SPEC-time.md) (not yet implemented).

`examples/central_park.py` puts space and time together end to end: a publisher fingerprints a
park and its opening hours, and a verifier holding only those 64 bytes checks whether a photo was
taken in the park while it was open.

