Skip to content

mortie.morton_index

The numpy-only surface: the packed-word scalar and the decimal parse functions. Nothing here imports pandas.

The pandas MortonIndexDtype / MortonIndexArray pair is re-exported from this module (mortie.morton_index.MortonIndexArray resolves), but it is defined in mortie.pandas and documented there.

MortonWord constructs from either form of a cell id (issue #152), disambiguated by type: a str (or a 0-d "U" array of one) is the decimal Morton label (MortonWord("-31123"), point-suffix grammar included), parsed eagerly through decimal_to_word — an invalid label raises ValueError at the boundary and never silently constructs. Anything else is the packed word, handed to numpy.uint64 and taking its semantics whole (int, numpy.uint64, and by numpy parity bool and a truncating float); bytes-like input is the one deliberate divergence, refused with a pointed TypeError rather than read as numpy would read it. The .decimal / .order / .base_cell accessors read the label string, the HEALPix order, and the base cell back off the word — and they are strict: a word that decodes to no legal cell (the empty sentinel included) raises a pointed ValueError naming the word, rather than propagating a sentinel string onward. Only the display dunders stay lazy/never-raise ("<NA>" for the empty word, "<invalid 0x...>" for one with an invalid prefix). The type is exported flat as mortie.MortonWord.

The morton_index datatype: the numpy-only surface over packed words.

The scalar and decimal parse surface of the packed 64-bit decimal-Morton MOC kernel (issue #35, phase 5). The pandas ExtensionArray skin over the same words lives in :mod:mortie.pandas (issue #135) and is re-exported from here.

The kernel lives in Rust (mortie-core/src/decimal_morton.rs, re-exported as mortie_rustie::decimal_morton); this module is the user-facing surface. Storage is raw uint64 packed words (issue #58; zero-copy over the kernel's bit layout [4-bit prefix | 54-bit body | 6-bit suffix]). The word is unsigned, so the Z-order is simply the raw word order -- base cells 7..=11 (prefix 8..=12) set bit 63 and sort after the northern cells with no special casing, and comparisons/sort operate on the words directly. Domain operations (coarsen/order/base_cell) and the (nested, depth) <-> word bridge delegate to the vectorized Rust bindings; no arithmetic operators are defined (raw arithmetic on packed words is meaningless).

pandas is an optional dependency: importing mortie succeeds with only numpy installed. Nothing in this module touches pandas; the ExtensionArray names resolve by importing :mod:mortie.pandas on demand, and a clear ImportError is raised if they are touched without pandas installed.

MortonWord

Bases: uint64

A packed morton_index word that displays as its decimal string.

Element access and iteration on a MortonIndexArray yield this type (issue #104), so a downstream f"{shard_key}" prints the decimal Morton id (-31123 style) rather than the raw packed word. It subclasses numpy.uint64: comparisons, hashing, and int() (the packed word) behave exactly like the word itself; only str/repr differ. The empty sentinel renders "<NA>"; a word with an invalid prefix renders "<invalid 0x...>" rather than raising (a repr must never raise). The accessor properties (decimal, order, base_cell) take the opposite posture: they are data queries, and raise ValueError on a word that decodes to no legal cell rather than propagating it.

Construct it from a packed word (an int or numpy.uint64) exactly as you would a numpy.uint64, or from the decimal Morton label itself: a str argument parses as a decimal label through :func:decimal_to_word (issue #152), so MortonWord("-31123") is the cell that displays as -31123. The two forms are disambiguated by type alone -- the inherited numpy.uint64 constructor used to read a label string as a base-10 packed word, silently constructing the wrong cell. An invalid label raises ValueError eagerly, at the boundary; display stays lazy/never-raise as above. Bytes-like input -- what an HDF5 attr reader hands back for a label -- is refused with a pointed TypeError rather than guessed at: numpy reads bytes as a base-10 word and bytearray as a raw buffer, and neither reading is the label.

Source code in mortie/morton_index.py
 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
123
124
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
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
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
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
346
347
348
class MortonWord(np.uint64):
    """A packed ``morton_index`` word that displays as its decimal string.

    Element access and iteration on a ``MortonIndexArray`` yield this type
    (issue #104), so a downstream ``f"{shard_key}"`` prints the decimal Morton
    id (``-31123`` style) rather than the raw packed word. It subclasses
    ``numpy.uint64``: comparisons, hashing, and ``int()`` (the packed word)
    behave exactly like the word itself; only ``str``/``repr`` differ. The
    empty sentinel renders ``"<NA>"``; a word with an invalid prefix renders
    ``"<invalid 0x...>"`` rather than raising (a repr must never raise). The
    accessor properties (``decimal``, ``order``, ``base_cell``) take the
    opposite posture: they are data queries, and raise ``ValueError`` on a
    word that decodes to no legal cell rather than propagating it.

    Construct it from a packed word (an ``int`` or ``numpy.uint64``) exactly as
    you would a ``numpy.uint64``, or from the decimal Morton label itself: a
    ``str`` argument parses as a decimal label through :func:`decimal_to_word`
    (issue #152), so ``MortonWord("-31123")`` is the cell that displays
    as ``-31123``. The two forms are disambiguated by type alone -- the
    inherited ``numpy.uint64`` constructor used to read a label string as a
    base-10 *packed word*, silently constructing the wrong cell. An invalid
    label raises ``ValueError`` eagerly, at the boundary; display stays
    lazy/never-raise as above. Bytes-like input -- what an HDF5 attr reader
    hands back for a label -- is refused with a pointed ``TypeError``
    rather than guessed at: numpy reads ``bytes`` as a base-10 word and
    ``bytearray`` as a raw buffer, and neither reading is the label.
    """

    def __new__(cls, value=0):
        """Build from a packed word, or parse a decimal Morton label.

        Parameters
        ----------
        value : int-like or str
            A ``str`` is the decimal Morton label, e.g. ``"-31123"``
            (parsed via :func:`decimal_to_word`, terminal ``p`` point
            suffix included); a 0-d ``"U"`` array of one is unwrapped and
            read the same way. Anything else is a packed word, handed to
            ``numpy.uint64`` and taking *its* semantics whole -- ``bool``
            and ``float`` included, so ``1.9`` truncates to ``1``, kept as
            numpy parity by choice rather than tightened here. Bytes-like
            input is refused: see *Raises*.

        Returns
        -------
        MortonWord
            The packed word, displaying as its decimal label -- for the
            label form and for every int-like scalar. Parity has one
            edge: an input ``numpy.uint64`` turns into an *array* rather
            than a scalar (a buffer such as ``memoryview``, or an array
            of more than one element) comes back as numpy returns it, a
            plain ``ndarray``, not this type.

        Raises
        ------
        ValueError
            If a ``str`` ``value`` is not a well-formed decimal Morton
            label (sign column + base digit ``1..6``, one ``1..4`` digit
            per order, optional terminal ``p`` -- spec sections 2 and 4).
        TypeError
            If ``value`` is bytes-like -- ``bytes``/``numpy.bytes_``
            (which numpy reads as a base-10 *packed word*) or
            ``bytearray`` (which numpy reads as a raw *buffer*, giving an
            array). Neither reading is the decimal label a byte string
            from an HDF5 attr almost always is, so the input is refused
            rather than guessed at. Decode it
            (``value.decode("ascii")``) for a label, or pass an ``int``
            for a packed word. Non-``str``, non-int-like values raise
            whatever ``numpy.uint64`` raises for them.
        """
        if (
            isinstance(value, np.ndarray)
            and value.ndim == 0
            and value.dtype.kind in "SU"
        ):
            # A 0-d "U"/"S" array is a string handed over in an array
            # wrapper (h5py attrs, ``arr[()]``); numpy.uint64 would read
            # either as a base-10 word and slip past both guards below.
            # Unwrap so "U" takes the label path and "S" hits the refusal.
            # Numeric 0-d arrays are left alone -- numpy parity.
            value = value.item()
        if isinstance(value, (bytes, bytearray)):
            raise TypeError(
                f"MortonWord({_clip(repr(value))}): bytes-like "
                f"input is ambiguous here -- numpy reads bytes as a base-10 "
                f"packed word and bytearray as a raw buffer, and neither "
                f"reading is the decimal Morton label a byte string from an "
                f"HDF5 attr almost always is. Pass value.decode('ascii') "
                f"for a label, or an int for a packed word."
            )
        if isinstance(value, str):
            try:
                word = decimal_to_word(value, dtype=int)
            except ValueError as exc:
                raise ValueError(
                    f"MortonWord({_clip(repr(value))}): not a "
                    f"decimal Morton "
                    f"label (['-'] + base digit 1..6 + one 1..4 digit per "
                    f"order + optional terminal 'p' -- spec sections 2 and "
                    f"4): {_clip(str(exc), 160)}"
                ) from exc
            return super().__new__(cls, word)
        return super().__new__(cls, value)

    def _require_cell(self):
        """Resolve this word's base cell, raising if it names no legal cell.

        The strict gate the accessor properties share (issue #152): accessors
        are data queries, so a word that decodes to no legal cell raises a
        pointed ``ValueError`` naming the word, instead of propagating a
        sentinel string onward. Only the display dunders stay never-raise.

        The gate is the base-cell kernel's ``255`` sentinel rather than a full
        label decode: the two agree on exactly which words are legal (verified
        over 400k random words, the empty sentinel included) and the base-cell
        decode is the cheaper of the pair by roughly 2.4x. It also raises
        nothing of its own to chain, so the message stays in this module's
        scalar vocabulary instead of quoting an array kernel at a scalar
        caller.

        Returns
        -------
        int
            The HEALPix base cell, 0-11 -- what :attr:`base_cell` returns, so
            that accessor needs no second kernel call.

        Raises
        ------
        ValueError
            For the empty sentinel (``0``) or a word whose packed form
            decodes to no legal cell.
        """
        word = int(self)
        if word == 0:
            raise ValueError(
                "MortonWord 0x0000000000000000 is the empty sentinel -- it "
                "decodes to no legal cell (display renders it '<NA>')"
            )
        cell = int(
            _rustie.rust_mi_base_cell_of(
                np.asarray([word], dtype=np.uint64)
            )[0]
        )
        if cell == 255:
            raise ValueError(
                f"MortonWord {word:#018x} decodes to no legal cell "
                f"(invalid packed word)"
            )
        return cell

    @property
    def decimal(self):
        """The canonical decimal Morton label of this word.

        Strict (issue #152): a valid word gives exactly the string ``str``
        renders; the empty sentinel or an invalid word raises rather than
        returning a ``"<NA>"`` / ``"<invalid 0x...>"`` sentinel string --
        accessors do not propagate invalid data. The never-raise rendering
        stays confined to the display dunders.

        Returns
        -------
        str
            The decimal Morton id, e.g. ``"-31123"``.

        Raises
        ------
        ValueError
            If this word decodes to no legal cell (empty sentinel included).
        """
        self._require_cell()
        return _rustie.rust_mi_decimal_repr(
            np.asarray([int(self)], dtype=np.uint64)
        )[0]

    @property
    def order(self):
        """The HEALPix order of this word (0-29), via :func:`mortie.orders_of`.

        Strict (issue #152): the word is gated on naming a legal cell first,
        so the empty sentinel or an invalid word raises instead of yielding
        the raw suffix arithmetic of a word that names no cell. Past that
        gate the order is :func:`mortie.orders_of`'s decode exactly.

        Returns
        -------
        int
            The HEALPix order, 0-29.

        Raises
        ------
        ValueError
            If this word decodes to no legal cell (empty sentinel included).
        """
        self._require_cell()
        # Lazy import: mortie.orders pulls in the batch/coverage/geometry
        # chain, and this module stays a leaf import (numpy + _rustie only).
        from .orders import orders_of

        return int(orders_of(self)[0])

    @property
    def base_cell(self):
        """The HEALPix base cell of this word (0-11).

        Named ``base_cell``, not ``base`` -- ``numpy.generic`` already owns a
        ``.base`` attribute (the buffer-protocol base object), and shadowing
        it would change inherited numpy behavior. Strict like the other
        accessors (issue #152): an invalid word raises rather than mapping to
        the kernel's ``255`` sentinel -- that sentinel *is*
        :meth:`_require_cell`'s gate, so this accessor is the gate's own
        result and costs exactly one kernel call. The decode is the same
        per-element kernel behind :meth:`MortonIndexArray.base_cells`.

        Returns
        -------
        int
            The base cell, 0-11.

        Raises
        ------
        ValueError
            If this word decodes to no legal cell (empty sentinel included).
        """
        return self._require_cell()

    def __str__(self):
        """Render the word as its decimal Morton string.

        Returns
        -------
        str
            The decimal Morton id, ``"<NA>"`` for the empty sentinel, or
            ``"<invalid 0x...>"`` for a word with an invalid prefix.
        """
        word = int(self)
        if word == 0:
            return "<NA>"
        try:
            return _rustie.rust_mi_decimal_repr(
                np.asarray([word], dtype=np.uint64)
            )[0]
        except ValueError:
            return f"<invalid {word:#018x}>"

    __repr__ = __str__

    def __format__(self, spec):
        """Format the decimal Morton string, not the packed word.

        numpy's numeric ``__format__`` would print the packed word; the display
        form of a morton_index is its decimal string, so ``f"{shard_key}"`` (and
        any string spec, e.g. ``">10"``) formats that instead. ``int(self)``
        remains the escape hatch to format the raw word numerically. Old-style
        ``"%d" % key`` bypasses ``__format__`` entirely and emits the raw word.

        Parameters
        ----------
        spec : str
            A standard format spec, applied to the decimal string.

        Returns
        -------
        str
            The formatted decimal Morton string.
        """
        return format(str(self), spec)

    def __reduce__(self):
        """Pickle as a ``MortonWord`` rather than a bare ``uint64``.

        numpy scalars pickle through ``multiarray.scalar``, which rebuilds the
        bare ``np.uint64`` and would silently drop the decimal display on any
        process boundary (multiprocessing/dask); rebuild the wrapper instead.

        Returns
        -------
        tuple
            The ``(callable, args)`` pair pickle uses to rebuild the wrapper.
        """
        return (type(self), (int(self),))

base_cell property

The HEALPix base cell of this word (0-11).

Named base_cell, not base -- numpy.generic already owns a .base attribute (the buffer-protocol base object), and shadowing it would change inherited numpy behavior. Strict like the other accessors (issue #152): an invalid word raises rather than mapping to the kernel's 255 sentinel -- that sentinel is :meth:_require_cell's gate, so this accessor is the gate's own result and costs exactly one kernel call. The decode is the same per-element kernel behind :meth:MortonIndexArray.base_cells.

Returns:

Type Description
int

The base cell, 0-11.

Raises:

Type Description
ValueError

If this word decodes to no legal cell (empty sentinel included).

decimal property

The canonical decimal Morton label of this word.

Strict (issue #152): a valid word gives exactly the string str renders; the empty sentinel or an invalid word raises rather than returning a "<NA>" / "<invalid 0x...>" sentinel string -- accessors do not propagate invalid data. The never-raise rendering stays confined to the display dunders.

Returns:

Type Description
str

The decimal Morton id, e.g. "-31123".

Raises:

Type Description
ValueError

If this word decodes to no legal cell (empty sentinel included).

order property

The HEALPix order of this word (0-29), via :func:mortie.orders_of.

Strict (issue #152): the word is gated on naming a legal cell first, so the empty sentinel or an invalid word raises instead of yielding the raw suffix arithmetic of a word that names no cell. Past that gate the order is :func:mortie.orders_of's decode exactly.

Returns:

Type Description
int

The HEALPix order, 0-29.

Raises:

Type Description
ValueError

If this word decodes to no legal cell (empty sentinel included).

__format__(spec)

Format the decimal Morton string, not the packed word.

numpy's numeric __format__ would print the packed word; the display form of a morton_index is its decimal string, so f"{shard_key}" (and any string spec, e.g. ">10") formats that instead. int(self) remains the escape hatch to format the raw word numerically. Old-style "%d" % key bypasses __format__ entirely and emits the raw word.

Parameters:

Name Type Description Default
spec str

A standard format spec, applied to the decimal string.

required

Returns:

Type Description
str

The formatted decimal Morton string.

Source code in mortie/morton_index.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
def __format__(self, spec):
    """Format the decimal Morton string, not the packed word.

    numpy's numeric ``__format__`` would print the packed word; the display
    form of a morton_index is its decimal string, so ``f"{shard_key}"`` (and
    any string spec, e.g. ``">10"``) formats that instead. ``int(self)``
    remains the escape hatch to format the raw word numerically. Old-style
    ``"%d" % key`` bypasses ``__format__`` entirely and emits the raw word.

    Parameters
    ----------
    spec : str
        A standard format spec, applied to the decimal string.

    Returns
    -------
    str
        The formatted decimal Morton string.
    """
    return format(str(self), spec)

__new__(value=0)

Build from a packed word, or parse a decimal Morton label.

Parameters:

Name Type Description Default
value int - like or str

A str is the decimal Morton label, e.g. "-31123" (parsed via :func:decimal_to_word, terminal p point suffix included); a 0-d "U" array of one is unwrapped and read the same way. Anything else is a packed word, handed to numpy.uint64 and taking its semantics whole -- bool and float included, so 1.9 truncates to 1, kept as numpy parity by choice rather than tightened here. Bytes-like input is refused: see Raises.

0

Returns:

Type Description
MortonWord

The packed word, displaying as its decimal label -- for the label form and for every int-like scalar. Parity has one edge: an input numpy.uint64 turns into an array rather than a scalar (a buffer such as memoryview, or an array of more than one element) comes back as numpy returns it, a plain ndarray, not this type.

Raises:

Type Description
ValueError

If a str value is not a well-formed decimal Morton label (sign column + base digit 1..6, one 1..4 digit per order, optional terminal p -- spec sections 2 and 4).

TypeError

If value is bytes-like -- bytes/numpy.bytes_ (which numpy reads as a base-10 packed word) or bytearray (which numpy reads as a raw buffer, giving an array). Neither reading is the decimal label a byte string from an HDF5 attr almost always is, so the input is refused rather than guessed at. Decode it (value.decode("ascii")) for a label, or pass an int for a packed word. Non-str, non-int-like values raise whatever numpy.uint64 raises for them.

Source code in mortie/morton_index.py
 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
144
145
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
def __new__(cls, value=0):
    """Build from a packed word, or parse a decimal Morton label.

    Parameters
    ----------
    value : int-like or str
        A ``str`` is the decimal Morton label, e.g. ``"-31123"``
        (parsed via :func:`decimal_to_word`, terminal ``p`` point
        suffix included); a 0-d ``"U"`` array of one is unwrapped and
        read the same way. Anything else is a packed word, handed to
        ``numpy.uint64`` and taking *its* semantics whole -- ``bool``
        and ``float`` included, so ``1.9`` truncates to ``1``, kept as
        numpy parity by choice rather than tightened here. Bytes-like
        input is refused: see *Raises*.

    Returns
    -------
    MortonWord
        The packed word, displaying as its decimal label -- for the
        label form and for every int-like scalar. Parity has one
        edge: an input ``numpy.uint64`` turns into an *array* rather
        than a scalar (a buffer such as ``memoryview``, or an array
        of more than one element) comes back as numpy returns it, a
        plain ``ndarray``, not this type.

    Raises
    ------
    ValueError
        If a ``str`` ``value`` is not a well-formed decimal Morton
        label (sign column + base digit ``1..6``, one ``1..4`` digit
        per order, optional terminal ``p`` -- spec sections 2 and 4).
    TypeError
        If ``value`` is bytes-like -- ``bytes``/``numpy.bytes_``
        (which numpy reads as a base-10 *packed word*) or
        ``bytearray`` (which numpy reads as a raw *buffer*, giving an
        array). Neither reading is the decimal label a byte string
        from an HDF5 attr almost always is, so the input is refused
        rather than guessed at. Decode it
        (``value.decode("ascii")``) for a label, or pass an ``int``
        for a packed word. Non-``str``, non-int-like values raise
        whatever ``numpy.uint64`` raises for them.
    """
    if (
        isinstance(value, np.ndarray)
        and value.ndim == 0
        and value.dtype.kind in "SU"
    ):
        # A 0-d "U"/"S" array is a string handed over in an array
        # wrapper (h5py attrs, ``arr[()]``); numpy.uint64 would read
        # either as a base-10 word and slip past both guards below.
        # Unwrap so "U" takes the label path and "S" hits the refusal.
        # Numeric 0-d arrays are left alone -- numpy parity.
        value = value.item()
    if isinstance(value, (bytes, bytearray)):
        raise TypeError(
            f"MortonWord({_clip(repr(value))}): bytes-like "
            f"input is ambiguous here -- numpy reads bytes as a base-10 "
            f"packed word and bytearray as a raw buffer, and neither "
            f"reading is the decimal Morton label a byte string from an "
            f"HDF5 attr almost always is. Pass value.decode('ascii') "
            f"for a label, or an int for a packed word."
        )
    if isinstance(value, str):
        try:
            word = decimal_to_word(value, dtype=int)
        except ValueError as exc:
            raise ValueError(
                f"MortonWord({_clip(repr(value))}): not a "
                f"decimal Morton "
                f"label (['-'] + base digit 1..6 + one 1..4 digit per "
                f"order + optional terminal 'p' -- spec sections 2 and "
                f"4): {_clip(str(exc), 160)}"
            ) from exc
        return super().__new__(cls, word)
    return super().__new__(cls, value)

__reduce__()

Pickle as a MortonWord rather than a bare uint64.

numpy scalars pickle through multiarray.scalar, which rebuilds the bare np.uint64 and would silently drop the decimal display on any process boundary (multiprocessing/dask); rebuild the wrapper instead.

Returns:

Type Description
tuple

The (callable, args) pair pickle uses to rebuild the wrapper.

Source code in mortie/morton_index.py
336
337
338
339
340
341
342
343
344
345
346
347
348
def __reduce__(self):
    """Pickle as a ``MortonWord`` rather than a bare ``uint64``.

    numpy scalars pickle through ``multiarray.scalar``, which rebuilds the
    bare ``np.uint64`` and would silently drop the decimal display on any
    process boundary (multiprocessing/dask); rebuild the wrapper instead.

    Returns
    -------
    tuple
        The ``(callable, args)`` pair pickle uses to rebuild the wrapper.
    """
    return (type(self), (int(self),))

__str__()

Render the word as its decimal Morton string.

Returns:

Type Description
str

The decimal Morton id, "<NA>" for the empty sentinel, or "<invalid 0x...>" for a word with an invalid prefix.

Source code in mortie/morton_index.py
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def __str__(self):
    """Render the word as its decimal Morton string.

    Returns
    -------
    str
        The decimal Morton id, ``"<NA>"`` for the empty sentinel, or
        ``"<invalid 0x...>"`` for a word with an invalid prefix.
    """
    word = int(self)
    if word == 0:
        return "<NA>"
    try:
        return _rustie.rust_mi_decimal_repr(
            np.asarray([word], dtype=np.uint64)
        )[0]
    except ValueError:
        return f"<invalid {word:#018x}>"

decimal_to_word(s, dtype=np.uint64)

Parse one decimal Morton string into its packed word (issue #114).

The scalar inverse of the decode-through-kernel repr, and the public counterpart to :meth:MortonIndexArray.decimal_repr: sign column + leading base digit (1..6), one 1..4 digit per order, and an optional terminal p kind suffix (spec section 4, issue #120). A p-marked string (legal only at order 29) yields the POINT word; an unmarked string always yields the AREA word -- the tie-break for the one ambiguous form, and fully backward compatible (every pre-suffix string is unmarked).

numpy-only: calling this imports no pandas, so it is usable from hot per-key parse paths.

Batch vectorized (issue #187), with numpy semantics literally: a str in gives one word out, anything else is treated as an array of ids and gives an array of words back, parsed in Rust in one pass. The array form is always uint64, so dtype applies to the scalar form only.

Parameters:

Name Type Description Default
s str or array_like of str

The decimal Morton id, e.g. "-31123", or an array of them (any shape) for the vectorized form.

required
dtype type

The return shape. np.uint64 (default) returns the bare packed word, staying numpy-native for hot loops; int returns a Python int; :class:MortonWord returns a word that displays back as its decimal string. "uint64" / np.dtype("uint64") are accepted spellings of the default.

uint64

Returns:

Type Description
uint64 or int or MortonWord or ndarray

The packed word, in the shape requested by dtype; for array input, a uint64 array in the shape of s.

Raises:

Type Description
ValueError

If s is a malformed decimal Morton id -- for the array form, naming the first malformed id in input order, not its index, so a wide array gives no row to look at.

TypeError

If dtype is not np.uint64 (or a spelling of it), int, or :class:MortonWord; or if a non-uint64 dtype is asked for alongside array input, which has no scalar shape to return.

See Also

_decimals_to_words : the vectorized kernel the array form delegates to.

Source code in mortie/morton_index.py
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
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
def decimal_to_word(s, dtype=np.uint64):
    """Parse one decimal Morton string into its packed word (issue #114).

    The scalar inverse of the decode-through-kernel repr, and the public
    counterpart to :meth:`MortonIndexArray.decimal_repr`: sign column +
    leading base digit (``1..6``), one ``1..4`` digit per order, and an
    optional terminal ``p`` kind suffix (spec section 4, issue #120). A
    ``p``-marked string (legal only at order 29) yields the POINT word; an
    unmarked string always yields the AREA word -- the tie-break for the one
    ambiguous form, and fully backward compatible (every pre-suffix string
    is unmarked).

    numpy-only: calling this imports no pandas, so it is usable from hot
    per-key parse paths.

    **Batch vectorized** (issue #187), with numpy semantics literally: a
    ``str`` in gives one word out, anything else is treated as an array of ids
    and gives an array of words back, parsed in Rust in one pass. The array
    form is always ``uint64``, so ``dtype`` applies to the scalar form only.

    Parameters
    ----------
    s : str or array_like of str
        The decimal Morton id, e.g. ``"-31123"``, or an array of them (any
        shape) for the vectorized form.
    dtype : type, optional
        The return shape. ``np.uint64`` (default) returns the bare packed
        word, staying numpy-native for hot loops; ``int`` returns a Python
        int; :class:`MortonWord` returns a word that displays back as
        its decimal string. ``"uint64"`` / ``np.dtype("uint64")`` are
        accepted spellings of the default.

    Returns
    -------
    numpy.uint64 or int or MortonWord or numpy.ndarray
        The packed word, in the shape requested by ``dtype``; for array input,
        a ``uint64`` array in the shape of ``s``.

    Raises
    ------
    ValueError
        If ``s`` is a malformed decimal Morton id -- for the array form,
        naming the first malformed *id* in input order, not its index, so a
        wide array gives no row to look at.
    TypeError
        If ``dtype`` is not ``np.uint64`` (or a spelling of it), ``int``, or
        :class:`MortonWord`; or if a non-``uint64`` ``dtype`` is asked
        for alongside array input, which has no scalar shape to return.

    See Also
    --------
    _decimals_to_words : the vectorized kernel the array form delegates to.
    """
    if not isinstance(s, str):
        # `MortonWord` is a uint64 subclass, so `np.dtype` resolves it
        # to uint64 -- it has to be ruled out by identity before that check, or
        # asking for it on array input would silently return bare words.
        if isinstance(dtype, type) and issubclass(dtype, MortonWord):
            uint64_asked = False
        else:
            try:
                uint64_asked = np.dtype(dtype) == np.uint64
            except TypeError:
                uint64_asked = False
        if not uint64_asked:
            raise TypeError(
                f"decimal_to_word dtype must be np.uint64 (the default) for "
                f"array input, which is always uint64; got {dtype!r}"
            )
        words = _decimals_to_words(s)
        # numpy semantics exactly: a 0-d input is a scalar, not a 0-d array.
        return words if words.ndim else np.uint64(words)
    word = int(_rustie.rust_mi_from_decimal([s])[0])
    if dtype is int:
        return word
    # `issubclass`, not `is`: a MortonWord subclass must round-trip as
    # itself rather than silently downgrading to a bare uint64.
    if isinstance(dtype, type) and issubclass(dtype, MortonWord):
        return dtype(word)
    # Only a dtype *spelling* is accepted here -- `np.dtype(np.uint64(0))`
    # happens to succeed on an instance, which would let a stray value through.
    if isinstance(dtype, (type, str, np.dtype)):
        try:
            requested = np.dtype(dtype)
        except TypeError:
            requested = None
        if requested == np.uint64:
            return np.uint64(word)
    raise TypeError(
        f"decimal_to_word dtype must be np.uint64 (the default), int, or "
        f"MortonWord; got {dtype!r}"
    )