Skip to content

compare

Compare frame files or directories for consistency (data and metadata).

ComparisonResult dataclass

ComparisonResult(file_results: list[FileComparison])

Result of comparing two paths (files or directories).

Attributes:

Name Type Description
file_results list[FileComparison]

Per-file-pair comparison results

consistent property

consistent: bool

True if every file pair is consistent.

num_differences property

num_differences: int

Total number of differences across all file pairs.

Difference dataclass

Difference(category: str, field: str, left: str, right: str, channel: str | None = None, frame_index: int | None = None, detail: str = '')

A single difference found between two frame files.

Attributes:

Name Type Description
category str

One of 'error', 'structure', 'channels', 'metadata', 'data'

field str

What differed, e.g. 'num_frames', 'start', 'duration', 'dtype', 'sample_rate', 'n_samples', 'unit', 'type', 'values', 'mask', 'only_in_left', 'only_in_right', 'unmatched_file'

left str

Rendered left-side value ('-' if absent on that side)

right str

Rendered right-side value ('-' if absent on that side)

channel str or None

Channel name, or None for file/frame-level differences

frame_index int or None

Frame index, or None for file-level differences

detail str

Extra context, e.g. sample mismatch statistics

FileComparison dataclass

FileComparison(left: Path | None, right: Path | None, differences: list[Difference], channels_compared: int, frames_compared: int)

Result of comparing a single pair of frame files.

Attributes:

Name Type Description
left Path or None

Left-side file (None if this side had no matching file)

right Path or None

Right-side file (None if this side had no matching file)

differences list[Difference]

All differences found between the two files

channels_compared int

Number of channels compared

frames_compared int

Number of frame pairs compared

consistent property

consistent: bool

True if no differences were found.

compare_files

compare_files(left: str | PathLike[str], right: str | PathLike[str], *, channels: Sequence[str] | None = None, common_channels: bool = False, common_time_spans: bool = False, ignore_channel_type: bool = False, metadata_only: bool = False, rtol: float = 0.0, atol: float = 0.0) -> FileComparison

Compare two GWF files for consistency.

Checks channel sets, frame structure (count and time spans), per-channel metadata (type, dtype, sample rate, sample count, unit), and sample data.

Parameters:

Name Type Description Default
left str or path - like

First GWF file

required
right str or path - like

Second GWF file

required
channels Sequence[str]

Compare only these channels. Channels found on neither side raise ChannelNotFoundError.

None
common_channels bool

If True, compare only channels present on both sides; channels present on one side only are not reported as differences.

False
common_time_spans bool

If True, pair frames by (start, duration) time span and skip frames present on one side only, instead of pairing frames by index and reporting count/span mismatches.

False
ignore_channel_type bool

If True, do not report channel type (adc/proc/sim) differences, e.g. when the same data is stored as ADC on one side and proc on the other.

False
metadata_only bool

If True, skip sample data comparison (faster).

False
rtol float

Relative tolerance for floating-point data comparison. Default 0.0 (exact comparison, with NaN == NaN).

0.0
atol float

Absolute tolerance for floating-point data comparison (default: 0.0).

0.0

Returns:

Name Type Description
result FileComparison

Structured comparison result; result.consistent is True when no differences were found.

Examples:

>>> result = gwframe.compare_files('a.gwf', 'b.gwf')
>>> if not result.consistent:
...     for diff in result.differences:
...         print(diff)
Source code in gwframe/compare.py
def compare_files(
    left: str | PathLike[str],
    right: str | PathLike[str],
    *,
    channels: Sequence[str] | None = None,
    common_channels: bool = False,
    common_time_spans: bool = False,
    ignore_channel_type: bool = False,
    metadata_only: bool = False,
    rtol: float = 0.0,
    atol: float = 0.0,
) -> FileComparison:
    """
    Compare two GWF files for consistency.

    Checks channel sets, frame structure (count and time spans), per-channel
    metadata (type, dtype, sample rate, sample count, unit), and sample data.

    Parameters
    ----------
    left : str or path-like
        First GWF file
    right : str or path-like
        Second GWF file
    channels : Sequence[str], optional
        Compare only these channels. Channels found on neither side raise
        ChannelNotFoundError.
    common_channels : bool, optional
        If True, compare only channels present on both sides; channels
        present on one side only are not reported as differences.
    common_time_spans : bool, optional
        If True, pair frames by (start, duration) time span and skip frames
        present on one side only, instead of pairing frames by index and
        reporting count/span mismatches.
    ignore_channel_type : bool, optional
        If True, do not report channel type (adc/proc/sim) differences,
        e.g. when the same data is stored as ADC on one side and proc on
        the other.
    metadata_only : bool, optional
        If True, skip sample data comparison (faster).
    rtol : float, optional
        Relative tolerance for floating-point data comparison. Default 0.0
        (exact comparison, with NaN == NaN).
    atol : float, optional
        Absolute tolerance for floating-point data comparison (default: 0.0).

    Returns
    -------
    result : FileComparison
        Structured comparison result; ``result.consistent`` is True when no
        differences were found.

    Examples
    --------
    >>> result = gwframe.compare_files('a.gwf', 'b.gwf')
    >>> if not result.consistent:
    ...     for diff in result.differences:
    ...         print(diff)
    """
    left_path, right_path = Path(left), Path(right)

    try:
        left_reader = FrameReader(left_path)
    except (OSError, RuntimeError) as exc:
        return _open_error(left_path, right_path, "left", exc)
    try:
        right_reader = FrameReader(right_path)
    except (OSError, RuntimeError) as exc:
        left_reader.close()
        return _open_error(left_path, right_path, "right", exc)

    with left_reader, right_reader:
        compared, differences = _select_channels(
            set(left_reader.channels),
            set(right_reader.channels),
            channels,
            common_channels=common_channels,
            source=f"{left_path} / {right_path}",
        )

        frame_pairs, structure_diffs = _pair_frames(
            left_reader,
            right_reader,
            common_time_spans=common_time_spans,
        )
        differences.extend(structure_diffs)

        compared_list = sorted(compared)
        for left_index, right_index, compare_data in frame_pairs:
            differences.extend(
                _compare_frame_pair(
                    left_reader,
                    left_index,
                    right_reader,
                    right_index,
                    compared_list,
                    ignore_channel_type=ignore_channel_type,
                    compare_data=compare_data and not metadata_only,
                    rtol=rtol,
                    atol=atol,
                )
            )

    return FileComparison(
        left=left_path,
        right=right_path,
        differences=differences,
        channels_compared=len(compared),
        frames_compared=len(frame_pairs),
    )

compare_paths

compare_paths(left: str | PathLike[str], right: str | PathLike[str], *, recursive: bool = False, channels: Sequence[str] | None = None, common_channels: bool = False, common_time_spans: bool = False, ignore_channel_type: bool = False, metadata_only: bool = False, rtol: float = 0.0, atol: float = 0.0) -> ComparisonResult

Compare two paths (GWF files or directories of GWF files).

When both paths are files they are compared directly. Otherwise each path is expanded to its GWF files and frames are paired across sides by their (start, duration) GPS span, regardless of which file holds them, so two productions of the same data compare equal even when their file boundaries differ. Results are grouped per pair of files that shared frames. Frames with no counterpart are reported per frame, or as one unmatched_file difference when a whole file has none, unless common_time_spans is set, in which case they are silently skipped. common_time_spans likewise skips files that disappear from disk while the comparison is running (e.g. removed from a live retention window); open failures where the file still exists are always reported.

Parameters:

Name Type Description Default
left str or path - like

First GWF file or directory

required
right str or path - like

Second GWF file or directory

required
recursive bool

Search directories recursively for GWF files (default: False)

False
channels Sequence[str] | None
None
common_channels Sequence[str] | None
None
common_time_spans Sequence[str] | None
None
ignore_channel_type Sequence[str] | None
None
metadata_only bool

See :func:compare_files.

False
rtol bool

See :func:compare_files.

False
atol bool

See :func:compare_files.

False

Returns:

Name Type Description
result ComparisonResult

Results per pair of files that shared frames; result.consistent is True when every pair is consistent.

Examples:

>>> result = gwframe.compare_paths('dir1/', 'dir2/', common_channels=True)
>>> print(f"{result.num_differences} differences")
Source code in gwframe/compare.py
def compare_paths(
    left: str | PathLike[str],
    right: str | PathLike[str],
    *,
    recursive: bool = False,
    channels: Sequence[str] | None = None,
    common_channels: bool = False,
    common_time_spans: bool = False,
    ignore_channel_type: bool = False,
    metadata_only: bool = False,
    rtol: float = 0.0,
    atol: float = 0.0,
) -> ComparisonResult:
    """
    Compare two paths (GWF files or directories of GWF files).

    When both paths are files they are compared directly. Otherwise each
    path is expanded to its GWF files and *frames* are paired across sides
    by their (start, duration) GPS span, regardless of which file holds
    them, so two productions of the same data compare equal even when their
    file boundaries differ. Results are grouped per pair of files that
    shared frames. Frames with no counterpart are reported per frame, or as
    one ``unmatched_file`` difference when a whole file has none, unless
    ``common_time_spans`` is set, in which case they are silently skipped.
    ``common_time_spans`` likewise skips files that disappear from disk
    while the comparison is running (e.g. removed from a live retention
    window); open failures where the file still exists are always
    reported.

    Parameters
    ----------
    left : str or path-like
        First GWF file or directory
    right : str or path-like
        Second GWF file or directory
    recursive : bool, optional
        Search directories recursively for GWF files (default: False)
    channels, common_channels, common_time_spans, ignore_channel_type,
    metadata_only, rtol, atol
        See :func:`compare_files`.

    Returns
    -------
    result : ComparisonResult
        Results per pair of files that shared frames; ``result.consistent``
        is True when every pair is consistent.

    Examples
    --------
    >>> result = gwframe.compare_paths('dir1/', 'dir2/', common_channels=True)
    >>> print(f"{result.num_differences} differences")
    """
    left_path, right_path = Path(left), Path(right)

    def _compare_pair(pair_left: Path, pair_right: Path) -> FileComparison:
        return compare_files(
            pair_left,
            pair_right,
            channels=channels,
            common_channels=common_channels,
            common_time_spans=common_time_spans,
            ignore_channel_type=ignore_channel_type,
            metadata_only=metadata_only,
            rtol=rtol,
            atol=atol,
        )

    if left_path.is_file() and right_path.is_file():
        return ComparisonResult([_compare_pair(left_path, right_path)])

    left_files = _expand_gwf_files(left_path, recursive=recursive)
    right_files = _expand_gwf_files(right_path, recursive=recursive)
    return ComparisonResult(
        _compare_directories(
            left_files,
            right_files,
            channels=channels,
            common_channels=common_channels,
            common_time_spans=common_time_spans,
            ignore_channel_type=ignore_channel_type,
            metadata_only=metadata_only,
            rtol=rtol,
            atol=atol,
        )
    )