Skip to main content

oxihipo (Python)

Documentation

Fast, columnar reading and writing of HIPO (CLAS12) files, powered by the Rust oxihipo core. A HIPO bank reads like a uproot jagged branch, and columns come back as Awkward arrays — built zero-copy from buffers the Rust side fills with the GIL released. Writing is columnar too: create a new file, or recreate to decorate an existing one with a derived bank.

import oxihipo as ox

f = ox.open("run5042.hipo")                 # file | dir | glob | list of paths
f.num_entries                               # event count
f.keys()                                    # ['REC::Particle', 'REC::Event', ...]

p = f.arrays("REC::Particle", ["pid", "px", "py", "pz"])
p.px                                        # jagged: p[event].px indexes particles
ak.sum(p.px, axis=1)                         # per-event reductions, no Python loop

See examples/ for runnable scripts (quickstart.py, analysis.py, streaming.py, parallel.py, rdataframe.py, and the bench_*.py benchmarks).

Reading

call returns
f.arrays(bank, [cols]) ak.Array — jagged record N * var * {col: T}
f.arrays([bankA, bankB]) / f.arrays(filter_name="REC::*") record with one field per bank
f.array(bank, col) one column, N * var * T
f.numpy(bank, col) (values, offsets, inner_len) — plain NumPy, no Awkward import
f.event_tags() per-event tag (EH_TAG) as uint32[n_events] — aligned 1:1 with arrays()
f["REC::Particle"] a bank proxy: .keys(), .typenames(), .array(col), ["col"]
f["REC::Particle/px"] the px column

Common knobs (on arrays / array / numpy / iterate):

  • entry_start=, entry_stop= — restrict to a global event range.
  • filter_name="REC::*" — glob over bank / bank/column keys.
  • library="ak" (default, ak.Array), "np" (dict of object-dtype ndarray), "pd" (pandas, one frame per bank), "arrow" (pyarrow.Table, one large_list column per field — for polars / duckdb). A non-matching filter_name / empty bank list yields an empty result, not an error.
  • threads=0 = all cores (default), 1 = sequential, n = n-thread pool.
  • workers= — read with N processes for I/O-bound filesystems; see Parallel reading.

Streaming (bigger than RAM)

iterate yields the chain in fully-materialized chunks; each is dropped before the next is read, so resident memory stays ≈ one chunk.

for chunk in f.iterate("REC::Particle", ["px"], step_size="200 MB"):
    hist.fill(ak.flatten(chunk.px))

for chunk, report in f.iterate("REC::Particle", step_size=1_000_000, report=True):
    ...  # report.entry_start / report.entry_stop / report.file_path

# multi-file, never opens it all at once:
for chunk in ox.iterate("/data/run5042/*.hipo", "REC::Particle", step_size="1 GB"):
    ...

step_size is an event count (int) or a byte budget ("200 MB", "1 GB"); chunks are aligned to record and file boundaries.

Parallel reading (multi-process)

On a parallel filesystem (JLab ifarm /volatile, Lustre) a single process saturates well below the filesystem's aggregate bandwidth — the limit is per-process, not per-node. workers=N splits the chain into N disjoint, record-aligned event ranges, reads them from N separate processes, and stitches the result — turning one I/O stream into N.

# whole-array read, N processes, stitched into one ak.Array:
a = ox.arrays("/volatile/run5042/*.hipo", "REC::Particle", ["px", "py", "pz"], workers=8)

# streaming, ~N reads in flight (resident memory ≈ N chunks), yielded in order:
for chunk in ox.iterate("/volatile/run5042/*.hipo", "REC::Particle", step_size="1 GB", workers=8):
    ...
  • Works with everything else: filter_name, entry_start/entry_stop, library=, and .filtered(...) all carry through to the workers.
  • Without an explicit threads=, the machine's cores are split across the workers (total ≈ all cores); on an I/O-bound farm the surplus decode threads simply wait on the read.
  • This helps only when I/O is the bottleneck. On a local, already-cached disk the limit is decode/bandwidth, not I/O, so workers>1 just adds process and IPC overhead — keep the default workers=1 there.
  • Each arrays(workers=N) / iterate(workers=N) call spins up its own worker pool, so pay the spawn cost once: prefer a single iterate(...) over a many-file chain to a loop of small arrays() calls.

Required: any script that passes workers= must be guarded by if __name__ == "__main__":. Workers are spawned (not forked — forking after Rust's thread pool exists is unsafe), so each re-imports your script; without the guard it would re-run at import. See examples/parallel.py.

Filtering and skimming

g = f.filtered(require=["REC::Particle"])           # events carrying a bank
g = f.filtered(record_tag=[0x42])                   # by record tag
g = f.filtered(event_tag=[1, 4])                    # by per-event tag (EH_TAG)
g = f.filtered(event_tag="dvcs")                    # by tag name (if the file has a registry)
summary = g.skim("electrons.hipo", compression="lz4percolumn")   # SkimSummary(events, records, bytes)

filtered() returns a new chain; the filter reduces what arrays() / skim() yield (its num_entries stays the pre-filter total, as in uproot).

Writing

create opens a new file; recreate decorates an existing one. Both return a columnar Writer with an uproot-style new_bank / extend / close API — columns are written zero-copy from NumPy or Awkward, with the GIL released.

with ox.create("out.hipo", compression="lz4percolumn") as w:
    w.new_bank("NEW::bank", {"px": "F", "pid": "I", "cov": "F#3"})   # scalars + T#N arrays
    w.extend({"NEW::bank": {                                          # a batch of events
        "px":  ak.Array([[1.0, 2.0], [], [3.0]]),                    # jagged: rows per event
        "pid": ak.Array([[11, -11], [], [211]]),
        "cov": ak.Array([[[1, 2, 3], [4, 5, 6]], [], [[7, 8, 9]]]),  # 3-vector per row
    }})
  • new_bank(bank, {col: typechar}) — declare a bank; typecharB/S/I/L/F/D, optionally #N for a fixed-length array column ("F#3"). The unique item auto-assigns.
  • extend({bank: data}) — append a batch. data is an ak.Array record (what arrays(bank) returns) or a dict of columns — a jagged ak.Array per column, or a 1-D NumPy array for a scalar-per-event bank. Call it in a loop to stream large outputs in bounded memory.
  • close() (or leaving the with) writes the trailer index and returns a SkimSummary.

Decorate — add a bank to a cooked file without rewriting the physics banks (an ML score, a computed kinematic):

f = ox.open("dst.hipo")
scores = model.predict(f.arrays("REC::Particle")).astype("float32")   # one per event

w = ox.recreate("dst.hipo", "decorated.hipo")   # or dst=None to replace in place
w.new_bank("ML::pred", {"score": "F"})
w.extend({"ML::pred": {"score": scores}})        # aligned 1:1 with the source events
w.close()

Every source event is copied verbatim (existing banks, array columns included), with the new banks attached; they must cover all source events (close errors otherwise). Full guide: Writing.

RDataFrame (ROOT)

rdataframe hands a selection to ROOT's RDataFrame through Awkward's generated RDataSource — a jagged bank column becomes an RVec<T>, a T#N array column a nested RVec, no copy of the view. Column names are the bank/column keys sanitized to C++ identifiers (REC::Particle/pxREC_Particle_px).

df = ox.rdataframe("run5042.hipo", "REC::Particle", ["px", "py", "pid"])
h = df.Define("pt", "sqrt(REC_Particle_px*REC_Particle_px"
                   " + REC_Particle_py*REC_Particle_py)").Histo1D("pt")

# bigger than RAM: one RDataFrame per chunk, merge histograms across chunks
total = None
for chunk in ox.iterate_rdataframe("run5042.hipo", "REC::Particle", ["px"], step_size="1 GB"):
    h = chunk.Histo1D(("pt", "", 100, 0, 10), "REC_Particle_px").GetValue()
    total = h.Clone() if total is None else (total.Add(h) or total)
    total.SetDirectory(0)

Needs a working ROOT/PyROOT (not on PyPI — conda-forge or system) plus awkward; pip install oxihipo[root] covers the awkward side. filter_name, entry_start/entry_stop, and .filtered(...) all carry through. See examples/rdataframe.py and the RDataFrame guide.

The bridge is a no-copy viewrdataframe costs ~1 ms over the bare arrays read. But the RDF loop is single-threaded here (implicit MT doesn't work with the Awkward-generated source), so on a simple kernel it runs slower than the vectorized Awkward equivalent: use it to reuse RDF/C++ code, not for speed. Numbers

Discovery

f.keys()                       # bank names
f.keys(recursive=True)         # 'bank/column' keys
f.keys(filter_name="REC::*")   # globbed
f.typenames()                  # {'REC::Particle/px': 'float32', 'REC::Track/cov': 'float32[3]'}
"REC::Particle" in f

How it works

The whole per-event loop runs in Rust with the GIL released. One pass over the file materializes each requested column into a flat NumPy buffer plus one shared int64 offsets buffer per bank — exactly an Awkward ListOffsetArray / Index64 layout — moved into NumPy zero-copy. The Python layer only wraps those buffers (NumpyArray / RegularArray for T#N array columns / ListOffsetArray), so nothing is copied past decompression and Python never iterates events. Errors map onto a Python exception tree (KeyError for a missing bank/column, TypeError for a dtype mismatch, OSError for I/O, oxihipo.CorruptFileError for a malformed record).

Performance

Reading through the binding runs within ~10% of native Rust — the per-event decode is Rust behind a released GIL, and columns move into NumPy zero-copy. On a 9.1 GB CLAS12 file (598k events, Apple M4 Pro, all cores), f.arrays("REC::Particle", ["px","py","pz","pid"]) reads at ~5.6 GB/s vs Rust's 6.3 GB/s. Details + reproduction: Python vs Rust benchmark and examples/bench_columns.py.

Build

Requires the Rust toolchain and maturin.

cd py
maturin develop --release        # build + install into the active venv
# or: maturin build --release     # produce an abi3 wheel under target/wheels

The extension is built with pyo3 0.29 and rust-numpy 0.29, with an abi3-py313 floor — so one abi3 wheel per OS/arch works across CPython ≥ 3.13. pyo3 0.29 supports current CPython natively; only for an interpreter newer than it knows do you need PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1.

Dependencies

Each extra pulls in everything its backend actually imports, so a single pip install oxihipo[<extra>] gives a working backend:

  • numpy >= 1.24 (required)
  • oxihipo[awkward]awkward >= 2.6, for array / arrays (library="ak")
  • oxihipo[pandas] — awkward + pandas, for library="pd"
  • oxihipo[arrow]pyarrow >= 14, for library="arrow" (assembled directly with pyarrow — no awkward needed on the polars / duckdb path)
  • oxihipo[root]awkward for rdataframe / iterate_rdataframe; plus a working ROOT/PyROOT, which is not on PyPI (install via conda-forge or system)
  • oxihipo[all] — awkward + pandas + pyarrow

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oxihipo-0.1.0.tar.gz (456.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

oxihipo-0.1.0-cp313-abi3-win_amd64.whl (528.3 kB view details)

Uploaded CPython 3.13+Windows x86-64

oxihipo-0.1.0-cp313-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (648.6 kB view details)

Uploaded CPython 3.13+manylinux: glibc 2.17+ x86-64

oxihipo-0.1.0-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (621.3 kB view details)

Uploaded CPython 3.13+manylinux: glibc 2.17+ ARM64

oxihipo-0.1.0-cp313-abi3-macosx_11_0_arm64.whl (570.1 kB view details)

Uploaded CPython 3.13+macOS 11.0+ ARM64

oxihipo-0.1.0-cp313-abi3-macosx_10_12_x86_64.whl (612.3 kB view details)

Uploaded CPython 3.13+macOS 10.12+ x86-64

File details

Details for the file oxihipo-0.1.0.tar.gz.

File metadata

  • Download URL: oxihipo-0.1.0.tar.gz
  • Upload date:
  • Size: 456.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for oxihipo-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5c0079cc5f3783215462eb8821a8576a0dcd674335406c9d2d3f6a40aba4530c
MD5 fe9ae698430daf4540327b06572e448e
BLAKE2b-256 732edd2c94d185c53f809ad0a1237b1d2c5817186bcb6fdaf35993a50ccffce5

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxihipo-0.1.0.tar.gz:

Publisher: wheels.yml on mathieuouillon/oxihipo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oxihipo-0.1.0-cp313-abi3-win_amd64.whl.

File metadata

  • Download URL: oxihipo-0.1.0-cp313-abi3-win_amd64.whl
  • Upload date:
  • Size: 528.3 kB
  • Tags: CPython 3.13+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for oxihipo-0.1.0-cp313-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 38e30f5e11b0c51d26b0ed55a7447c804b995575d3a8b4d67f08ec3231b834ea
MD5 8c905b79f8d7448cbe9c14a0cd2a926e
BLAKE2b-256 bc0deaaa3bd09e537ed659b8dca0c91d1cb1510d2a8996586805ac229d6ad915

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxihipo-0.1.0-cp313-abi3-win_amd64.whl:

Publisher: wheels.yml on mathieuouillon/oxihipo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oxihipo-0.1.0-cp313-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for oxihipo-0.1.0-cp313-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 1768ac41bb6096f9d7784e094ab1d4c5629b7682377b365347052e880a3880e6
MD5 ff2320a7a6114721f713e5fe809b44af
BLAKE2b-256 5c6761eb88f66efbfe983532283565f90dea015dadc08e9e34f9ffee98443762

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxihipo-0.1.0-cp313-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: wheels.yml on mathieuouillon/oxihipo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oxihipo-0.1.0-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for oxihipo-0.1.0-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 078ddbb014ef65497c6141c743c77ac6c312b9797d56ae590e465f4f0c3d115b
MD5 c2165799a56022d42209642ac30d7c08
BLAKE2b-256 c40570a2d14c9fd4db8d3e99ebe1259649f8110b95ed9e68c1631607cffc8abc

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxihipo-0.1.0-cp313-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: wheels.yml on mathieuouillon/oxihipo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oxihipo-0.1.0-cp313-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for oxihipo-0.1.0-cp313-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 57e718fb4349fab0cbd2520fb0287e728a0417a06686a60931d69e51bfea94e8
MD5 f6ba06643aca2a146d2f257a06a6187a
BLAKE2b-256 b2cfcdf1b9410991f9c1e3b18fb6b170dae4d1a44e4b50065e4f2aca14f0d984

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxihipo-0.1.0-cp313-abi3-macosx_11_0_arm64.whl:

Publisher: wheels.yml on mathieuouillon/oxihipo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oxihipo-0.1.0-cp313-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for oxihipo-0.1.0-cp313-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 d456f1e9bf01295568aed1a7f80867c3473c0fab1d93ceaffbfe9a4cb769690c
MD5 5a01c6a1cb57e87e2ce9d28b80874520
BLAKE2b-256 c7faff8992ff611250ea17c6ce9265676de8423efd9c1bd722da69f900d2480c

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxihipo-0.1.0-cp313-abi3-macosx_10_12_x86_64.whl:

Publisher: wheels.yml on mathieuouillon/oxihipo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page