What's needed
A vectorized, numpy-in/numpy-out point-kind encoder on the geo2mort surface:
geo2mort(lats, lons, order=29, points=True) # -> np.ndarray[uint64], Kind::Point words
points=False (default): exactly today's behavior — area-kind words, byte-identical, fully backward compatible.
points=True: route through the kernel's existing point encoding (rust_ang2pix at order 29 → rust_mi_from_nested_point, i.e. Kind::Point via from_nested_point in src_rust/src/decimal_morton.rs) and return a plain contiguous uint64 ndarray — no pandas ExtensionArray wrapper.
- Point encoding is order-29-only, so
points=True with an explicit order != 29 should raise ValueError, matching MortonIndexArray.from_latlon's existing contract (mortie/morton_index.py).
- Null/sentinel semantics: same as the area path — inputs are dense float64 lat/lon with no null mask; non-finite lat/lon should fail loudly (or produce the documented sentinel
0, whichever the area path does today — the two paths must agree). 0 remains the only reserved word (empty/invalid), never a valid encode result.
- Tests to include: (a)
points=True words differ from area words at order 29 but clip2order coarsening is bit-identical at every order (this is the property zagg's grouping relies on); (b) a lone points=True word survives common_ancestor unchanged (kind preserved); (c) points=True, order != 29 raises; (d) dtype is uint64, C-contiguous, same shape as input.
Why (the hot-path numbers)
zagg's HealpixGrid.assign runs on every observation of every granule read (per-photon for ATL03 — millions of points per shard). It now needs point-kind words end-to-end (englacial/zagg#87: located t-digest channel via common_ancestor). The only public point encode today is the pandas ExtensionArray classmethod MortonIndexArray.from_latlon(..., points=True), and the public ways out of the wrapper are element-wise. Measured on 1M random points (mortie 0.8.4):
| path |
ms / 1M pts |
geo2mort (area) |
42.6 |
raw kernel rust_ang2pix + rust_mi_from_nested_point |
46.0 |
from_latlon(points=True) construction alone |
45.2 |
+ public unwrap (np.asarray / .to_numpy / .astype) |
~490–560 |
The kernel is at parity with the area encode (~8%); the ~12x penalty is entirely the ExtensionArray→ndarray unwrap. zagg currently works around it by reading the array's internal uint64 storage through its own boundary adapter (zagg.grids.morton.morton_words), which is fast (~70 ms total) but leans on a private attribute. A numpy-level geo2mort(..., points=True) gives the hot path a public vectorized surface and drops the wrapper entirely.
Consumer
englacial/zagg#87 / englacial/zagg#165 — HealpixGrid.assign swaps one call (from_latlon(points=True) + unwrap → geo2mort(..., points=True)) the moment this lands in a release; that swap is the PR's tracked phase 6.
What's needed
A vectorized, numpy-in/numpy-out point-kind encoder on the
geo2mortsurface:points=False(default): exactly today's behavior — area-kind words, byte-identical, fully backward compatible.points=True: route through the kernel's existing point encoding (rust_ang2pixat order 29 →rust_mi_from_nested_point, i.e.Kind::Pointviafrom_nested_pointinsrc_rust/src/decimal_morton.rs) and return a plain contiguousuint64ndarray — no pandas ExtensionArray wrapper.points=Truewith an explicitorder != 29should raiseValueError, matchingMortonIndexArray.from_latlon's existing contract (mortie/morton_index.py).0, whichever the area path does today — the two paths must agree).0remains the only reserved word (empty/invalid), never a valid encode result.points=Truewords differ from area words at order 29 butclip2ordercoarsening is bit-identical at every order (this is the property zagg's grouping relies on); (b) a lonepoints=Trueword survivescommon_ancestorunchanged (kind preserved); (c)points=True, order != 29raises; (d) dtype isuint64, C-contiguous, same shape as input.Why (the hot-path numbers)
zagg's
HealpixGrid.assignruns on every observation of every granule read (per-photon for ATL03 — millions of points per shard). It now needs point-kind words end-to-end (englacial/zagg#87: located t-digest channel viacommon_ancestor). The only public point encode today is the pandas ExtensionArray classmethodMortonIndexArray.from_latlon(..., points=True), and the public ways out of the wrapper are element-wise. Measured on 1M random points (mortie 0.8.4):geo2mort(area)rust_ang2pix+rust_mi_from_nested_pointfrom_latlon(points=True)construction alonenp.asarray/.to_numpy/.astype)The kernel is at parity with the area encode (~8%); the ~12x penalty is entirely the ExtensionArray→ndarray unwrap. zagg currently works around it by reading the array's internal
uint64storage through its own boundary adapter (zagg.grids.morton.morton_words), which is fast (~70 ms total) but leans on a private attribute. A numpy-levelgeo2mort(..., points=True)gives the hot path a public vectorized surface and drops the wrapper entirely.Consumer
englacial/zagg#87 / englacial/zagg#165 —
HealpixGrid.assignswaps one call (from_latlon(points=True)+ unwrap →geo2mort(..., points=True)) the moment this lands in a release; that swap is the PR's tracked phase 6.