Skip to content

buckets

tit.opt.ex.buckets

EEG-cap helpers for exhaustive search: electrode positions, the bundled 2-D template coordinates of a known net, and each electrode's left/right mirror.

:func:build_electrode_mirror_map drives the symmetric bucket search (:mod:tit.opt.ex.symmetry); :func:_read_eeg_positions also feeds the ex-search cap figures (:mod:tit.plotting.ti_metrics).

canonical_template_coord_path

canonical_template_coord_path(eeg_net_name: str | Path | None) -> Path | None

Return a canonical 2D EEG-template coordinate file for known net names.

Source code in tit/opt/ex/buckets.py
def canonical_template_coord_path(eeg_net_name: str | Path | None) -> Path | None:
    """Return a canonical 2D EEG-template coordinate file for known net names."""
    if not eeg_net_name:
        return None
    name = Path(str(eeg_net_name)).name
    coord_file = _CANONICAL_TEMPLATE_COORD_FILES.get(name)
    if coord_file is None:
        coord_file = _CANONICAL_TEMPLATE_COORD_FILES.get(Path(name).stem)
    if coord_file is None:
        return None
    path = _RESOURCE_DIR / coord_file
    return path if path.is_file() else None

build_electrode_mirror_map

build_electrode_mirror_map(eeg_csv_path: str | Path) -> dict[str, str]

Map each electrode to its left/right mirror in an EEG CSV.

The mirror plane is estimated from the cap (:func:_midsagittal_plane), so off-centre and slightly rotated subject-space caps and 2-D templates in pixel units mirror correctly. Electrodes are then matched greedily, best first: a left/right pair costs the distance between one electrode's image and the other; a midline electrode (closer to the plane than half the gap to its nearest neighbour) may pair with itself at the cost of the distance to its own image. The map is therefore an involution; an electrode left without a partner on the other side is not in it. Downstream channel-pair validation still prevents self-pairs from being evaluated.

Source code in tit/opt/ex/buckets.py
def build_electrode_mirror_map(eeg_csv_path: str | Path) -> dict[str, str]:
    """Map each electrode to its left/right mirror in an EEG CSV.

    The mirror plane is estimated from the cap (:func:`_midsagittal_plane`),
    so off-centre and slightly rotated subject-space caps and 2-D templates in
    pixel units mirror correctly. Electrodes are then matched greedily, best
    first: a left/right pair costs the distance between one electrode's image
    and the other; a midline electrode (closer to the plane than half the gap to
    its nearest neighbour) may pair with itself at the cost of the distance to
    its own image. The map is therefore an involution; an electrode left
    without a partner on the other side is not in it. Downstream channel-pair
    validation still prevents self-pairs from being evaluated.
    """
    positions = _read_eeg_positions(eeg_csv_path)
    if not positions:
        raise ValueError(f"No electrode positions found in {eeg_csv_path}")

    labels = list(positions)
    points = np.array([positions[label] for label in labels], dtype=float)
    normal, offset = _midsagittal_plane(points)
    side = points @ normal - offset
    cost = _squared_distances(points - 2 * side[:, None] * normal, points)
    gaps = _squared_distances(points, points)
    np.fill_diagonal(gaps, np.inf)
    nearest_gap = np.sqrt(gaps.min(axis=1)) if len(labels) > 1 else [np.inf]

    candidates = [
        (cost[i, i], i, i)
        for i in range(len(labels))
        if 2 * abs(side[i]) < nearest_gap[i]
    ]
    candidates += [
        (cost[i, j], i, j)
        for i in range(len(labels))
        for j in range(i + 1, len(labels))
        if side[i] * side[j] < 0
    ]
    partner: dict[int, int] = {}
    for _, i, j in sorted(candidates):
        if i not in partner and j not in partner:
            partner[i] = j
            partner[j] = i
    return {labels[i]: labels[partner[i]] for i in range(len(labels)) if i in partner}