mortie.coverage
Polygon-to-morton coverage. The MOC (multi-order coverage) set algebra over
the covers it produces lives in the MOC kernel; the MOC coverer's
public entry point, polygons_to_morton_mocs, lives in
mortie.batch (issues #170, #187 — the scalar
morton_coverage_moc retired with the plural batch names; the one-ring-set
multipart form is reached through from_geometry / from_wkb / from_wkt
with moc=True, or mortie.Moc).
The ring-validity checks below report whether any documented winding convention is in play for a ring before covering it — see Ring validity for the narrative.
Polygon-to-morton coverage.
Compute the set of morton indices at a given order that completely cover a polygon defined by lat/lon vertices. Supports single and multipart polygons.
Set algebra over the covers this module produces — union / intersection /
difference, the canonical compaction, the densify back to a flat single order —
lives in :mod:mortie._moc, split out by domain (issue #156). It is all
computed in Rust; there is no Python-level MOC set algebra in either module.
The batch-native MOC coverer,
:func:~mortie.batch.polygons_to_morton_mocs, lives in :mod:mortie.batch
(issue #170), and since issue #187 it is the operation's only public entry
point: the scalar morton_coverage_moc retired to the private
:func:_morton_coverage_moc kernel below, reached through
:func:mortie.from_geometry / :func:mortie.from_wkb with moc=True.
All three modules' names stay flat on the package
(mortie.morton_coverage, mortie.moc_and,
mortie.polygons_to_morton_mocs).
RingValidity = namedtuple('RingValidity', ['simple', 'identity_consistent'])
module-attribute
Both ring-validity verdicts; see :func:ring_validity.
morton_coverage(lats, lons, order=18, normalize=True, *, latitude='authalic')
Compute morton indices covering a polygon defined by lat/lon vertices.
Given a polygon (as arrays of vertex latitudes and longitudes), returns the set of morton indices at the requested HEALPix order that completely cover the polygon interior. The coverage is optimally compact — it includes all boundary cells plus every cell whose centre lies inside the polygon.
For multipart polygons and holes, pass lats and lons as lists of
rings. All rings are covered by a single even-odd descent: a cell is
covered iff its centre is inside an odd number of rings. So disjoint
outer rings are unioned (with no seam along shared interior borders), and a
ring nested inside another carves a hole (a donut is [outer, hole]).
Not batch vectorized: one polygon (ring-set) per call, covered to one cover.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lats
|
array_like or list of array_like
|
Vertex latitudes in degrees. For a single polygon, a 1-D array with at least 3 vertices. For multipart polygons, a list of such arrays. |
required |
lons
|
array_like or list of array_like
|
Vertex longitudes in degrees. Must match the structure of lats. |
required |
order
|
int
|
HEALPix depth / tessellation order (1–29). Default 18. |
18
|
normalize
|
bool
|
Auto-correct ring orientation at ingest. Default |
True
|
latitude
|
str
|
Latitude convention of the input vertices (issue #186):
|
'authalic'
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Sorted 1-D array of unique morton indices (dtype |
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than 3 vertices, mismatched lengths, invalid order, or coordinates containing NaN/infinity. |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If the returned flat cover exceeds ~1M cells. This is a best-effort,
post-hoc signal — it fires only after the cover is materialized, so
it does not prevent the blow-up: a flat cover's cell count grows as
|
Notes
- Self-intersecting polygons produce undefined results.
- Holes are supported via the multipart form: pass
[outer, hole, ...](even-odd nesting carves the holes). - Ring winding. The interior is the region to the left of each
directed edge. With
normalize=True(default) the winding you author does not matter: any simple ring decisively enclosing the larger region is reversed at ingest, so the smaller side is taken either way (S2's convention; issue #144 decision (A)). Author per RFC 7946 §3.1.6 (CCW exteriors, CW holes) or any other way — the RFC's CW-hole spelling is what ingest delivers, not what it requires. Withnormalize=Falsenothing is reordered, so you must wind every ring so its intended region lies to its left — for a carved hole that means counter-clockwise, like its exterior, not the RFC's clockwise. A ring wound the other way selects its complement, which inverts the even-odd fill: a CW hole undernormalize=Falsemakes the "donut" larger than its own outer ring. That same complement is the only way a lone ring covers a region larger than its complement. - The point-in-polygon test is a single robust spherical winding-number backend (issue #22): it is correct at any polygon size, including hemisphere-plus polygons, and degeneracy-free when an edge's great circle passes through a HEALPix cell centre (issue #11).
Examples:
Single polygon:
>>> import mortie
>>> lats = [40.0, 50.0, 45.0]
>>> lons = [-120.0, -120.0, -110.0]
>>> cells = mortie.morton_coverage(lats, lons, order=6)
Multipart polygon:
>>> lats_parts = [[40.0, 50.0, 45.0], [10.0, 20.0, 15.0]]
>>> lons_parts = [[-120.0, -120.0, -110.0], [-80.0, -80.0, -70.0]]
>>> cells = mortie.morton_coverage(lats_parts, lons_parts, order=6)
Source code in mortie/coverage.py
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | |
ring_validity(lats, lons, *, latitude='authalic')
Both validity verdicts for a ring -- whether any convention is in play.
simple is :func:ring_is_simple's verdict (no transversal
self-intersection); identity_consistent is True when no vertex
coordinate recurs at non-adjacent positions (a bit-exact pinch or a
revisited vertex makes the symbolic tie-break depend on vertex
numbering, so verdicts at the pinch can legitimately change under
rotation). Both True means every winding rule documented for
coverage is exact for this ring, with no convention in play.
Rings failing either check are still accepted by every coverage function -- the verdicts say which documented convention resolves the ambiguity, not that the cover is wrong.
Not batch vectorized: one ring per call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lats
|
array_like
|
Vertex latitudes / longitudes in degrees (single ring, open or closed). |
required |
lons
|
array_like
|
Vertex latitudes / longitudes in degrees (single ring, open or closed). |
required |
latitude
|
str
|
Latitude convention of the input vertices; see
:func: |
'authalic'
|
Returns:
| Type | Description |
|---|---|
RingValidity
|
Named tuple |
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer than 3 vertices, mismatched lengths, or coordinates containing NaN/infinity. |
Examples:
>>> import mortie
>>> mortie.ring_validity([0, 10, 10, 0], [0, 0, 10, 10])
RingValidity(simple=True, identity_consistent=True)
>>> mortie.ring_validity([0, 10, 0, 10], [0, 0, 10, 10]).simple
False
Source code in mortie/coverage.py
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 | |
ring_is_simple(lats, lons, *, latitude='authalic')
Whether this polygon ring is free of transversal self-intersections.
A ring whose edges cross has no single right-hand-rule interior, so
mortie falls back to a documented convention (the positively wound
region) and ingest normalization declines to trust the Gauss-Bonnet
turning sign fully. This check tells you before covering whether
that convention is in play: True means no two non-adjacent edges
cross transversally, and the winding rules read exactly on the
geometry as given.
It answers the crossing half of the question only. A bit-exact
pinch -- one coordinate repeated at non-adjacent positions -- is
resolved by the symbolic identity convention instead, whose verdict
can flip with where the vertex list starts (of 400 randomly pinched
rings, 17 flip under a rotation of their vertices). The full "is any
convention in play?" question therefore needs the second verdict too:
sphere::ring_set_identity_conflict, returned alongside this one by
the Rust sphere::ring_set_validity — or, from Python, the combined
:func:ring_validity, which returns both verdicts.
The check is the S2-architecture bucketed transversal-crossing test
(issue #145): edges are indexed into adaptively refined HEALPix cells
and only co-located, non-adjacent pairs are tested with the exact
crossing predicate -- no sweep line, O(V log V)-ish on realistic
rings. Consecutive duplicate vertices and a duplicated closing vertex
are handled exactly as coverage ingest handles them.
Not batch vectorized: one ring per call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lats
|
array_like
|
Vertex latitudes / longitudes in degrees (single ring, open or closed). |
required |
lons
|
array_like
|
Vertex latitudes / longitudes in degrees (single ring, open or closed). |
required |
latitude
|
str
|
Latitude convention of the input vertices; see
:func: |
'authalic'
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If lats and lons have different lengths, there are fewer than 3 vertices, or a coordinate is NaN/infinity. |
Examples:
>>> import mortie
>>> mortie.ring_is_simple([0, 10, 0, 10], [0, 0, 10, 10]) # a bowtie
False
>>> mortie.ring_is_simple([0, 10, 10, 0], [0, 0, 10, 10]) # untangled
True
Source code in mortie/coverage.py
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 | |