Skip to content

Python API reference

Auto-generated from the source docstrings. For a task-oriented walkthrough see the Python guide.

The native API:

mef3io.Reader

Reader(path: str, password: str = '', backend: str = 'cpp', n_threads: int = 0, cache=None, warn_declarations: bool = True)

Read-only interface to a MEF 3.0 session.

Times throughout are uUTC (microseconds since the Unix epoch). Windowed reads fetch only the bytes they need, so they stay cheap on huge sessions.

Parameters:

Name Type Description Default
path str

Path to the .mefd session directory, or to an uncompressed tar archive of one (name.mefd.tar, from :func:mef3io.archive_session) — tar sessions are read in place, without extraction. The suffix is enforced: anything else is refused, so a stray directory or file can never be misread as a session.

required
password str

Password for encrypted sessions. A level-2 password unlocks everything; a level-1 password reads the signal and technical metadata but leaves subject metadata locked. Empty for unencrypted sessions.

''
backend ('cpp', 'pure')

Which implementation to use. Defaults to the C++ backend; the pure backend is not yet implemented.

"cpp"
n_threads int

Worker threads for RED block decoding. 0 (default) uses all cores, 1 is serial. Output is byte-identical regardless of thread count.

0
cache str or None

Opt-in warm-start cache for channel metadata. None (default) disables it; "auto" uses the per-user OS cache directory; a path makes it persistent. Warm opens serve :attr:channels / :meth:info without touching the session tree.

None
warn_declarations bool

Emit a :class:SessionDeclarationWarning when the session leaves section-2 size declarations unset. The check is free — the metadata is already parsed at open — and never affects the data read here; it flags a file that can mislead other MEF readers. Set False, or filter the warning category, to silence it.

True

Examples:

>>> with mef3io.Reader("session.mefd") as r:
...     x = r.read(r.channels[0], t0, t1)   # float64, NaN in gaps

declaration_issues property

declaration_issues: list

Section-2 size declarations this session leaves unset.

The structured form of the :class:SessionDeclarationWarning raised on open: a list of {"channel", "segment", "field"} dicts, empty when the session declares everything. Free — computed at open from metadata that was already parsed. It sees only what is missing; use :class:mef3io.Validator to find what is merely wrong.

channels property

channels: list[str]

Channel names in the session, sorted.

Returns:

Type Description
list of str

metadata property

metadata

Session subject/acquisition metadata as a :class:mef3io.Metadata.

Built from the first channel (metadata is session-wide). Subject fields are empty unless the reader was opened with a level-2 password.

Returns:

Type Description
Metadata

close

close() -> None

Release the backend and its file handles.

After this the reader is CLOSED: a further read raises rather than quietly reopening the session. A caller who closed to release handles — before archiving the directory, or to stay under an fd limit — got them back without notice, which is the opposite of what they asked for. Closing twice is harmless.

info

info(channel: str) -> dict

Channel metadata.

Parameters:

Name Type Description Default
channel str

Channel name.

required

Returns:

Type Description
dict

Keys: sampling_frequency (Hz), units_conversion_factor, units_description, start_time / end_time (uUTC), number_of_samples (stored samples — NaN gaps are not counted, so a gridded :meth:read usually returns more), recording_time_offset, n_segments, section3_available, and the section-3 subject fields (subject_name_1 / subject_name_2 / subject_id / recording_location; None without level-2 access).

read

read(channel: str, t0: Optional[int] = None, t1: Optional[int] = None, n_threads: Optional[int] = None) -> np.ndarray

Read float64 samples on the uniform sampling grid.

Parameters:

Name Type Description Default
channel str

Channel name.

required
t0 int or float

Half-open time window [t0, t1) in uUTC. Defaults span the whole channel. The number of samples returned is round((t1 - t0) * fs / 1e6).

None
t1 int or float

Half-open time window [t0, t1) in uUTC. Defaults span the whole channel. The number of samples returned is round((t1 - t0) * fs / 1e6).

None
n_threads int

Per-call override of the reader's thread count (0 = all cores, 1 = serial). None (default) uses the reader default.

None

Returns:

Type Description
ndarray

1-D float64 array on the sampling grid. Discontinuity gaps are filled with NaN; values are scaled by the channel's units-conversion factor.

See Also

read_raw : the unscaled int32 form with an explicit validity mask.

read_raw

read_raw(channel: str, t0: Optional[int] = None, t1: Optional[int] = None, n_threads: Optional[int] = None) -> dict

Read the stored int32 counts with an explicit validity mask.

Parameters:

Name Type Description Default
channel str

Channel name.

required
t0 int or float

Half-open [t0, t1) window in uUTC; defaults span the channel.

None
t1 int or float

Half-open [t0, t1) window in uUTC; defaults span the channel.

None
n_threads int

Per-call thread-count override (see :meth:read).

None

Returns:

Type Description
dict

Keys: samples (int32 ndarray, on the grid), valid (uint8 ndarray; 0 marks gap samples with no data), start_uutc, sampling_frequency, units_conversion_factor. Physical units are samples * units_conversion_factor where valid.

segments

segments(channel: str) -> list[dict]

Per-segment map of a channel — what data is where.

Read from metadata only (nothing is decoded), so it is cheap even for huge, gap-riddled sessions. Use it to locate data across large recording gaps, then :meth:toc for the block-level view within a segment.

Parameters:

Name Type Description Default
channel str

Channel name.

required

Returns:

Type Description
list of dict

One dict per segment (sorted by segment number) with keys segment, start_time / end_time (uUTC), start_sample (channel-wide index of the first sample), number_of_samples, number_of_blocks, and the on-disk path.

toc

toc(channel: str) -> list[dict]

Block-level table of contents, for seeking and viewers.

Parameters:

Name Type Description Default
channel str

Channel name.

required

Returns:

Type Description
list of dict

One dict per RED block with start_uutc, start_sample, number_of_samples, maximum_sample_value, minimum_sample_value, and discontinuity (True when the block does not continue seamlessly from the previous one).

records

records(channel: Optional[str] = None) -> list[dict]

Read records (annotations).

Parameters:

Name Type Description Default
channel str or None

Channel name for channel-level records, or None (default) for session-level records.

None

Returns:

Type Description
list of dict

One dict per record with type (e.g. "Note", "EDFA"), time (uUTC), optional text and duration.

mef3io.Writer

Writer(path: str, overwrite: bool = False, password1: str = '', password2: str = '', units: Optional[str] = None, block_length: Optional[int] = None, n_threads: int = 0, metadata=None, durability: str = 'fast')

Write a MEF 3.0 session.

Times throughout are uUTC (microseconds since the Unix epoch). The first write to a channel creates segment 0; later writes append in-segment (extending the existing files) unless new_segment=True.

Parameters:

Name Type Description Default
path str

Path to the .mefd session directory to create or extend (the suffix is enforced). Tar archives are read-only sessions: a .tar path raises — write a directory and pack it with :func:mef3io.archive_session.

required
overwrite bool

True deletes any existing session first. False (default) reopens an existing session for appending — state is recovered from disk, so appends work across program runs.

False
password1 str

Level-1 and level-2 passwords. MEF has no "level-1 only" files, so to encrypt a session pass both; leave both empty for no encryption.

''
password2 str

Level-1 and level-2 passwords. MEF has no "level-1 only" files, so to encrypt a session pass both; leave both empty for no encryption.

''
units str or None

Physical units label stored in metadata (e.g. "uV").

None
block_length int or None

RED block size in samples. None (default) derives it from fs (fs samples for fs >= 5000, else 10*fs).

None
n_threads int

Worker threads for RED encoding (0 = all cores, 1 = serial). Output is byte-identical regardless of thread count.

0
metadata Metadata or dict

Session-wide subject/acquisition metadata written to every channel. Also settable later via :meth:set_metadata (before writing).

None
durability ('fast', 'full')

How hard an append works to survive an unclean shutdown.

"fast" (default) performs no flushes. These files are built by appending for days to months, so the append is the hot path, and the barriers cost roughly 2.5x on it. It matches what every MEF writer before this one did — the reference C library flushes nothing.

What that costs: the order in which the .tdat, .tidx and .tmet updates reach the disk is not enforced, so a crash mid-append can leave the index referencing data that never landed, blocks present in .tdat that the index never mentioned, or a torn tail on either. Every one of those is detected by :func:mef3io.validate_session and repaired by :func:mef3io.recover_session, which rebuilds missing index entries from the RED block headers. So the failure mode is "run recover after an unclean shutdown", not "lose the recording" — and data already committed by an earlier append is never at risk.

"full" adds the barriers: the .tdat is flushed before the .tidx that references it, and each header before it publishes what was flushed. An append then becomes all-or-nothing across the three files with no recovery step, at ~3 flushes per channel per append. Worth it when an unclean shutdown must need no operator action.

A fresh write is unaffected either way — there is nothing underneath it to lose.

"fast"

Examples:

>>> with mef3io.Writer("session.mefd", overwrite=True, units="uV") as w:
...     w.write("ch1", data, start_uutc, fs=256.0)   # NaN marks gaps

set_metadata

set_metadata(metadata) -> None

Set session-wide subject/acquisition metadata (a :class:mef3io.Metadata, or a flat dict of fields). Call before writing; applies to every channel.

close

close() -> None

Finalize and release the writer. Called automatically on context-manager exit.

write

write(channel: str, data: ndarray, start_uutc: int, fs: float, precision: int = -1, new_segment: bool = False) -> dict

Write float data; NaN runs become discontinuity gaps.

Values are quantized to int32 counts as round(data * 10**precision) with the conversion factor 10**-precision kept in metadata. NaN samples are not stored — they read back as NaN.

Parameters:

Name Type Description Default
channel str

Channel name (created on first write).

required
data array_like

Any numeric input; coerced to float64 (lists, int arrays, float32 all accepted).

required
start_uutc int or float

Timestamp of the first sample, uUTC.

required
fs float

Sampling frequency in Hz.

required
precision int

Decimal precision for quantization. -1 (default) infers it from the data — or, when appending, reuses the segment's stored precision so the append cannot conflict.

-1
new_segment bool

Force a fresh segment instead of appending in-segment.

False

Returns:

Type Description
dict

samples_written, blocks, gaps_skipped, segment.

Raises:

Type Description
RuntimeError

On an append conflict (fs / conversion-factor mismatch, or data starting before the segment's end).

write_int32

write_int32(channel: str, data: ndarray, ufact: float, start_uutc: int, fs: float, valid: Optional[ndarray] = None, new_segment: bool = False) -> dict

Write integer counts verbatim with a conversion factor (bit-exact).

The primitive path: counts are stored exactly as given, with ufact (e.g. an amplifier's volts-per-bit) in metadata. Physical units on read are counts * ufact.

Parameters:

Name Type Description Default
channel str

Channel name (created on first write).

required
data array_like of int

Integer counts (any integer width). Stored bit-exact.

required
ufact float

Conversion factor from counts to physical units.

required
start_uutc int or float

Timestamp of the first sample, uUTC.

required
fs float

Sampling frequency in Hz.

required
valid array_like or None

Same length as data; any numeric/bool mask where nonzero marks a real sample and zero a discontinuity gap. None = all valid.

None
new_segment bool

Force a fresh segment instead of appending in-segment.

False

Returns:

Type Description
dict

samples_written, blocks, gaps_skipped, segment.

Raises:

Type Description
TypeError

If data is floating-point (use :meth:write for float data — counts are never silently rounded here).

ValueError

If any value is outside the int32 range (it would wrap).

RuntimeError

On an append conflict.

write_annotations

write_annotations(annotations, channel: Optional[str] = None) -> None

Write records (annotations).

Replaces the records at the given level. In encrypted sessions record bodies are level-2 encrypted.

Parameters:

Name Type Description Default
annotations iterable of dict or pandas.DataFrame

Each record needs time (uUTC); type defaults to "Note"; text and duration are optional. A DataFrame with those columns is accepted.

required
channel str or None

Channel name for channel-level records, or None (default) for session-level records.

None

mef3io.archive_session

archive_session(session_path, tar_path: Optional[str] = None, overwrite: bool = False) -> str

Pack a session directory into a single uncompressed tar archive.

The archive (conventionally name.mefd.tar) is a plain ustar file: :class:Reader opens it directly — no extraction — and any tar tool (tar -xf) reproduces the original directory. Because it is uncompressed, windowed reads still fetch only the byte ranges they need. The source directory is left untouched; output is deterministic, so archiving the same session twice yields identical bytes. Tar sessions are read-only: :class:Writer refuses .tar paths.

Parameters:

Name Type Description Default
session_path str or path - like

The .mefd session directory to pack (the suffix is enforced).

required
tar_path str

Target archive path; must end .mefd.tar. Default derives <session_path>.tar (name.mefd becomes name.mefd.tar).

None
overwrite bool

Replace an existing target archive. Default False (an existing target raises).

False

Returns:

Type Description
str

Path of the created archive.

Examples:

>>> tar = mef3io.archive_session("session.mefd")
>>> with mef3io.Reader(tar) as r:
...     x = r.read(r.channels[0])

mef3io.extract_session

extract_session(tar_path, dest_dir: Optional[str] = None, overwrite: bool = False) -> str

Unpack a session archive back into a .mefd directory.

The inverse of :func:archive_session — after extraction the directory is a normal writable session again. The session root inside the archive is stripped, so dest_dir becomes the session directory itself; archives from foreign tar tools work too. A failed extraction never leaves a half-written directory behind.

Parameters:

Name Type Description Default
tar_path str or path - like

The session archive to unpack; must end .mefd.tar.

required
dest_dir str

Target directory; must end .mefd. Default strips the .tar suffix (name.mefd.tar becomes name.mefd next to the archive).

None
overwrite bool

Replace an existing target directory. Default False (an existing target raises).

False

Returns:

Type Description
str

Path of the extracted session directory.

Examples:

>>> session = mef3io.extract_session("session.mefd.tar")
>>> with mef3io.Writer(session) as w:      # writable again
...     w.write("ch1", more_data, t, fs=256.0)

Validation and repair

See the validation guide for what each check means.

mef3io.Validator

Validator(path: str, password: str = '', channels: Sequence[str] | None = None, segments: Sequence[int] | None = None, exact_difference_bytes: bool = True)

Check one MEF 3.0 session. Read-only: it cannot write a byte.

There is no repair method here by design — see :func:repair_session.

Parameters:

Name Type Description Default
path str

A .mefd directory or a .mefd.tar archive.

required
password str

Needed to read section 2 of an encrypted session.

''
channels sequence of str

Restrict to these channels. Default: every channel.

None
segments sequence of int

Restrict to these segment numbers. Default: every segment.

None
exact_difference_bytes bool

Read every RED block header to learn the real maximum_difference_bytes. It is the one declaration not derivable from the block index, so the exact answer costs one small read per block. Set False on very large sessions: the check then only reports a clearly-unset value, bounded by meflib's worst case, without touching .tdat at all.

True

available_checks staticmethod

available_checks() -> list[Check]

Every check in the registry, in the order they run.

describe_check staticmethod

describe_check(check_id: str) -> Check | None

One check by id, or None if unknown.

validate

validate(check_ids: Iterable[str] | None = None) -> Report

Run every check (or only check_ids) and return the full report.

Never modifies the session.

check

check(check_id: str) -> Report

Run one named check. Never modifies the session.

mef3io.Report dataclass

Report(findings: tuple[Finding, ...] = (), skipped: tuple[SkippedSegment, ...] = (), segments_checked: int = 0, segments_repaired: int = 0, checks_run: tuple[str, ...] = (), checks_repaired: tuple[str, ...] = (), path: str = '', channels: tuple[str, ...] = (), segments: tuple[int, ...] = (), measured_difference_bytes: bool = True)

The result of a validate or repair pass.

is_narrowed property

is_narrowed: bool

True when this run cannot speak for the whole session.

ok property

ok: bool

True when nothing worse than a warning is left outstanding.

Findings this pass repaired no longer count against it, so a repair that resolved everything reports ok. A skipped segment always does count — an unchecked segment is not a clean one.

errors property

errors: tuple[Finding, ...]

Every error-severity finding, repaired or not.

unresolved property

unresolved: tuple[Finding, ...]

Findings that are still outstanding — nothing repaired this pass.

repairable_check_ids property

repairable_check_ids: list[str]

Distinct check ids that reported something a repair could fix.

Pass these to :func:repair_session to opt in deliberately.

scope_caveat

scope_caveat() -> str

One sentence naming what this run did not cover, or "" if it covered everything.

by_check

by_check() -> dict[str, list[Finding]]

Findings grouped by check id, in registry order.

summary

summary(max_examples: int = 3) -> str

A full human-readable report: counts, then each check that fired.

mef3io.Finding dataclass

Finding(check_id: str, severity: str, channel: str, segment: int, path: str, field: str, stored: str, expected: str, message: str, repairable: bool, repaired: bool)

One problem, found by one check, in one segment.

mef3io.Check dataclass

Check(id: str, title: str, description: str, severity: str, repairable: bool)

One check in the registry.

mef3io.available_checks

available_checks() -> list[Check]

Every check, in the order they run. Ids are stable API.

mef3io.validate_session

validate_session(path: str, **kwargs) -> Report

Check a session and return the report. Reads only.

Accepts the same keyword arguments as :class:Validator.

mef3io.repair_session

repair_session(path: str, check_ids: Sequence[str], backup: bool = True, **kwargs) -> Report

Rewrite the declarations named in check_ids — and only those.

This is the only function in mef3io that modifies an existing session, and it is deliberately not a method of :class:Validator: inspecting a session and rewriting one should not be reachable through the same object.

Every check still runs, so the returned report describes the whole session; Finding.repaired marks what was actually written — a repair that declines to change anything is not counted.

Only declarations are rewritten: metadata section 2 and the universal headers of .tmet/.tidx/.tdat. Sample data and the block index are never touched. A segment whose CRCs do not verify is reported and left alone, and a .mefd.tar archive is refused outright (extract it first).

Parameters:

Name Type Description Default
path str

A .mefd directory. Archives cannot be rewritten in place.

required
check_ids sequence of str

Which repairs to apply. Required and non-empty — repairs are never implicit. :attr:Report.repairable_check_ids lists the candidates.

required
backup bool

Copy each file before rewriting it, into <session>.repair-backup/ (outside the session tree, so no reader mistakes a backup for data). An existing backup is never overwritten.

True
**kwargs

The same keyword arguments as :class:Validator (password, channels, segments, exact_difference_bytes).

{}

Raises:

Type Description
ValueError

If check_ids is empty, or names an unknown or non-repairable check.

mef3io.recover_session

recover_session(path: str, apply: bool = False, backup: bool = True, password: str = '') -> RecoveryReport

Make each segment's block index and data agree after an interrupted write.

This is not :func:repair_session, which only ever rewrites declarations and is safe to run on anything. Recovery may truncate the index, rebuild index entries from the data file, and drop a trailing fragment — so it is a dry run unless you ask for it, and it backs up what it changes first.

An interrupted append leaves one of two shapes, and they are handled differently because one has lost data and the other has not:

  • index ahead of data — entries reference bytes that never landed. Those samples do not exist, so the entries are dropped.
  • data ahead of index — blocks reached .tdat but the index was not extended. Those samples do exist, so they are recovered: a RED block header carries the sample count, byte count, start time and discontinuity flag, which is everything an index entry needs. Only blocks whose CRC verifies are indexed; a torn tail is not a block.

With the default durability="full" an append cannot leave either state. They are reachable with durability="fast", which is what makes that trade a reasonable one to offer.

Parameters:

Name Type Description Default
path str

.mefd session directory. Tar archives are refused.

required
apply bool

False reports what it would do and writes nothing.

False
backup bool

Save what changes to <session>.recover-backup/ first. Only the .tidx, the .tdat's 1024-byte header and any dropped fragment — never the whole data file, which may be tens of gigabytes.

True
password str

Only needed for encrypted sessions.

''

Returns:

Type Description
RecoveryReport
Notes

Declarations are not updated here. Run :func:repair_session afterwards — python -m mef3io recover does it for you.

mef3io.RecoveryReport dataclass

RecoveryReport(segments: tuple[RecoveredSegment, ...] = (), skipped: tuple[str, ...] = (), segments_examined: int = 0, applied: bool = False, backup_root: str = '')

What recover_session found, and did or would do.

mef3io.RecoveredSegment dataclass

RecoveredSegment(channel: str = '', segment: int = 0, path: str = '', blocks_before: int = 0, blocks_after: int = 0, blocks_recovered: int = 0, blocks_dropped: int = 0, tdat_bytes_dropped: int = 0, action: str = '')

One segment that an interrupted write left inconsistent.

mef3io.SessionDeclarationWarning

Bases: UserWarning

A session leaves size declarations unset that other readers rely on.

Raised on open, once per process per session (Python's default warning filter suppresses a repeat of the same message from the same line). It never affects mef3io's own reads — mef3io sizes every buffer from the block headers themselves — but meflib-based readers (CyberPSG and most established MEF tooling) allocate from metadata section 2 before decoding, and cannot tell an unset field from a real measurement.

Silence it like any warning::

warnings.filterwarnings("ignore", category=mef3io.SessionDeclarationWarning)

or per call with Reader(path, warn_declarations=False). To see the detail, or to fix it, use :class:mef3io.Validator.

Metadata objects

mef3io.Metadata dataclass

Metadata(subject: Subject = Subject(), acquisition: Acquisition = Acquisition())

Session-wide metadata: a :class:Subject and an :class:Acquisition block. Written to every channel; on read, reflects the first channel.

to_dict

to_dict() -> dict

Nested plain-dict view: {"subject": {...}, "acquisition": {...}}.

from_info classmethod

from_info(info: dict) -> 'Metadata'

Build a Metadata from a :meth:Reader.info dict. Subject fields are empty when the reader lacks level-2 access (section3_available).

mef3io.Subject dataclass

Subject(name_1: str = '', name_2: str = '', id: str = '', recording_location: str = '', gmt_offset: int = 0)

Subject / recording-context metadata (MEF section 3, level-2 encrypted).

mef3io.Acquisition dataclass

Acquisition(session_description: str = '', channel_description: str = '', reference_description: str = '', acquisition_channel_number: int = 1, low_frequency_filter: float = _UNSET_HZ, high_frequency_filter: float = _UNSET_HZ, notch_filter: float = _UNSET_HZ, line_frequency: float = _UNSET_HZ)

Descriptive / acquisition metadata (MEF section 2, level-1 encrypted).

Filter settings default to -1.0 meaning "not recorded".

Legacy mef_tools compatibility

Drop-in replacements for mef_tools.io — from mef3io import MefReader, MefWriter. Same call shapes and defaults as the legacy classes; see the legacy comparison for the measured differences.

mef3io.compat.MefReader

MefReader(session_path: str, password2: Optional[str] = None, warn_declarations: bool = True)

mef_tools.io.MefReader-compatible reader.

session_path may also be an uncompressed tar archive of a session (name.mefd.tar, see :func:mef3io.archive_session).

mef3io.compat.MefWriter

MefWriter(session_path, overwrite=False, password1=None, password2=None, verbose=False, metadata=None, durability='fast')

mef_tools.io.MefWriter-compatible writer.

Like the legacy writer, appends extend the channel's last segment in place (in-segment append); pass new_segment=True to start a fresh segment.

Metadata works two ways: mutate the legacy section3_dict / section2_ts_dict (e.g. w.section3_dict['subject_ID'] = 'Smith') as with mef_tools, or use the modern object via set_metadata / the metadata= argument (mef3io.Metadata).

mef_block_len property writable

mef_block_len

RED block length in samples (None = derive from fs, like legacy).

max_nans_written property writable

max_nans_written: int

Kept for legacy compatibility. mef3io always splits data on NaN runs (never stores NaN as values), i.e. it behaves like the legacy writer's recommended setting of 0; other values are accepted and ignored.

record_offset property writable

record_offset: int

Kept for legacy compatibility. mef3io writes records with a zero recording-time offset (annotation times round-trip unchanged either way); non-zero values are accepted and ignored.

set_metadata

set_metadata(metadata) -> None

Set session-wide subject/acquisition metadata (a :class:mef3io.Metadata or a flat dict). Applied on the next write.

get_mefblock_len

get_mefblock_len(fs: float) -> int

Block length that will be used for data at fs (legacy formula).