A Python library and command-line tool for reading and inspecting EVIO (Event Input/Output) files from Jefferson Lab.
pyevio provides a Python API and a command-line interface for:
- Reading EVIO v6 files, record by record and event by event, through a memory map
- Reading the raw streaming files the JLab SRO hardware writes (8-word block headers, no file header)
- Decompressing LZ4, LZ4_BEST and gzip records on access
- Walking the bank tree of an event (banks, segments, tagsegments) with every length checked against its container
- Converting bank data to NumPy arrays
- Decoding CODA streaming fADC250 time frames into hit arrays and computing histograms and file checks over a whole file in one parallel pass
- Browsing a file — checks, plots, record table, structure tree — in a browser (
pyevio web, docs/web_viewer.md) or in the terminal over SSH (pyevio gui, docs/console_gui.md); the two share one statistics cache
The repository also holds rust/, a cargo workspace with evio-stream, a streaming read-only EVIO v6 reader in Rust that passes the same golden and SRO rulers (about 3 s for every hit of a 15.5 GB raw file on one thread), its evio-count and evio-dump tools, and evio-tui, a minimal terminal viewer. It is independent of the Python package.
| file | how it is read |
|---|---|
EVIO v6 file (file type id EVIO or HIPO in word 0, 14-word headers) |
native; dictionary and first event in the user header are skipped with their padding |
| Records compressed with LZ4, LZ4_BEST or gzip | decompressed on first access into a per-record buffer |
| A trailer record with or without an index | exposed as EvioFile.trailer; it yields no events |
| JLab SRO hardware stream: no file header, 8-word block headers with version word 4 and no bit-info bits | native, as header_layout == "sro-8word"; a final block cut by the end of the file keeps its complete events and is reported in checks |
| EVIO v4 or v1—3 file written by the evio library | ValueError naming the version and pointing at the evio tools (evio2xml, eviocopy) |
| EVIO v6 record stream without a file header (an evio writer buffer dump) | native, as header_layout == "v6-records"; records start at offset 0 and EvioFile.header is a stand-in |
The SRO detection walks the whole chain of 8-word headers at open: a chain in which some header carries a bit-info bit (last-record, dictionary) is an evio-library v4 file and raises the version error. The format details are in docs/evio6_file_structure.md (container format) and docs/stream_decoding.md (streaming fADC250 payload).
pip install pyevioThe browser viewer installs as an extra: pip install 'pyevio[web]' (see docs/web_viewer.md); the console viewer as pip install 'pyevio[gui]' (see docs/console_gui.md).
Requirements: Python 3.9 or later. Dependencies installed with the package: click, lz4, rich, numpy, matplotlib.
Objects nest the way the file does: EvioFile → Record → Event → Bank, Segment, TagSegment.
from pyevio import EvioFile
with EvioFile("run.evio") as evio_file:
print(evio_file.header_layout) # "v6" or "sro-8word"
print(evio_file.record_count) # data records; the trailer is evio_file.trailer
print(evio_file.get_total_event_count())
print(evio_file.checks) # [] for a clean file
record = evio_file.get_record(0)
for event in record.get_events():
root = event.get_bank() # the event's top-level bank
for child in root.get_children(): # Bank, Segment or TagSegment, typed by the parent
print(hex(child.tag), child.data_type, child.length)
data = root.to_numpy() # None for a container bankrecord_count,get_record(i),iter_records()cover the data records. The trailer record (header type 3) isevio_file.trailer, orNonewhen the file ends in a plain record.Record.is_trailertells the two apart.Record.get_events()buildsEventobjects;Record.get_event_offsets()returns(offset, length)pairs without building objects, which is the way to walk a large file.- For an uncompressed record, offsets are file offsets and
Record.bufferis the file mmap. For an LZ4, LZ4_BEST or gzip record,Record.bufferis the decompressed bytes and every offset indexes that buffer. Read event bytes throughrecord.buffer;Record.release()drops the decompressed buffer and the Event cache. EvioFilecaches oneRecord(the last accessed), so a compressed file never sits in memory whole.- A final record whose declared length runs past the end of the file stays the last record:
Record.is_truncatedis true,record.header.event_countis the header's claim andlen(record.get_event_offsets())is the number of complete events present.record_countcounts that record.
EvioFile.checkslists what the record scan found wrong with the file:("truncated_tail", declared_bytes, present_bytes, events_salvaged)and("bad_record_header", offset, message)where the scan stopped on bytes that are no record header (garbage, a byte-swapped header, a second file header of two concatenated files).Record.errorsandBank.errorslist why an event or child walk stopped early: a length that runs past its container, a decompression failure. A bank whose declared length passes its parent is clamped to the parent; no read runs past a container because a length word said so.
close() or the end of the with block releases the mmap and the file. A numpy array or memoryview that still refers to the mmap makes close() raise BufferError after the file is closed; drop the views (del) and call close() again. Such views come from np.frombuffer on the mmap, for example fadc250_stream.event_words() on an uncompressed record; Bank.to_numpy() and Bank.get_data() return copies.
A length word counts the words that follow it: a structure with length N occupies N + 1 words. Bank.size, data_length and end_offset follow that rule, and Bank.get_children() types the children by the parent's data type (banks of banks hold Bank, banks of segments hold Segment, banks of tagsegments hold TagSegment).
pyevio.decoders.fadc250_stream holds the one copy of the frame walk and the hit-word layout. It works on uint32 event words and returns numpy columns (frame, roc, stream, port, channel, time_raw, t_ns, charge) plus per-frame, per-ROC-bank and per-empty-bank tables. Only ports the payload map names as fADC250 are decoded as hits; other modules (DCRB) are counted as words.
from pyevio import EvioFile
from pyevio.decoders import fadc250_stream as fs
with EvioFile("sro_000791.evio.00000") as evio_file:
record = evio_file.get_record(0)
buffer = record.buffer
events = [fs.event_words(buffer, offset, length, evio_file.header.endian)
for offset, length in record.get_event_offsets()]
hits = fs.decode_frames(events)
print(hits.n_stream_frames, hits.n_hits, hits.frames["frame"][:4])
del events # mmap views; close() refuses while they livepyevio.stream_stats runs the decoder over a whole file in parallel (one worker per record range) and returns StreamStats: rate, charge and time-in-frame histograms per (ROC, port, channel), occupancy, hits per frame, empty banks per port, and frame checks (stride, missing and duplicate frames, out-of-order frames, timestamp-step outliers). The result round-trips through .npz.
from pyevio.stream_stats import compute_stream_stats, StreamStats
stats = compute_stream_stats("sro_000791.evio.00000", workers=8)
print(stats.n_frames, stats.n_hits, stats.checks.stride, stats.checks.missing_frames)
print(stats.rocs, stats.ports) # axes of every histogram
rate = stats.rate_hz(roc=7) # Hz per time bin, summed over ports and channels
charge = stats.per_roc(stats.charge_hist) # (roc, 128) counts; edges in stats.charge_edges
stats.save("run791.npz")
stats = StreamStats.load("run791.npz") # rejects a file written by another stats versionThe frame layout, the hit word, the time units and the checks are described in docs/stream_decoding.md. examples/fadc250_stream_to_parquet.py writes the hits of a file to Parquet with the same decoder.
pyevio --help lists the commands; pyevio COMMAND --help lists the options of one command.
pyevio info run.evio # header fields and a record table
pyevio info run.evio --full # every record row
pyevio info run.evio --hexdumppyevio record run.evio 5 # record 5: header, events, tag statistics
pyevio record run.evio 5 --analyze # event size distribution
pyevio event run.evio 25 # event 25 by global index
pyevio event run.evio 2 --record 3 # event 2 of record 3
pyevio event run.evio 25 --hexdumppyevio dump run.evio # words of event 2 (the default start) as hex
pyevio dump run.evio 100 --start 200 # 100 events from global index 200
pyevio dump run.evio 100 --record 5 # 100 events of record 5
pyevio dump run.evio --as dec --only 0,1,2pyevio stats run.evio # totals, frame checks, per-ROC table
pyevio stats run.evio --out run.npz # save the histograms
pyevio stats run.evio --workers 8 --records 200 --tick-ns 1 --rate-bins 256pyevio debug run.evio --record 1 # frame summary per event
pyevio debug run.evio --record 1 --event 0 # one event, every ROC bank
pyevio debug run.evio --record 1 --payload 0 --hexdumppyevio hex run.evio 256 --bytes # 30 words from byte offset 256 (decimal offsets only)
pyevio hex run.evio 200 --size 50 # 50 words from word 200
pyevio hex run.evio 256 --bytes --endian '<' # the default is big-endianpyevio ana run.evio --per-record # event tag statistics over the file
pyevio gui run.evio # console viewer (Textual), see docs/console_gui.md
pyevio gui /path/to/runs # the same, picking a file from a folder
pyevio web /path/to/runs # browser viewer, see docs/web_viewer.md
pyevio web run.evio # the same, opening this file at oncegit clone https://github.com/JeffersonLab/pyevio
cd pyevio
uv sync # installs the package and the dev group (pytest, pyyaml, pyarrow)
uv run pytest -qTwo test sets need data outside the repository and skip when it is absent:
-
The golden-set harness (
tests/golden/test_golden.py) reads the evio golden manifest from the path inGOLDEN_MANIFESTand the files from thegolden_rootthe manifest names. One test runs per manifest row: event counts, no event past the end of the file, and the version error for v4 and v1—3 files. -
The SRO corpus tests (
tests/golden/test_corpus.py) scan 15 GB files and run only withPYEVIO_CORPUS=1:PYEVIO_CORPUS=1 uv run pytest tests/golden/test_corpus.py -v
[License information]
[Contributor information]