Skip to main content

NumPy Utils

NumPy structured array utilities — dtype construction, field views, joining, enum mapping, position arrays, byte/string conversion for C++ interop, and growable (std::vector-like) arrays.

Overview

vcti-nputils collects the low-level NumPy helpers shared across the vcti stack. Most of it is stateless functions over structured arrays and dtypes — building and reshaping dtypes, taking zero-copy field views, joining arrays, mapping enum values, and converting byte fields for pybind11/C++ interop. Alongside those it provides two small stateful containers for building a numpy array of unknown final size: GrowableArray (append-only — the common case) and DynamicArray (the same growth model plus soft deletion). Both grow on demand and hand off a plain contiguous array via to_numpy() for the read-heavy phase.

Which to use: append-only? GrowableArray. Need to remove elements mid-build? DynamicArray.

Installation

pip install "vcti-nputils>=2.0.0"

In pyproject.toml dependencies

dependencies = [
    "vcti-nputils>=2.0.0",
]

Quick Start

import numpy as np
from vcti.nputils import (
    as_ndarray,
    check_overflow,
    drop_fields,
    DynamicArray,
    fields_view,
    flatten_dtype,
    get_string,
    GrowableArray,
    join_struct_arrays,
    merge_adjacent_fields,
    name_array,
    position_array,
    rename_fields,
    set_string,
    structured_dtype,
    with_field_metadata,
)

# Join structured arrays horizontally
dt1 = np.dtype([('id', 'i4'), ('value', 'f8')])
dt2 = np.dtype([('name', 'U10')])
arr1 = np.array([(1, 1.5), (2, 2.5)], dtype=dt1)
arr2 = np.array([('Alice',), ('Bob',)], dtype=dt2)
joined = join_struct_arrays([arr1, arr2])
# dtype: [('id', 'i4'), ('value', 'f8'), ('name', 'U10')]

# Create a zero-copy view with selected fields
view = fields_view(joined, ['id', 'name'])

# Drop fields from a structured array (zero-copy)
clean = drop_fields(joined, ['value'])

# Build a structured dtype from a scalar dtype + names
coord_dt = structured_dtype('f8', ['x', 'y', 'z'])
# dtype([('x', '<f8'), ('y', '<f8'), ('z', '<f8')])

# Rename fields in a dtype
new_dt = rename_fields(dt1, {'id': 'node_id', 'value': 'temperature'})

# Flatten array fields into individual columns (default naming)
dt = np.dtype([('id', 'i4'), ('coords', 'f8', (3,))])
_, cols = flatten_dtype(dt)
# cols: ['id', 'coord_0', 'coord_1', 'coord_2']

# Flatten with explicit per-field names
_, cols = flatten_dtype(dt, field_names={'coords': ['x', 'y', 'z']})
# cols: ['id', 'x', 'y', 'z']

# Flatten with a custom format string
_, cols = flatten_dtype(dt, fmt="{name}[{dim}]")
# cols: ['id', 'coord[0]', 'coord[1]', 'coord[2]']

# Merge adjacent 'S' fields into one (pure dtype view). Multiple merges
# can be specified at once; same-field overlap and name collisions are
# validated before anything is returned.
dt = np.dtype([
    ('first', 'S4'), ('last', 'S6'),
    ('city', 'S8'), ('state', 'S2'),
    ('age', 'i4'),
])
merged = merge_adjacent_fields(dt, {
    'name':    ['first', 'last'],
    'address': ['city', 'state'],
})
# dtype([('name', 'S10'), ('address', 'S10'), ('age', '<i4')])

# Map numeric enum values to names
enum_dict = {1: 'ACTIVE', 2: 'INACTIVE', 3: 'PENDING'}
names = name_array(np.array([1, 2, 1, 3]), enum_dict)

# Convert counts to cumulative offsets
offsets = position_array(np.array([3, 2, 4, 1]))
# array([0, 3, 5, 9, 10])

# Safely coerce inputs to ndarray
arr = as_ndarray([1, 2, 3], dtype=np.float64)
empty = as_ndarray(None)  # array([], dtype=float64)

# Strings in byte fields, for C++/pybind11 interop: declare once which
# column holds each field's length, then read and write single rows
dt = np.dtype([('id', 'u4'), ('name_str', 'S1024'), ('name_len', 'u8')])
dt = with_field_metadata(dt, {'name_str': {'length_field': 'name_len'}})
materials = np.zeros(2, dtype=dt)
set_string(materials, 0, 'name_str', 'Structural steel S355')  # also sets name_len
name = get_string(materials, 0, 'name_str')                    # decodes this row only
overflow = check_overflow(materials, 'name_str')               # vectorized, decodes nothing

# Build an array incrementally without knowing the final size (append-only)
ga = GrowableArray(np.dtype([('id', 'i4'), ('value', 'f8')]))
ga.append((1, 1.5))
ga.extend([(2, 2.5), (3, 3.5)])
result = ga.to_numpy()  # independent, exact-size array you own

# Need to remove elements mid-build? DynamicArray adds soft deletion
da = DynamicArray(np.dtype('i8'))
da.extend([10, 20, 30, 40])
da.delete(1)            # elements shift: da[1] is now 30
clean = da.to_numpy()   # array([10, 30, 40])

These are typed, numpy-backed accumulators, not faster lists. For a method-by-method comparison, measured trade-offs, and guidance on when to use them versus a Python list, see docs/design/comparison.md; for usage recipes, docs/patterns.md. Reproduce the numbers with python benchmarks/bench_growable_array.py.


Module layout

Each category lives in its own module. All public functions are re-exported from vcti.nputils.

Module Functions
dtype_utils structured_dtype, flatten_dtype (+ flatten_record_dtype alias), merge_adjacent_fields, rename_fields
view_utils fields_view, drop_fields
join_utils join_struct_arrays
mapping_utils name_array, name_array_sparse
offset_utils position_array
coerce_utils as_ndarray
growable_array GrowableArray
dynamic_array DynamicArray
byte_utils get_string, set_string, check_overflow, check_terminator, byte_field_dtype, with_field_metadata, get_encoding, with_encoding, string_from_bytes, bytes_from_string, StringOverflowWarning, UnknownFieldMetadataWarning, ZERO_CHAR

Functions

Dtype construction & transformation

Function Purpose
structured_dtype(dtype, names) Build a structured dtype from a scalar or subdtype plus field names
flatten_dtype(dt, *, field_names, fmt, strip_plural) Expand array fields into scalars with flexible naming
flatten_record_dtype(dt, ...) Legacy alias for flatten_dtype
merge_adjacent_fields(dt, merges) Merge one or more groups of adjacent 'S' fields into a single field each (pure dtype view)
rename_fields(dt, mapping) Return a new dtype with fields renamed

Zero-copy views

Function Purpose
fields_view(sa, fields) View containing only the selected fields
drop_fields(sa, exclude) View containing all fields except those excluded

Joining

Function Purpose
join_struct_arrays(arrays) Join structured arrays horizontally by combining fields

Mapping, offsets, coercion

Function Purpose
name_array(nparray, enum_dict, default, *, max_lookup_bytes) Map integer values to names through a dense lookup array (keys that mostly fill their range)
name_array_sparse(nparray, enum_dict, default) Same result for keys scattered over a wide range; cost independent of the key range
position_array(counts, dtype) Convert count array to cumulative offset array
as_ndarray(value, dtype) Coerce None, list, or ndarray to ndarray

Containers

Type Purpose
GrowableArray(dtype, initial_capacity, growth_factor) Append-only growable numpy array: amortized O(1) append/extend (append_get_index returns the index), reserve/shrink_to_fit/clear, full/resize for sized fills, zero-copy as_array() view, and to_numpy() for an independent copy
DynamicArray(dtype, initial_capacity, growth_factor) Same growth model plus soft deletion: lazy delete (shifting semantics), compact, active_indices

Byte / string conversion (pybind11 interop)

Function Purpose
get_string(sa, row, field_name, *, length_field, encoding, on_overflow) Decode one row's string from the raw field bytes — exactly length bytes with a paired length column, else up to the first zero code unit (C semantics)
set_string(sa, row, field_name, value, *, length_field, encoding, on_overflow) Encode one row C++-style (text in at most itemsize - 1 bytes, rest zero-filled), set its length column, and return the original byte length
check_overflow(sa, field_name, length_field) Rows whose original byte length exceeded the field's payload — vectorized
check_terminator(sa, field_name) Rows whose terminator (last) byte is not \x00 — vectorized
byte_field_dtype(max_chars, encoding, bytes_per_char) Dtype of a field holding any string of max_chars characters (S<4 * (max_chars + 1)> by default, the C++ char[4 * N] rule), with its properties in the metadata
with_field_metadata(dtype, attrs, *, on_unknown) Attach encoding, bytes_per_char and length_field to fields — e.g. of an array from C++, via arr.view(...)
get_encoding(dtype, default) / with_encoding(dtype, encoding) Read / attach a scalar dtype's encoding
string_from_bytes(value, encoding) / bytes_from_string(value, length, encoding) Scalar primitives: one value to a string; a string to (field_bytes, original_length)
StringOverflowWarning, UnknownFieldMetadataWarning Warning categories for the "warn" policy
ZERO_CHAR The null character ("\x00")

Dependencies


Documentation

If you want to… Read
Get started using the package Quick Start above
See practical, real-world usage docs/patterns.md
Understand the architecture and design decisions docs/design.md
Choose between a container and a Python list docs/design/comparison.md
Navigate and understand the source docs/source-guide.md

Metadata

Release files for vcti-nputils 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vcti-nputils 2.0.0
File Size Uploaded
vcti_nputils-2.0.0.tar.gz 65.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for vcti-nputils 2.0.0
File Interpreter ABI Platform
vcti_nputils-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 106.1 kB

Release files / vcti_nputils-2.0.0.tar.gz

Download URL vcti_nputils-2.0.0.tar.gz
Size 65.2 kB
Tags Source
SHA-256 checksum
How to use checksums
6813ad383dc4d92493cea5ba3e41fd54ecf4f726ad065284f47bb01bb077bc6e
BLAKE2b-256 checksum
How to use checksums
f55152a82525bfb4a41264bf414ec023649990f560cc89481ed60ccd6c1f619c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / vcti_nputils-2.0.0-py3-none-any.whl

Download URL vcti_nputils-2.0.0-py3-none-any.whl
Size 40.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
445027a5bfc7f06acaa645287baaf7764e9d9bd1351240fac49608fb8e2b6f37
BLAKE2b-256 checksum
How to use checksums
7f9fd75f95b4b215e72870fe3acbab49c3c648f22fcda4bc70315d01125bf891
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page