Skip to content

mortie.moc

The MOC (multi-order coverage) algebra over morton sets: compaction, densify, boolean set ops, and the ancestry reductions. Split out of mortie.coverage by domain (issue #156); the ragged batch twins — mocs_to_orders, common_ancestors, and the 1×N set-op broadcast mocs_and / mocs_intersect — live in mortie.batch (issues #170, #173). The names stay flat on the package (mortie.moc_to_order, mortie.mocs_to_orders).

Multi-Order Coverage (MOC) algebra over morton sets.

A mortie MOC is just a uint64 array of packed morton words at mixed orders — the word self-encodes its own order and ancestry — so every operation here is an array in, array out. :func:compress_moc is the canonical compaction, :func:moc_to_order densifies back to a flat single-order list, :func:moc_or / :func:moc_and / :func:moc_minus / :func:moc_xor are the healpix-crate BMOC set algebra, :func:moc_intersects the intersection predicate (no BMOC build, no materialized result), :func:moc_not its domain-bounded complement, and :func:common_ancestor / :func:split_base_cells the ancestry reductions. All of it is computed in Rust — there is no Python-level MOC set algebra.

Split out of :mod:mortie.coverage by domain (issue #156): MOC algebra against polygon coverage. The ragged batch twins of the operators here — :func:~mortie.batch.mocs_to_orders for :func:moc_to_order and :func:~mortie.batch.common_ancestors for :func:common_ancestor — are split off again by arity into :mod:mortie.batch (issue #170), where every bulk operator outside the pyarrow skin now sits — :func:mortie.arrow.from_wkbs and :func:mortie.arrow.polygons_to_morton_mocs are bulk operators too, and stay in :mod:mortie.arrow (issue #154). The names stay flat on the package (mortie.moc_to_order, mortie.mocs_to_orders): the module is where they live, not how they are spelled.

moc_min = common_ancestor module-attribute

compress_moc(morton)

Compress a morton set into its canonical compact MOC.

Merges any 4 complete sibling cells into their parent (repeatedly) and drops any cell already contained in a coarser one. Use after unioning covers from several polygons / parts so that sibling groups spanning the seams collapse.

Parameters:

Name Type Description Default
morton array_like

Morton indices (mixed order allowed).

required

Returns:

Type Description
ndarray

Sorted, compacted morton indices (uint64).

Source code in mortie/moc.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def compress_moc(morton):
    """Compress a morton set into its canonical compact MOC.

    Merges any 4 complete sibling cells into their parent (repeatedly) and drops
    any cell already contained in a coarser one.  Use after unioning covers from
    several polygons / parts so that sibling groups spanning the seams collapse.

    Parameters
    ----------
    morton : array_like
        Morton indices (mixed order allowed).

    Returns
    -------
    numpy.ndarray
        Sorted, compacted morton indices (``uint64``).
    """
    morton = np.asarray(morton, dtype=np.uint64).ravel()
    return np.asarray(_rustie.rust_moc_normalize(morton))

moc_to_order(morton, order, max_cells=_FLAT_COVER_WARN_THRESHOLD)

Densify a (mixed-order) morton set to a flat list at order.

Unlike :func:morton_coverage's post-hoc warning, the densify path can over-allocate to the point of OOM before any warning is reachable — a tiny compact MOC densifies to Σ 4**(order - depth) flat cells (issue #80). So this guards pre-emptively: an upper bound on the densified count is computed from the input set alone (an O(n) pass, no flat allocation) and, when it exceeds max_cells, a :class:ValueError is raised before materializing. The bound is exact unless morton holds cells finer than order (which coarsen and dedup on densify), where it is a safe over-count — so the guard never lets more than max_cells cells through.

order is range-checked here, in the wrapper, for the same reason. The kernel's densify shift is only defined over 0-29; an out-of-range order reaches it as a Rust panic, surfacing as pyo3_runtime.PanicException, which derives from :class:BaseException — so neither except ValueError nor except Exception catches it. The budget does not screen it either: the estimate's 1 << (2 * (order - depth)) wraps mod 64 in a release build, so for depth-6 input the whole band order 38-48 estimates under the default budget and passes through to the panic. Refusing with the :class:ValueError this contract already promises keeps it catchable by the handlers consumers already have (issue #108).

Parameters:

Name Type Description Default
morton array_like

Morton indices (mixed order allowed).

required
order int

Target HEALPix order (0-29) to densify to.

required
max_cells int or None

Pre-emptive budget on the densified flat cell count. Raises :class:ValueError if the estimate exceeds it (default 1 << 20 — the same ~1M-cell line as the flat-cover warning). Pass None to opt out and densify unconditionally.

_FLAT_COVER_WARN_THRESHOLD

Returns:

Type Description
ndarray

Sorted 1-D array of flat morton indices at order (uint64).

Raises:

Type Description
ValueError

If order is outside 0-29, or the estimated densified count exceeds max_cells.

See Also

morton_coverage : flat single-order cover (post-hoc large-cover warning). mortie.batch.mocs_to_orders : the ragged batch form (many MOCs in one call).

Source code in mortie/moc.py
 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
 83
 84
 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
def moc_to_order(morton, order, max_cells=_FLAT_COVER_WARN_THRESHOLD):
    """Densify a (mixed-order) morton set to a flat list at ``order``.

    Unlike :func:`morton_coverage`'s post-hoc warning, the densify path can
    over-allocate to the point of OOM before any warning is reachable — a tiny
    compact MOC densifies to ``Σ 4**(order - depth)`` flat cells (issue #80).
    So this guards **pre-emptively**: an upper bound on the densified count is
    computed from the input set alone (an O(n) pass, no flat allocation) and,
    when it exceeds ``max_cells``, a :class:`ValueError` is raised *before*
    materializing.  The bound is exact unless ``morton`` holds cells finer than
    ``order`` (which coarsen and dedup on densify), where it is a safe over-count
    — so the guard never lets more than ``max_cells`` cells through.

    ``order`` is range-checked here, in the wrapper, for the same reason.  The
    kernel's densify shift is only defined over 0-29; an out-of-range order
    reaches it as a Rust panic, surfacing as ``pyo3_runtime.PanicException``,
    which derives from :class:`BaseException` — so neither ``except ValueError``
    nor ``except Exception`` catches it.  The budget does not screen it either:
    the estimate's ``1 << (2 * (order - depth))`` wraps mod 64 in a release
    build, so for depth-6 input the whole band ``order`` 38-48 estimates *under*
    the default budget and passes through to the panic.  Refusing with the
    :class:`ValueError` this contract already promises keeps it catchable by the
    handlers consumers already have (issue #108).

    Parameters
    ----------
    morton : array_like
        Morton indices (mixed order allowed).
    order : int
        Target HEALPix order (0-29) to densify to.
    max_cells : int or None, optional
        Pre-emptive budget on the densified flat cell count.  Raises
        :class:`ValueError` if the estimate exceeds it (default
        ``1 << 20`` — the same ~1M-cell line as the flat-cover warning).  Pass
        ``None`` to opt out and densify unconditionally.

    Returns
    -------
    numpy.ndarray
        Sorted 1-D array of flat morton indices at ``order`` (``uint64``).

    Raises
    ------
    ValueError
        If ``order`` is outside 0-29, or the estimated densified count exceeds
        ``max_cells``.

    See Also
    --------
    morton_coverage : flat single-order cover (post-hoc large-cover warning).
    mortie.batch.mocs_to_orders : the ragged batch form (many MOCs in one call).
    """
    morton = np.asarray(morton, dtype=np.uint64).ravel()
    if not 0 <= order <= 29:
        raise ValueError(f"Order must be between 0 and 29, got {order}")
    if max_cells is not None:
        estimated = int(_rustie.rust_moc_to_order_count(morton, order))
        if estimated > max_cells:
            raise ValueError(
                f"moc_to_order would densify to ~{estimated} cells at order "
                f"{order}, exceeding max_cells={max_cells}. Pass a larger "
                f"max_cells, or max_cells=None to proceed (risking OOM), or "
                f"densify to a coarser order."
            )
    return np.asarray(_rustie.rust_moc_to_order(morton, order))

moc_or(a, b)

Union of two morton covers (the cells in a or b).

Equivalent to compress_moc(concatenate([a, b])), but computed by the healpix-crate BMOC or rather than a concatenate-then-compress pass.

Parameters:

Name Type Description Default
a array_like

Morton covers (mixed order allowed).

required
b array_like

Morton covers (mixed order allowed).

required

Returns:

Type Description
ndarray

Sorted, compacted union (uint64).

See Also

moc_and : intersection of two covers. moc_minus : difference a \ b. compress_moc : moc_or(a, b) == compress_moc(concatenate([a, b])).

Source code in mortie/moc.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def moc_or(a, b):
    r"""Union of two morton covers (the cells in ``a`` or ``b``).

    Equivalent to ``compress_moc(concatenate([a, b]))``, but computed by the
    healpix-crate BMOC ``or`` rather than a concatenate-then-compress pass.

    Parameters
    ----------
    a, b : array_like
        Morton covers (mixed order allowed).

    Returns
    -------
    numpy.ndarray
        Sorted, compacted union (``uint64``).

    See Also
    --------
    moc_and : intersection of two covers.
    moc_minus : difference ``a \ b``.
    compress_moc : ``moc_or(a, b) == compress_moc(concatenate([a, b]))``.
    """
    a = np.asarray(a, dtype=np.uint64).ravel()
    b = np.asarray(b, dtype=np.uint64).ravel()
    return np.asarray(_rustie.rust_moc_or(a, b))

moc_and(a, b)

Intersection of two morton covers (the cells in both a and b).

Parameters:

Name Type Description Default
a array_like

Morton covers (mixed order allowed).

required
b array_like

Morton covers (mixed order allowed).

required

Returns:

Type Description
ndarray

Sorted, compacted intersection (uint64).

See Also

moc_or : union of two covers. moc_minus : difference a \ b. moc_intersects : tests for overlap without materializing this result. mortie.batch.mocs_and : the 1 x N broadcast form (one shared cover against many ragged MOCs).

Source code in mortie/moc.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def moc_and(a, b):
    r"""Intersection of two morton covers (the cells in both ``a`` and ``b``).

    Parameters
    ----------
    a, b : array_like
        Morton covers (mixed order allowed).

    Returns
    -------
    numpy.ndarray
        Sorted, compacted intersection (``uint64``).

    See Also
    --------
    moc_or : union of two covers.
    moc_minus : difference ``a \ b``.
    moc_intersects : tests for overlap without materializing this result.
    mortie.batch.mocs_and : the 1 x N broadcast form (one shared cover
        against many ragged MOCs).
    """
    a = np.asarray(a, dtype=np.uint64).ravel()
    b = np.asarray(b, dtype=np.uint64).ravel()
    return np.asarray(_rustie.rust_moc_and(a, b))

moc_intersects(a, b)

Whether two morton covers intersect (share any area at any order).

The predicate twin of :func:moc_and (issue #173): moc_intersects(a, b) equals moc_and(a, b).size > 0, but materializes no intersection — both covers are normalized (the only allocation) and walked as sorted disjoint ranges, exiting on the first overlap. It is compaction-safe by construction: it tests geometric overlap, never identity against a compacted cover, so a dense region that compacts to its parent still answers True for any cell inside it.

Parameters:

Name Type Description Default
a array_like

Morton covers (mixed order allowed).

required
b array_like

Morton covers (mixed order allowed).

required

Returns:

Type Description
bool

True if the two covers share any area.

See Also

moc_and : materializes the intersection this only tests. mortie.batch.mocs_intersect : the 1 x N broadcast form (one shared cover against many ragged MOCs).

Source code in mortie/moc.py
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 moc_intersects(a, b):
    """Whether two morton covers intersect (share any area at any order).

    The predicate twin of :func:`moc_and` (issue #173): ``moc_intersects(a, b)``
    equals ``moc_and(a, b).size > 0``, but materializes no intersection — both
    covers are normalized (the only allocation) and walked as sorted disjoint
    ranges, exiting on the first overlap.  It is compaction-safe by construction: it tests geometric
    overlap, never identity against a compacted cover, so a dense region that
    compacts to its parent still answers ``True`` for any cell inside it.

    Parameters
    ----------
    a, b : array_like
        Morton covers (mixed order allowed).

    Returns
    -------
    bool
        ``True`` if the two covers share any area.

    See Also
    --------
    moc_and : materializes the intersection this only tests.
    mortie.batch.mocs_intersect : the 1 x N broadcast form (one shared cover
        against many ragged MOCs).
    """
    a = np.asarray(a, dtype=np.uint64).ravel()
    b = np.asarray(b, dtype=np.uint64).ravel()
    return bool(_rustie.rust_moc_intersects(a, b))

moc_minus(a, b)

Difference of two morton covers (the cells in a but not b).

Computes a \ b.

Parameters:

Name Type Description Default
a array_like

Morton covers (mixed order allowed).

required
b array_like

Morton covers (mixed order allowed).

required

Returns:

Type Description
ndarray

Sorted, compacted difference (uint64).

See Also

moc_or : union of two covers. moc_and : intersection of two covers.

Source code in mortie/moc.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def moc_minus(a, b):
    r"""Difference of two morton covers (the cells in ``a`` but not ``b``).

    Computes ``a \ b``.

    Parameters
    ----------
    a, b : array_like
        Morton covers (mixed order allowed).

    Returns
    -------
    numpy.ndarray
        Sorted, compacted difference (``uint64``).

    See Also
    --------
    moc_or : union of two covers.
    moc_and : intersection of two covers.
    """
    a = np.asarray(a, dtype=np.uint64).ravel()
    b = np.asarray(b, dtype=np.uint64).ravel()
    return np.asarray(_rustie.rust_moc_minus(a, b))

moc_xor(a, b)

Symmetric difference of two morton covers (cells in exactly one).

Computes a △ b — the cells in a or b but not both, i.e. moc_minus(moc_or(a, b), moc_and(a, b)). Useful for "what changed" between two coverages: against an earlier cover a and a later cover b, moc_xor is exactly the cells that gained or lost coverage.

Parameters:

Name Type Description Default
a array_like

Morton covers (mixed order allowed).

required
b array_like

Morton covers (mixed order allowed).

required

Returns:

Type Description
ndarray

Sorted, compacted symmetric difference (uint64).

See Also

moc_or : union of two covers. moc_and : intersection of two covers. moc_minus : difference a \ b (the directional half of xor).

Source code in mortie/moc.py
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
def moc_xor(a, b):
    r"""Symmetric difference of two morton covers (cells in exactly one).

    Computes ``a △ b`` — the cells in ``a`` or ``b`` but not both, i.e.
    ``moc_minus(moc_or(a, b), moc_and(a, b))``.  Useful for "what changed"
    between two coverages: against an earlier cover ``a`` and a later cover
    ``b``, ``moc_xor`` is exactly the cells that gained *or* lost coverage.

    Parameters
    ----------
    a, b : array_like
        Morton covers (mixed order allowed).

    Returns
    -------
    numpy.ndarray
        Sorted, compacted symmetric difference (``uint64``).

    See Also
    --------
    moc_or : union of two covers.
    moc_and : intersection of two covers.
    moc_minus : difference ``a \ b`` (the directional half of ``xor``).
    """
    a = np.asarray(a, dtype=np.uint64).ravel()
    b = np.asarray(b, dtype=np.uint64).ravel()
    return np.asarray(_rustie.rust_moc_xor(a, b))

moc_not(cover, domain=None)

Complement a morton cover within a domain.

The result is the cells in domain but not cover. A complement is only well-defined relative to a bounded domain, so moc_not is a domain-bounded difference: it returns domain \ cover, i.e. moc_minus(domain, cover).

Parameters:

Name Type Description Default
cover array_like

The morton cover to complement (mixed order allowed).

required
domain array_like

The morton cover to complement within. A single morton index or a list/array of them (e.g. a coarse "shard" cell whose finer cells are enumerated in cover). Defaults to the whole sphere — the 12 order-0 base cells.

None

Returns:

Type Description
ndarray

Sorted, compacted complement domain \ cover (uint64).

Warns:

Type Description
UserWarning

If cover contains cells outside domain. Such cells cannot be complemented within the domain, so they are clipped: the result is domain \ (cover ∩ domain), which equals domain \ cover whenever cover ⊆ domain.

See Also

moc_minus : difference a \ b (moc_not is moc_minus against a domain, with the whole-sphere default and an out-of-domain warning).

Examples:

The shard case — a coarse cell with some finer cells enumerated inside it, asking for the finer cells not yet enumerated within the shard:

>>> import mortie
>>> shard = mortie.norm2mort(0, 0, 0)          # one order-0 base cell
>>> enumerated = mortie.morton_coverage_moc(lats, lons, order=6)
>>> gaps = mortie.moc_not(enumerated, domain=shard)
Source code in mortie/moc.py
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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def moc_not(cover, domain=None):
    r"""Complement a morton cover within a domain.

    The result is the cells in ``domain`` but not ``cover``. A complement is
    only well-defined relative to a bounded domain, so ``moc_not`` is a
    domain-bounded difference: it returns ``domain \ cover``, i.e.
    ``moc_minus(domain, cover)``.

    Parameters
    ----------
    cover : array_like
        The morton cover to complement (mixed order allowed).
    domain : array_like, optional
        The morton cover to complement *within*.  A single morton index or a
        list/array of them (e.g. a coarse "shard" cell whose finer cells are
        enumerated in ``cover``).  Defaults to the whole sphere — the 12 order-0
        base cells.

    Returns
    -------
    numpy.ndarray
        Sorted, compacted complement ``domain \ cover`` (``uint64``).

    Warns
    -----
    UserWarning
        If ``cover`` contains cells outside ``domain``.  Such cells cannot be
        complemented within the domain, so they are **clipped**: the result is
        ``domain \ (cover ∩ domain)``, which equals ``domain \ cover`` whenever
        ``cover ⊆ domain``.

    See Also
    --------
    moc_minus : difference ``a \ b`` (``moc_not`` is ``moc_minus`` against a
        domain, with the whole-sphere default and an out-of-domain warning).

    Examples
    --------
    The shard case — a coarse cell with some finer cells enumerated inside it,
    asking for the finer cells *not* yet enumerated within the shard:

    >>> import mortie
    >>> shard = mortie.norm2mort(0, 0, 0)          # one order-0 base cell
    >>> enumerated = mortie.morton_coverage_moc(lats, lons, order=6)  # doctest: +SKIP
    >>> gaps = mortie.moc_not(enumerated, domain=shard)               # doctest: +SKIP
    """
    cover = np.asarray(cover, dtype=np.uint64).ravel()
    if domain is None:
        domain = _whole_sphere()
    else:
        domain = np.asarray(domain, dtype=np.uint64).ravel()

    if domain.size == 0:
        # The complement within an empty domain is empty for any cover; the
        # out-of-domain warning would be vacuously true, so skip it.
        return np.asarray([], dtype=np.uint64)

    # Cells of `cover` outside `domain` cannot be complemented within it; warn
    # and clip them (the clip is implicit in `moc_minus(domain, cover)`, which
    # only ever subtracts the in-domain part of `cover`).
    if moc_minus(cover, domain).size > 0:
        warnings.warn(
            "moc_not: `cover` has cells outside `domain`; they cannot be "
            "complemented within the domain and are clipped away.",
            stacklevel=2,
        )

    return moc_minus(domain, cover)

common_ancestor(morton)

Deepest common ancestor (highest-order common parent) of a morton set.

The array-reduction sibling of :func:clip2order (coarsen): where coarsening lowers each word to a caller-given order, common_ancestor discovers the deepest order at which the whole input collapses to a single enclosing cell, and returns that one cell. Because a packed morton word self-encodes its order and ancestry, this is the longest shared path prefix after the common base cell, capped at each word's own order — so mixed-order input is fine (each word is capped at its own order).

Parameters:

Name Type Description Default
morton array_like

Morton indices (mixed order allowed). A single index returns itself.

required

Returns:

Type Description
uint64

The packed morton index of the deepest cell that contains every input. A batch (more than one input) always yields an area cell — even when the inputs collapse to a single order-29 cell, since the shared cell is an enclosing area, not any one input point. Only a single-element input is returned unchanged (its area/point kind preserved), so a lone area or point returns itself.

Raises:

Type Description
ValueError

If morton is empty, contains an empty/invalid word, or spans more than one HEALPix base cell — there is then no common ancestor above the (non-existent) whole-sphere root.

See Also

clip2order : coarsen each word to a fixed order (the elementwise form; common_ancestor is its reduce-by-common-coarsening reduction). mortie.batch.common_ancestors : the batch form (many groups in one call).

Examples:

The four order-5 children of an order-4 cell reduce to that parent:

>>> import mortie, numpy as np
>>> parent = mortie.norm2mort(11, 0, 4)              # one order-4 cell in base 0
>>> kids = mortie.norm2mort([11 * 4 + s for s in range(4)], [0] * 4, 5)
>>> int(mortie.common_ancestor(kids)) == int(parent)
True
Source code in mortie/moc.py
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
def common_ancestor(morton):
    """Deepest common ancestor (highest-order common parent) of a morton set.

    The array-reduction sibling of :func:`clip2order` (coarsen): where coarsening
    lowers each word to a *caller-given* order, ``common_ancestor`` *discovers*
    the deepest order at which the whole input collapses to a single enclosing
    cell, and returns that one cell.  Because a packed morton word self-encodes
    its order and ancestry, this is the longest shared path prefix after the
    common base cell, capped at each word's own order — so mixed-order input is
    fine (each word is capped at its own order).

    Parameters
    ----------
    morton : array_like
        Morton indices (mixed order allowed).  A single index returns itself.

    Returns
    -------
    numpy.uint64
        The packed morton index of the deepest cell that contains every input.
        A batch (more than one input) always yields an **area** cell — even when
        the inputs collapse to a single order-29 cell, since the shared cell is
        an enclosing area, not any one input point.  Only a single-element input
        is returned unchanged (its area/point kind preserved), so a lone area or
        point returns itself.

    Raises
    ------
    ValueError
        If ``morton`` is empty, contains an empty/invalid word, or spans more
        than one HEALPix base cell — there is then no common ancestor above the
        (non-existent) whole-sphere root.

    See Also
    --------
    clip2order : coarsen each word to a fixed order (the elementwise form;
        ``common_ancestor`` is its reduce-by-common-coarsening reduction).
    mortie.batch.common_ancestors : the batch form (many groups in one call).

    Examples
    --------
    The four order-5 children of an order-4 cell reduce to that parent:

    >>> import mortie, numpy as np
    >>> parent = mortie.norm2mort(11, 0, 4)              # one order-4 cell in base 0
    >>> kids = mortie.norm2mort([11 * 4 + s for s in range(4)], [0] * 4, 5)
    >>> int(mortie.common_ancestor(kids)) == int(parent)
    True
    """
    morton = np.asarray(morton, dtype=np.uint64).ravel()
    return np.uint64(_rustie.rust_moc_min(morton))

split_base_cells(words, sort=False)

Partition a morton set by HEALPix base cell.

Each group is keyed by its own :func:moc_min. The companion to :func:moc_min for the cross-base-cell case it refuses: where moc_min reduces a single base cell's words to one ancestor and raises on mixed base cells, split_base_cells groups the words by base cell and hands back each group untouched. Every group is keyed by its own moc_min — the deepest cell enclosing that group — which is self- describing (a packed word the same 64 bits wide as the data) and from which the base cell id is cheap to recover (e.g. mort2healpix / MortonIndexArray.base_cell).

Parameters:

Name Type Description Default
words array_like

Morton indices (mixed order and mixed base cell allowed).

required
sort bool

If False (default, the faster path) each group keeps the input order of its words. If True each group's words are sorted, giving a canonical MOC per base cell.

False

Returns:

Type Description
dict[int, ndarray]

Maps the int of each group's moc_min word to that group's uint64 array of words. Empty input returns {}; a single base cell returns a one-entry dict.

Raises:

Type Description
ValueError

If a group's moc_min reduction fails — e.g. words contains an empty/invalid word (moc_min rejects it).

See Also

moc_min : the single-base-cell reduction this partitions for; its mixed- base-cell error points here.

Examples:

>>> import mortie, numpy as np
>>> a = np.atleast_1d(mortie.norm2mort(0, 2, 4))   # one cell in base 2
>>> b = np.atleast_1d(mortie.norm2mort(0, 5, 4))   # one cell in base 5
>>> groups = mortie.split_base_cells(np.concatenate([a, b]))
>>> sorted(int(np.uint64(k) >> np.uint64(60)) - 1 for k in groups)
[2, 5]
Source code in mortie/moc.py
405
406
407
408
409
410
411
412
413
414
415
416
417
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
def split_base_cells(words, sort=False):
    """Partition a morton set by HEALPix base cell.

    Each group is keyed by its own :func:`moc_min`.
    The companion to :func:`moc_min` for the cross-base-cell case it refuses:
    where ``moc_min`` reduces a *single* base cell's words to one ancestor and
    raises on mixed base cells, ``split_base_cells`` groups the words by base
    cell and hands back each group untouched.  Every group is keyed by its own
    ``moc_min`` — the deepest cell enclosing that group — which is self-
    describing (a packed word the same 64 bits wide as the data) and from which
    the base cell id is cheap to recover (e.g. ``mort2healpix`` /
    ``MortonIndexArray.base_cell``).

    Parameters
    ----------
    words : array_like
        Morton indices (mixed order and mixed base cell allowed).
    sort : bool, optional
        If ``False`` (default, the faster path) each group keeps the input
        order of its words.  If ``True`` each group's words are sorted, giving a
        canonical MOC per base cell.

    Returns
    -------
    dict[int, numpy.ndarray]
        Maps the ``int`` of each group's ``moc_min`` word to that group's
        ``uint64`` array of words.  Empty input returns ``{}``; a single base
        cell returns a one-entry dict.

    Raises
    ------
    ValueError
        If a group's ``moc_min`` reduction fails — e.g. ``words`` contains an
        empty/invalid word (``moc_min`` rejects it).

    See Also
    --------
    moc_min : the single-base-cell reduction this partitions for; its mixed-
        base-cell error points here.

    Examples
    --------
    >>> import mortie, numpy as np
    >>> a = np.atleast_1d(mortie.norm2mort(0, 2, 4))   # one cell in base 2
    >>> b = np.atleast_1d(mortie.norm2mort(0, 5, 4))   # one cell in base 5
    >>> groups = mortie.split_base_cells(np.concatenate([a, b]))
    >>> sorted(int(np.uint64(k) >> np.uint64(60)) - 1 for k in groups)
    [2, 5]
    """
    words = np.asarray(words, dtype=np.uint64).ravel()
    if words.size == 0:
        return {}

    bases = _rustie.rust_mi_base_cell_of(words)
    out = {}
    # Stable group-by: dict.fromkeys yields base cells in first-seen order, and
    # the boolean mask below keeps each group's words in input order.
    for base in dict.fromkeys(bases.tolist()):
        group = words[bases == base]
        if sort:
            group = np.sort(group)
        out[int(moc_min(group))] = group
    return out