Skip to content

mortie.orders

Querying, changing and validating a packed word's HEALPix order, plus the resolution ladder those orders sit on. Split out of mortie.tools by domain (issue #159) so the Python surface mirrors the Rust tree (morton.rs); the dense batch kernel behind generate_morton_children's array form lives in mortie.batch as a private function (issues #170, #187). The names stay flat on the package (mortie.orders_of, mortie.clip2order).

HEALPix order query, change and validation over packed morton words.

Everything here reads, rewrites or validates a word's order: :func:orders_of and :func:orders_of_uniq decode it per element, :func:infer_order_from_morton collapses a uniform array to one, :func:validate_morton and :func:is_point check the word itself, :func:clip2order coarsens and :func:generate_morton_children refines, and :func:order2res / :func:res2display give the resolution ladder those orders sit on. The bulk refiner behind :func:generate_morton_children's array form lives in :mod:mortie.batch as the private kernel :func:~mortie.batch._children_of (issues #170, #187).

Split out of mortie.tools (issue #159) so the Python surface mirrors the Rust tree's own decomposition -- this module is the Python side of mortie-core/src/morton.rs (re-exported as mortie_rustie::morton). The names stay flat on the package (mortie.orders_of, mortie.clip2order): this module is where they live, not how they are spelled.

infer_order_from_morton(morton)

Infer the single HEALPix order of packed morton word(s).

Decodes through the packed-u64 kernel (issue #48): the order is carried in the word's suffix, not in any decimal-digit count. The return is one scalar order, so array input must be uniform-order; mixed-order input raises, naming the distinct orders (issue #116 — previously the first element's order was returned silently). For per-element orders of a mixed array use :func:orders_of.

Batch vectorized: array in, one order out — a reduction, not elementwise, so any input shape is accepted (issue #219). Empty is the one input with no answer to give, and it is refused by name rather than through an index error (see Raises).

Parameters:

Name Type Description Default
morton int or array - like

Packed morton word(s), all at one order. Must be non-empty.

required

Returns:

Type Description
int

The HEALPix order.

Raises:

Type Description
ValueError

If the words are at mixed orders, naming the distinct orders. Or if morton is empty, at any rank: the return is one order and an empty array has none, so unlike :func:validate_morton -- whose empty verdict is vacuously True by deliberate issue #187 design -- there is no vacuous answer to return here. Use :func:orders_of for a per-element (and so empty-safe) answer.

Source code in mortie/orders.py
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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def infer_order_from_morton(morton):
    """Infer the single HEALPix order of packed morton word(s).

    Decodes through the packed-u64 kernel (issue #48): the order is carried in
    the word's suffix, not in any decimal-digit count. The return is one
    scalar order, so array input must be uniform-order; mixed-order input
    raises, naming the distinct orders (issue #116 — previously the first
    element's order was returned silently). For per-element orders of a mixed
    array use :func:`orders_of`.

    **Batch vectorized**: array in, one order out — a reduction, not
    elementwise, so any input shape is accepted (issue #219).  Empty is the
    one input with no answer to give, and it is refused by name rather than
    through an index error (see Raises).

    Parameters
    ----------
    morton : int or array-like
        Packed morton word(s), all at one order.  Must be non-empty.

    Returns
    -------
    int
        The HEALPix order.

    Raises
    ------
    ValueError
        If the words are at mixed orders, naming the distinct orders.  Or if
        ``morton`` is empty, at any rank: the return is one order and an
        empty array has none, so unlike :func:`validate_morton` -- whose
        empty verdict is vacuously True by deliberate issue #187 design --
        there is no vacuous answer to return here.  Use :func:`orders_of`
        for a per-element (and so empty-safe) answer.
    """
    m = _as_u64(morton, "morton")
    if m.size == 0:
        raise ValueError(
            "empty morton array has no single order; use orders_of for "
            "per-element orders"
        )
    _, depths = _rust_mort2nested(np.ascontiguousarray(m.ravel()))
    distinct = np.unique(depths)
    if distinct.size > 1:
        raise ValueError(
            f"Mixed orders in morton array: {[int(d) for d in distinct]}; "
            "use orders_of for per-element orders"
        )
    return int(depths[0])

orders_of(morton)

Per-element HEALPix order of packed morton words.

Vectorized numpy decode of the 6-bit suffix (bits 5-0) per the spec page's suffix table (docs/specification.md §1):

  • suffix 0..=27 — variable-length area element; the order is the suffix value (0 = base-cell-only).
  • suffix 28..=47 — order-28/29 area cells in parent-first preorder suffix = 28 + t28*5 + (t29 present ? t29 + 1 : 0): each t28 owns a 5-block (the order-28 parent, then its four order-29 children), so (suffix - 28) % 5 == 0 is order 28 and everything else is order 29.
  • suffix 48..=63 — order-29 point (max-encoded, no area claim — spec §4); points are order 29 by definition.

Pure bit arithmetic — words are not validated (the empty sentinel 0 decodes as order 0; use :func:validate_morton to reject malformed words). This is the per-element, mixed-order-native counterpart of :func:infer_order_from_morton.

Batch vectorized: array in, array out, elementwise.

Parameters:

Name Type Description Default
morton int or array - like

Packed morton word(s) (uint64).

required

Returns:

Type Description
ndarray

uint8 order per element, 0-29 (scalar in -> length-1 ndarray, matching :func:geo2mort).

Source code in mortie/orders.py
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
def orders_of(morton):
    """Per-element HEALPix order of packed morton words.

    Vectorized numpy decode of the 6-bit suffix (bits 5-0) per the spec page's
    suffix table (``docs/specification.md`` §1):

    * suffix ``0..=27`` — variable-length area element; the order *is* the
      suffix value (``0`` = base-cell-only).
    * suffix ``28..=47`` — order-28/29 area cells in parent-first preorder
      ``suffix = 28 + t28*5 + (t29 present ? t29 + 1 : 0)``: each ``t28`` owns
      a 5-block (the order-28 parent, then its four order-29 children), so
      ``(suffix - 28) % 5 == 0`` is order 28 and everything else is order 29.
    * suffix ``48..=63`` — order-29 **point** (max-encoded, no area claim —
      spec §4); points are order 29 by definition.

    Pure bit arithmetic — words are not validated (the empty sentinel ``0``
    decodes as order 0; use :func:`validate_morton` to reject malformed
    words). This is the per-element, mixed-order-native counterpart of
    :func:`infer_order_from_morton`.

    **Batch vectorized**: array in, array out, elementwise.

    Parameters
    ----------
    morton : int or array-like
        Packed morton word(s) (``uint64``).

    Returns
    -------
    ndarray
        ``uint8`` order per element, 0-29 (scalar in -> length-1 ndarray,
        matching :func:`geo2mort`).
    """
    m = _as_u64(morton, "morton")
    suffix = (m & np.uint64(0x3F)).astype(np.uint8)
    # 0..=27: order == suffix. 28..=47: order-28 on the 5-block parent slots,
    # order 29 otherwise. 48..=63: order-29 point.
    orders = suffix.copy()
    band = (suffix >= 28) & (suffix <= 47)
    orders[band] = np.where((suffix[band] - 28) % 5 == 0, 28, 29)
    orders[suffix >= 48] = 29
    return orders

orders_of_uniq(uniq)

Per-element HEALPix order decoded from UNIQ cell numbers.

UNIQ is self-describing: uniq = 4 * 4**order + nested with 0 <= nested < 12 * 4**order, so order-k values occupy exactly [4**(k+1), 4**(k+2)) and consecutive orders tile that line without gaps. The order is therefore a pure function of the value — no caller-supplied order is needed and mixed-resolution input decodes element by element (issue #136).

Implemented as an exact integer bucket search rather than the log2(uniq / 4) // 2 form this module used previously: the float64 round-trip is not exact above ~2**53, so e.g. 4**30 - 1 (the last order-28 value) rounds up to 4**30 and mis-decodes as order 29.

The UNIQ counterpart of :func:orders_of, and mirrors its contract: per-element, mixed-order-native, uint8 out, scalar in -> length-1 ndarray. One deliberate difference: :func:orders_of is pure bit arithmetic and never validates, because every 6-bit morton suffix decodes to some order. UNIQ has no such total decode -- a value outside [4, 4**31) names no cell at any order -- so this raises instead of inventing an answer.

Batch vectorized: array in, array out, elementwise.

Parameters:

Name Type Description Default
uniq int or array - like

UNIQ encoded cell number(s).

required

Returns:

Type Description
ndarray

uint8 order per element, 0-MAX_ORDER (scalar in -> length-1 ndarray, matching :func:orders_of).

Raises:

Type Description
ValueError

If uniq is not integer-typed, or a value does not fit in int64 -- refused by name (issue #194) -- or if a value lies outside the UNIQ range for orders 0-MAX_ORDER.

Source code in mortie/orders.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
def orders_of_uniq(uniq):
    """Per-element HEALPix order decoded from UNIQ cell numbers.

    UNIQ is self-describing: ``uniq = 4 * 4**order + nested`` with
    ``0 <= nested < 12 * 4**order``, so order-``k`` values occupy exactly
    ``[4**(k+1), 4**(k+2))`` and consecutive orders tile that line without
    gaps. The order is therefore a pure function of the value — no
    caller-supplied order is needed and mixed-resolution input decodes element
    by element (issue #136).

    Implemented as an exact integer bucket search rather than the
    ``log2(uniq / 4) // 2`` form this module used previously: the float64
    round-trip is not exact above ~2**53, so e.g. ``4**30 - 1`` (the last
    order-28 value) rounds up to ``4**30`` and mis-decodes as order 29.

    The UNIQ counterpart of :func:`orders_of`, and mirrors its contract:
    per-element, mixed-order-native, ``uint8`` out, scalar in -> length-1
    ndarray. One deliberate difference: :func:`orders_of` is pure bit
    arithmetic and never validates, because every 6-bit morton suffix decodes
    to *some* order. UNIQ has no such total decode -- a value outside
    ``[4, 4**31)`` names no cell at any order -- so this raises instead of
    inventing an answer.

    **Batch vectorized**: array in, array out, elementwise.

    Parameters
    ----------
    uniq : int or array-like
        UNIQ encoded cell number(s).

    Returns
    -------
    ndarray
        ``uint8`` order per element, 0-``MAX_ORDER`` (scalar in -> length-1
        ndarray, matching :func:`orders_of`).

    Raises
    ------
    ValueError
        If ``uniq`` is not integer-typed, or a value does not fit in
        ``int64`` -- refused by name (issue #194) -- or if a value lies
        outside the UNIQ range for orders 0-``MAX_ORDER``.
    """
    # Strict intake, shared with the two callers below (`unique2parent` and
    # `uniq2geo` validate the same column before handing it here): float is
    # refused by dtype rather than by value, so the one UNIQ entry point that
    # still decoded an *integral* float -- `orders_of_uniq([16.0]) -> 1` while
    # `unique2parent([16.0])` refused -- now answers like the rest of the
    # family (issue #194 review).  Oversized values keep being named rather
    # than wrapping through the int64 cast or leaking numpy's OverflowError.
    u = _as_i64(uniq, "uniq")
    # bounds[k] = 4**(k+1) is the first UNIQ value of order k; the trailing
    # entry closes order MAX_ORDER's range (4**31 still fits int64).
    bounds = np.int64(4) ** np.arange(1, MAX_ORDER + 3, dtype=np.int64)
    orders = (np.searchsorted(bounds, u, side='right') - 1).astype(np.int64)
    bad = (orders < 0) | (orders > MAX_ORDER)
    if np.any(bad):
        raise ValueError(
            f"Not a valid UNIQ cell number for orders 0-{MAX_ORDER}: "
            f"{int(u[bad][0])}")
    return orders.astype(np.uint8)

is_point(morton)

Per-element point-kind predicate for packed morton words.

Kind is carried by the encoding itself (spec §4): suffix 0..=47 decodes as an area word, suffix 48..=63 as an order-29 point (a location with no area claim — docs/specification.md §1 suffix table). Pure bit arithmetic; words are not validated (see :func:validate_morton).

Batch vectorized: array in, array out, elementwise.

Parameters:

Name Type Description Default
morton int or array - like

Packed morton word(s) (uint64).

required

Returns:

Type Description
ndarray

bool per element, True for point words (scalar in -> length-1 ndarray, matching :func:geo2mort).

Source code in mortie/orders.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def is_point(morton):
    """Per-element point-kind predicate for packed morton words.

    Kind is carried by the encoding itself (spec §4): suffix ``0..=47``
    decodes as an **area** word, suffix ``48..=63`` as an order-29 **point**
    (a location with no area claim — ``docs/specification.md`` §1 suffix
    table). Pure bit arithmetic; words are not validated (see
    :func:`validate_morton`).

    **Batch vectorized**: array in, array out, elementwise.

    Parameters
    ----------
    morton : int or array-like
        Packed morton word(s) (``uint64``).

    Returns
    -------
    ndarray
        ``bool`` per element, True for point words (scalar in -> length-1
        ndarray, matching :func:`geo2mort`).
    """
    m = _as_u64(morton, "morton")
    return (m & np.uint64(0x3F)) >= np.uint64(48)

validate_morton(morton, order=None)

Validate that packed morton word(s) are well-formed.

The kernel decode rejects the empty sentinel (0) and any word with an invalid base-cell prefix; this also checks the decoded order matches order when one is supplied.

Batch vectorized (issue #187): array in, one verdict out — a reduction, since the answer is "every word is valid", and any offender raises. Any input shape is accepted; an offender in an N-D array is named by its flat C-order index (issue #219). The order check covers every element: it used to compare order against the first word alone, so a mixed-order array passed validation on the strength of its first element while the rest went unchecked (the decode itself has always run per element).

Parameters:

Name Type Description Default
morton int or array - like

Packed morton word(s) to validate.

required
order int

Expected HEALPix order -- one order, checked against every element. If None, no order check is made. Per-element expectations are not a thing here: a non-scalar order is refused rather than broadcast (use :func:orders_of and compare yourself).

None

Returns:

Type Description
bool

True if every word is a valid morton word. Empty in, True out, for any order: no word disagrees, so the reduction is vacuously true (see Notes).

Raises:

Type Description
ValueError

If morton is float-typed or negative -- refused by name rather than silently cast (issue #194). Or if a word does not decode -- the kernel's own refusal, which names no index and takes precedence over the order check, since the decode runs first and over the whole array. Or, past a clean decode, if any word's order disagrees with order -- that refusal names the lowest-index offender and its own order. The (word i of n) suffix is carried by every array form, length-1 included, and dropped only for a scalar (or 0-d) word: the rank of the input decides, not its size.

TypeError

If order is not a scalar. The per-element comparison the order check now makes would otherwise broadcast an array-valued order into an undocumented per-element expectation (and report the whole array as "expected"); the scalar comparison it replaced refused that input, so the guard keeps the refusal.

Notes

An empty input returns True whatever order says, including an order no word could have: both checks quantify over the words, and there are none. That is the numpy reduction reading (np.all([]) is True) and it is what keeps the batch form usable on a legal empty column -- a refusal there would make an empty partition an error rather than a no-op. Before issue #187 this raised IndexError from indexing depths[0], which was the accident of a first-element check rather than a verdict.

Source code in mortie/orders.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
def validate_morton(morton, order=None):
    """Validate that packed morton word(s) are well-formed.

    The kernel decode rejects the empty sentinel (0) and any word with an
    invalid base-cell prefix; this also checks the decoded order matches
    ``order`` when one is supplied.

    **Batch vectorized** (issue #187): array in, one verdict out — a
    reduction, since the answer is "every word is valid", and any offender
    raises.  Any input shape is accepted; an offender in an N-D array is
    named by its flat C-order index (issue #219).  The ``order`` check
    covers **every element**: it used to compare ``order`` against the
    *first* word alone, so a mixed-order array passed validation on the
    strength of its first element while the rest went unchecked (the decode
    itself has always run per element).

    Parameters
    ----------
    morton : int or array-like
        Packed morton word(s) to validate.
    order : int, optional
        Expected HEALPix order -- **one** order, checked against every element.
        If None, no order check is made. Per-element expectations are not a
        thing here: a non-scalar ``order`` is refused rather than broadcast
        (use :func:`orders_of` and compare yourself).

    Returns
    -------
    bool
        True if every word is a valid morton word.  **Empty in, True out**,
        for any ``order``: no word disagrees, so the reduction is vacuously
        true (see Notes).

    Raises
    ------
    ValueError
        If ``morton`` is float-typed or negative -- refused by name rather
        than silently cast (issue #194).  Or if a word does not decode --
        the kernel's own refusal, which names no
        index and **takes precedence** over the order check, since the decode
        runs first and over the whole array.  Or, past a clean decode, if any
        word's order disagrees with ``order`` -- that refusal names the
        **lowest-index** offender and its own order.  The ``(word i of n)``
        suffix is carried by every array form, length-1 included, and dropped
        only for a scalar (or 0-d) word: the rank of the input decides, not
        its size.
    TypeError
        If ``order`` is not a scalar.  The per-element comparison the ``order``
        check now makes would otherwise broadcast an array-valued ``order``
        into an undocumented per-element expectation (and report the whole
        array as "expected"); the scalar comparison it replaced refused that
        input, so the guard keeps the refusal.

    Notes
    -----
    An empty input returns ``True`` whatever ``order`` says, including an
    order no word could have: both checks quantify over the words, and there
    are none.  That is the numpy reduction reading (``np.all([])`` is
    ``True``) and it is what keeps the batch form usable on a legal empty
    column -- a refusal there would make an empty partition an error rather
    than a no-op.  Before issue #187 this raised ``IndexError`` from indexing
    ``depths[0]``, which was the accident of a first-element check rather
    than a verdict.
    """
    if order is not None and np.ndim(order) != 0:
        raise TypeError(
            f"order must be a single int, not {type(order).__name__} of shape "
            f"{np.shape(order)}; validate_morton checks one order against "
            "every word (use orders_of for per-element orders)"
        )
    # Rank of the *input*, read before coercion -- a length-1 array is an
    # array, so it is indexed like one in the message (issue #187, the same
    # rule norm2mort / mort2norm follow for their return form).
    is_scalar = np.ndim(morton) == 0
    m = _as_u64(morton, "morton")
    # The kernel raises ValueError on the empty sentinel / an invalid prefix.
    _, depths = _rust_mort2nested(np.ascontiguousarray(m.ravel()))
    if order is not None:
        bad = np.flatnonzero(depths != order)
        if bad.size:
            i = int(bad[0])
            where = "" if is_scalar else f" (word {i} of {m.size})"
            raise ValueError(
                f"Morton word decodes to order {int(depths[i])}, expected "
                f"{order}{where}"
            )
    return True

clip2order(clip_order, midx)

Coarsen packed morton words to a lower resolution.

Degrades each packed word to clip_order by coarsening it through the kernel (the inverse of refining): the base cell and the first clip_order tuples are kept, finer detail is dropped, and the suffix is rewritten. Words already at or below clip_order are returned unchanged.

The print_factor flag was removed for the 1.x freeze (issue #68). It returned 18 - clip_order, a level count anchored to the retired decimal encoding's order-18 ceiling, so it went negative for the order-19..29 words this package now encodes. There is no replacement: the levels a word actually drops is order - clip_order for its own decoded order, which :func:orders_of gives directly.

Batch vectorized: array in, array out, elementwise (one shared clip_order). N-D input keeps its shape (issue #219); it used to be silently flattened to 1-D.

Parameters:

Name Type Description Default
clip_order int

HEALPix order to degrade to.

required
midx array-like of int

Packed morton words (see :func:res2display for approximate resolutions).

required

Returns:

Type Description
ndarray

Coarsened packed words, one per input word, in the input's shape (scalar in -> length-1).

Source code in mortie/orders.py
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
def clip2order(clip_order, midx):
    """Coarsen packed morton words to a lower resolution.

    Degrades each packed word to ``clip_order`` by coarsening it through the
    kernel (the inverse of refining): the base cell and the first ``clip_order``
    tuples are kept, finer detail is dropped, and the suffix is rewritten. Words
    already at or below ``clip_order`` are returned unchanged.

    The ``print_factor`` flag was removed for the 1.x freeze (issue #68). It
    returned ``18 - clip_order``, a level count anchored to the retired
    decimal encoding's order-18 ceiling, so it went negative for the
    order-19..29 words this package now encodes. There is no replacement: the
    levels a word actually drops is ``order - clip_order`` for its own decoded
    order, which :func:`orders_of` gives directly.

    **Batch vectorized**: array in, array out, elementwise (one shared
    ``clip_order``).  N-D input keeps its shape (issue #219); it used to be
    silently flattened to 1-D.

    Parameters
    ----------
    clip_order : int
        HEALPix order to degrade to.
    midx : array-like of int
        Packed morton words (see :func:`res2display` for approximate resolutions).

    Returns
    -------
    ndarray
        Coarsened packed words, one per input word, in the input's shape
        (scalar in -> length-1).
    """
    midx = _as_u64(midx, "midx")
    out = _rustie.rust_mi_coarsen(np.ascontiguousarray(midx.ravel()), int(clip_order))
    return out.reshape(midx.shape)

generate_morton_children(parent_morton, target_order, *, max_cells=None)

Generate all child morton indices at a target order.

Batch vectorized (issue #187), with numpy semantics: a scalar parent gives the 1-D (4**d,) block of its children, and an array of parents gives the dense (n, 4**d) matrix — one row per parent, in input order. Every parent in an array must sit at one shared order, which is what makes the result dense rather than ragged. The array form is new behaviour, not a re-spelling: an array used to be coerced through np.uint64 and describe only its first element whenever target_order was finer than the parents' order (at equal order it happened to return the parents themselves), so a caller who was passing one gets a corrected answer of a different shape. Only 1-D is accepted — a higher-rank array raises rather than flattening, since a flattened result could not be indexed back.

Parameters:

Name Type Description Default
parent_morton int or array_like

Parent packed morton word, or a 1-D array of them (all at one order).

required
target_order int

Target order for children (must be >= parent order).

required
max_cells int or None

Budget on the total children produced, refused pre-emptively in both forms (4**d for a scalar parent, n * 4**d for an array). None (default) is unbudgeted.

None

Returns:

Name Type Description
children ndarray

Array of child packed morton words at target_order. If target_order equals parent_order, returns array with parent_morton. For array input, the (n, 4**d) matrix of every parent's children.

Raises:

Type Description
ValueError

If target_order is coarser than the parent word's own order, or (array form) the parents do not share one order or the result would exceed max_cells. A float-typed or negative parent_morton is refused by name (issue #194), never silently cast.

See Also

mortie.batch._children_of : the dense batch kernel the array form delegates to.

Notes

Children are generated in HEALPix NESTED space — descending level_diff orders multiplies the cell count by 4**level_diff — then packed back to morton words via the kernel. If already at target_order, returns the parent itself.

Source code in mortie/orders.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
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
546
547
548
549
550
551
552
553
554
555
556
557
558
def generate_morton_children(parent_morton, target_order, *, max_cells=None):
    """Generate all child morton indices at a target order.

    **Batch vectorized** (issue #187), with numpy semantics: a scalar parent
    gives the 1-D ``(4**d,)`` block of its children, and an *array* of parents
    gives the dense ``(n, 4**d)`` matrix — one row per parent, in input order.
    Every parent in an array must sit at one shared order, which is what makes
    the result dense rather than ragged.  The array form is **new behaviour,
    not a re-spelling**: an array used to be coerced through ``np.uint64`` and
    describe only its first element whenever ``target_order`` was finer than
    the parents' order (at equal order it happened to return the parents
    themselves), so a caller who was passing one gets a corrected answer of a
    different shape.  Only 1-D is accepted — a higher-rank array raises rather
    than flattening, since a flattened result could not be indexed back.

    Parameters
    ----------
    parent_morton : int or array_like
        Parent packed morton word, or a 1-D array of them (all at one order).
    target_order : int
        Target order for children (must be >= parent order).
    max_cells : int or None, optional
        Budget on the total children produced, refused pre-emptively in both
        forms (``4**d`` for a scalar parent, ``n * 4**d`` for an array).
        ``None`` (default) is unbudgeted.

    Returns
    -------
    children : ndarray
        Array of child packed morton words at target_order.
        If target_order equals parent_order, returns array with parent_morton.
        For array input, the ``(n, 4**d)`` matrix of every parent's children.

    Raises
    ------
    ValueError
        If ``target_order`` is coarser than the parent word's own order, or
        (array form) the parents do not share one order or the result would
        exceed ``max_cells``.  A float-typed or negative ``parent_morton``
        is refused by name (issue #194), never silently cast.

    See Also
    --------
    mortie.batch._children_of : the dense batch kernel the array form
        delegates to.

    Notes
    -----
    Children are generated in HEALPix NESTED space — descending ``level_diff``
    orders multiplies the cell count by ``4**level_diff`` — then packed back to
    morton words via the kernel. If already at target_order, returns the parent
    itself.
    """
    if np.ndim(parent_morton) > 1:
        raise ValueError(
            f"parent_morton must be a scalar or 1-D, got "
            f"{np.ndim(parent_morton)}-D"
        )
    if np.ndim(parent_morton) > 0:
        try:
            # Name the caller-facing parameter before delegating -- the
            # kernel's own pass stays as the backstop and sees uint64.
            return _children_of(_as_u64(parent_morton, "parent_morton"),
                                target_order, max_cells=max_cells)
        except ValueError as exc:
            # The kernel's refusals predate the plural name's retirement
            # (issue #187); re-raise naming the surviving entry point.  Same
            # type, same text otherwise, so handlers keep working.
            raise ValueError(
                str(exc).replace("children_of", "generate_morton_children")
            ) from None
    # Decode the parent to its (nested, depth) via the packed kernel.
    parent_morton = _as_u64(parent_morton, "parent_morton")[0]
    nested, depths = _rust_mort2nested(
        np.ascontiguousarray(np.atleast_1d(parent_morton))
    )
    parent_order = int(depths[0])
    parent_nested = int(nested[0])

    if target_order < parent_order:
        raise ValueError(
            f"target_order ({target_order}) must be >= parent_order ({parent_order})"
        )

    if max_cells is not None and 4 ** (target_order - parent_order) > max_cells:
        raise ValueError(
            f"generate_morton_children would produce "
            f"{4 ** (target_order - parent_order)} children at order "
            f"{target_order}, exceeding max_cells={max_cells}"
        )

    if target_order == parent_order:
        return np.array([parent_morton], dtype=np.uint64)

    level_diff = target_order - parent_order
    # In NESTED space a cell's descendants at `target_order` are the contiguous
    # block `nested * 4**level_diff + [0 .. 4**level_diff)`.
    span = 4 ** level_diff
    child_nested = (parent_nested << (2 * level_diff)) + np.arange(
        span, dtype=np.uint64
    )
    depths = np.full(span, target_order, dtype=np.uint8)
    return _rust_nested2mort(np.ascontiguousarray(child_nested), depths)

order2res(order)

Approximate cell scale (km) at a HEALPix tessellation order.

The exact RMS cell spacing on the mean-radius HEALPix sphere: every order-k cell has identical area 4*pi*R**2 / (12 * 4**order) (HEALPix is equal-area), and the cell scale is the square root of that area. Derived from :data:EARTH_RADIUS_KM so code and the spec page (§3) share one Earth model (issue #119).

order may be a scalar (returns a float) or an array of orders such as :func:orders_of yields (returns an ndarray).

Batch vectorized: array in, array out, elementwise.

Parameters:

Name Type Description Default
order int or array - like

HEALPix tessellation order(s).

required

Returns:

Type Description
float or ndarray

Approximate cell scale in kilometres (scalar in -> float out, array in -> ndarray out).

See Also

res2display : the same ladder as display-ready records, order by order.

Source code in mortie/orders.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def order2res(order):
    """Approximate cell scale (km) at a HEALPix tessellation ``order``.

    The exact RMS cell spacing on the mean-radius HEALPix sphere: every
    order-k cell has identical area ``4*pi*R**2 / (12 * 4**order)`` (HEALPix is
    equal-area), and the cell scale is the square root of that area. Derived
    from :data:`EARTH_RADIUS_KM` so code and the spec page (§3) share one Earth
    model (issue #119).

    ``order`` may be a scalar (returns a ``float``) or an array of orders such
    as :func:`orders_of` yields (returns an ``ndarray``).

    **Batch vectorized**: array in, array out, elementwise.

    Parameters
    ----------
    order : int or array-like
        HEALPix tessellation order(s).

    Returns
    -------
    float or ndarray
        Approximate cell scale in kilometres (scalar in -> ``float`` out,
        array in -> ``ndarray`` out).

    See Also
    --------
    res2display : the same ladder as display-ready records, order by order.
    """
    # Exponentiate in float so 4**order does not overflow an integer dtype at
    # high orders (an ``orders_of`` uint8 array wraps 4**29 to 0 -> div-by-zero).
    order = np.asarray(order, dtype=np.float64)
    area = 4 * np.pi * EARTH_RADIUS_KM**2 / (12 * 4.0**order)  # km2
    res = np.sqrt(area)
    return float(res) if res.ndim == 0 else res

res2display(max_order=MAX_ORDER)

Resolution ladder for tessellation orders 0 through max_order.

Returns one record per order rather than printing (issue #68): each resolution is expressed in the largest sensible unit -- km at coarse orders, m once it drops below 1 km, cm once it drops below 1 m -- rounded to three decimals within that bracket, so fine orders read naturally (order 12 -> 1.592 km, order 13 -> 795.852 m) rather than as tiny km fractions.

Not batch vectorized: one ladder per call.

Parameters:

Name Type Description Default
max_order int

Highest order to include, inclusive. Must lie in 0..MAX_ORDER (default MAX_ORDER = 29, the finest order the packed-u64 kernel encodes).

MAX_ORDER

Returns:

Type Description
list of ResolutionLevel

One named tuple (order, value, unit, km) per order, in ascending order. value/unit are the display pair; km is the unrounded resolution in kilometres for further arithmetic.

Raises:

Type Description
ValueError

If max_order lies outside 0..MAX_ORDER.

See Also

order2res : the raw kilometres for a single order.

Examples:

>>> from mortie import res2display
>>> levels = res2display(max_order=3)
>>> levels[0].order, levels[0].unit
(0, 'km')
>>> for lvl in res2display(max_order=2):
...     print(f"{lvl.value} {lvl.unit} at tessellation order {lvl.order}")
...
Source code in mortie/orders.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def res2display(max_order=MAX_ORDER):
    """Resolution ladder for tessellation orders 0 through ``max_order``.

    Returns one record per order rather than printing (issue #68): each
    resolution is expressed in the largest sensible unit -- km at coarse
    orders, m once it drops below 1 km, cm once it drops below 1 m --
    rounded to three decimals within that bracket, so fine orders read
    naturally (order 12 -> ``1.592 km``, order 13 -> ``795.852 m``) rather
    than as tiny km fractions.

    **Not batch vectorized**: one ladder per call.

    Parameters
    ----------
    max_order : int, optional
        Highest order to include, inclusive. Must lie in ``0..MAX_ORDER``
        (default ``MAX_ORDER`` = 29, the finest order the packed-u64
        kernel encodes).

    Returns
    -------
    list of ResolutionLevel
        One named tuple ``(order, value, unit, km)`` per order, in
        ascending order. ``value``/``unit`` are the display pair; ``km``
        is the unrounded resolution in kilometres for further arithmetic.

    Raises
    ------
    ValueError
        If ``max_order`` lies outside ``0..MAX_ORDER``.

    See Also
    --------
    order2res : the raw kilometres for a single order.

    Examples
    --------
    >>> from mortie import res2display
    >>> levels = res2display(max_order=3)
    >>> levels[0].order, levels[0].unit
    (0, 'km')
    >>> for lvl in res2display(max_order=2):
    ...     print(f"{lvl.value} {lvl.unit} at tessellation order {lvl.order}")
    ... # doctest: +SKIP
    """
    if not 0 <= max_order <= MAX_ORDER:
        raise ValueError(
            f"max_order must be between 0 and {MAX_ORDER}, got {max_order!r}")
    levels = []
    for res in range(max_order + 1):
        km = order2res(res)
        if km >= 1.0:
            value, unit = km, 'km'
        elif km >= 1e-3:
            value, unit = km * 1e3, 'm'
        else:
            value, unit = km * 1e5, 'cm'
        levels.append(ResolutionLevel(res, round(value, 3), unit, km))
    return levels