Skip to content

ex_search

Ex-search report: one self-contained HTML record per exhaustive montage search.

Built on :mod:tit.reporting.html.components (ARCHITECTURE.md §14). Everything is read from the run folder: run_config.json (buckets, current sweep, ROI name and radius, leadfield), final_output.csv (one row per montage and current split), the ROI confirmation (roi.tetravox.json) and the leadfield's SimNIBS log (electrode geometry, conductivity). The goal definition and dose record come from tit.reporting.qc_rules.RULES["opt"]; the page has no checks.

The default view is the winner (ranked by composite = ROI mean × focality) with its electrodes and dose, one chart of every montage's ROI mean against focality, the top 25 as a sortable table, and the ROI and ranking in words; methods and the run record are collapsed.

tit.opt.ex and tit.opt.mex write one when a search finishes (an mEx page lists four channels at its one current). Rebuild one (nothing is re-searched)::

simnibs_python -m tit.reporting.generators.ex_search <project> <subject> <run name> [--out DIR]

read_results

read_results(csv_path: str | Path) -> list[dict]

final_output.csv rows with numbers as floats, ranked by composite (best first).

Source code in tit/reporting/generators/ex_search.py
def read_results(csv_path: str | Path) -> list[dict]:
    """``final_output.csv`` rows with numbers as floats, ranked by composite (best first)."""
    return _read_rows(csv_path)[0]

top_distinct

top_distinct(rows: list[dict], top: int = TOP_N) -> list[dict]

The best top rows that are different stimulations.

A row is the set of its channels, each an unordered electrode pair at its current: the TI envelope ignores channel polarity and order, so runs written before pool mode evaluated each montage once list one montage up to eight times. Rows are ranked best first.

Source code in tit/reporting/generators/ex_search.py
def top_distinct(rows: list[dict], top: int = TOP_N) -> list[dict]:
    """The best *top* rows that are different stimulations.

    A row is the set of its channels, each an unordered electrode pair at its current: the TI
    envelope ignores channel polarity and order, so runs written before pool mode evaluated each
    montage once list one montage up to eight times. Rows are ranked best first.
    """
    seen, kept = set(), []
    for r in rows:
        key = frozenset(
            (frozenset(pair), None if current is None else round(current, 9))
            for pair, current in zip_longest(r["pairs"], channel_currents(r))
        )
        if key not in seen:
            seen.add(key)
            kept.append(r)
            if len(kept) == top:
                break
    return kept

channel_currents

channel_currents(row: dict) -> list[float]

The current of each channel of row (mA), in pair order: two for ex, four for mEx.

Source code in tit/reporting/generators/ex_search.py
def channel_currents(row: dict) -> list[float]:
    """The current of each channel of *row* (mA), in pair order: two for ex, four for mEx."""
    return row.get("currents") or [row["I1"], row["I2"]]

leadfield_facts

leadfield_facts(log_text: str) -> dict

Electrode geometry and conductivity the leadfield was computed with (its SimNIBS log).

Source code in tit/reporting/generators/ex_search.py
def leadfield_facts(log_text: str) -> dict:
    """Electrode geometry and conductivity the leadfield was computed with (its SimNIBS log)."""

    def first(pattern: str) -> str | None:
        m = re.search(pattern, log_text)
        return m.group(1).strip() if m else None

    return {
        "shape": first(r"\nshape: (\S+)"),
        "dimensions": first(r"\ndimensions: \[([^\]]*)\]"),
        "thickness": first(r"\nthickness:\s*\[([^\]]*)\]"),
        "tensor": first(r"Using anisotropic .*? based on the file: (\S+)"),
        "conductivity": (
            "anisotropic"
            if "Using anisotropic" in log_text
            else "isotropic" if "Using isotropic conductivities" in log_text else None
        ),
        "simnibs": first(r"simnibs (\d[\w.]*)"),
    }

leadfield_log

leadfield_log(leadfield_dir: str | Path, cfg: dict) -> str | None

The text of the SimNIBS log that built this run's leadfield, or None.

A subject's leadfields/ keeps one log per attempt, for every net. A log whose EEG Cap: line names another net is never used; among the rest, one that names the run's HDF5 file (written when the leadfield finished) beats one that only names its net, which beats one that names no net (an attempt that died first); the newest breaks ties.

Source code in tit/reporting/generators/ex_search.py
def leadfield_log(leadfield_dir: str | Path, cfg: dict) -> str | None:
    """The text of the SimNIBS log that built this run's leadfield, or ``None``.

    A subject's ``leadfields/`` keeps one log per attempt, for every net. A log whose
    ``EEG Cap:`` line names another net is never used; among the rest, one that names the run's
    HDF5 file (written when the leadfield finished) beats one that only names its net, which
    beats one that names no net (an attempt that died first); the newest breaks ties.
    """
    hdf = Path(cfg.get("leadfield_hdf") or "").name
    net = net_of(cfg)
    best, best_key = None, None
    for log in sorted(Path(leadfield_dir).glob("simnibs_simulation_*.log")):
        text = log.read_text(errors="replace")
        cap = re.search(r"EEG Cap:\s*(\S+)", text)
        if cap and Path(cap.group(1)).stem != net:
            continue
        key = (bool(hdf) and hdf in text, cap is not None, log.name)
        if best_key is None or key > best_key:
            best, best_key = text, key
    return best

collect

collect(run_dir: str | Path, leadfield_dir: str | Path | None = None) -> dict

Everything the report shows, from one ex-search run folder.

Source code in tit/reporting/generators/ex_search.py
def collect(run_dir: str | Path, leadfield_dir: str | Path | None = None) -> dict:
    """Everything the report shows, from one ex-search run folder."""
    run_dir = Path(run_dir)
    cfg = json.loads((run_dir / "run_config.json").read_text())
    lf = {}
    log = leadfield_log(leadfield_dir, cfg) if leadfield_dir else None
    if log:
        lf = leadfield_facts(log)
    rows, skipped = _read_rows(run_dir / "final_output.csv")
    if cfg.get("current_mA") is not None:
        # mEx drives every pair at the one configured current; its older CSVs carry only the
        # first two channels' currents, rounded to 0.1 mA.
        for row in rows:
            row["currents"] = [float(cfg["current_mA"])] * len(row["pairs"])
    return {
        "run": run_dir.name,
        "config": cfg,
        "rows": rows,
        "skipped": skipped,
        "renamed_carrier_columns": _renamed_columns(run_dir / "final_output.csv"),
        "roi": common.roi_summary(run_dir),
        "leadfield": lf,
    }

target_description

target_description(cfg: dict) -> str

The ROI as run_config.json defines it, for a run without a ROI plate.

Spheres around the centres in roi_names (or roi_name), in subject or MNI coordinates, joined with any roi_atlas labels or masks. A run written before these fields were recorded is named, not described: its roi_name may be only a label.

Source code in tit/reporting/generators/ex_search.py
def target_description(cfg: dict) -> str:
    """The ROI as ``run_config.json`` defines it, for a run without a ROI plate.

    Spheres around the centres in ``roi_names`` (or ``roi_name``), in subject or MNI
    coordinates, joined with any ``roi_atlas`` labels or masks. A run written before these
    fields were recorded is named, not described: its ``roi_name`` may be only a label.
    """
    name = cfg.get("roi_name") or "target ROI"
    if not any(k in cfg for k in ("roi_names", "roi_atlas", "roi_coordinate_space")):
        return f"{name} (shape not recorded in this run)"
    radius = cfg.get("roi_radius")
    names = cfg.get("roi_names")
    names = [name] if names is None else names
    mni = "MNI " if cfg.get("roi_coordinate_space") == "mni" else ""
    parts = []
    if len(names) == 1:
        parts.append(f"sphere of {radius:g} mm around the {mni}centre in {names[0]}")
    elif names:
        parts.append(
            f"union of {radius:g} mm spheres around the {mni}centres in {', '.join(names)}"
        )
    for atlas in cfg.get("roi_atlas") or []:
        where = Path(str(atlas.get("atlas_path", ""))).name
        where += " (MNI)" if atlas.get("atlas_space") == "mni" else ""
        label = atlas.get("label")
        parts.append(f"mask {where}" if label is None else f"label {label} of {where}")
    return " + ".join(parts) or name

density_chart

density_chart(rows: list[dict], top: int = TOP_N) -> str

Every montage as ROI mean against focality: a log-shaded density grid, the top top as rings, the winner filled, and three curves of equal composite (ROI mean × focality).

Source code in tit/reporting/generators/ex_search.py
def density_chart(rows: list[dict], top: int = TOP_N) -> str:
    """Every montage as ROI mean against focality: a log-shaded density grid, the top *top* as
    rings, the winner filled, and three curves of equal composite (ROI mean × focality).
    """
    width, height, ml, mr, mt, mb = 720, 380, 50, 16, 16, 40
    pw, ph = width - ml - mr, height - mt - mb
    xs = [r["TImean_ROI"] for r in rows]
    ys = [r["Focality"] for r in rows]
    # One value on an axis (a single row, or every montage tied at 0) still needs a span.
    x0, x1 = 0.0, max(xs) * 1.05 or 1.0
    y0, y1 = min(ys) * 0.97, max(ys) * 1.03
    if y1 <= y0:
        y1 = y0 + 1.0
    nx, ny = 72, 40

    def X(v: float) -> float:
        return ml + (v - x0) / (x1 - x0) * pw

    def Y(v: float) -> float:
        return mt + ph - (v - y0) / (y1 - y0) * ph

    cells: dict[tuple[int, int], int] = {}
    for x, y in zip(xs, ys):
        key = (
            min(int((x - x0) / (x1 - x0) * nx), nx - 1),
            min(int((y - y0) / (y1 - y0) * ny), ny - 1),
        )
        cells[key] = cells.get(key, 0) + 1
    peak = math.log10(max(cells.values()) + 1)
    cw, ch = pw / nx, ph / ny
    p = [
        f'<svg class="chart" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" '
        f'aria-label="ROI mean against focality for {len(rows):,} montages; the top {top} and the winner marked">'
    ]
    for t in c.nice_ticks(y0, y1, 5):
        p.append(
            f'<line class="grid" x1="{ml}" x2="{ml + pw}" y1="{Y(t):.1f}" y2="{Y(t):.1f}"/>'
            f'<text x="{ml - 6}" y="{Y(t) + 4:.1f}" text-anchor="end">{t:g}</text>'
        )
    for t in c.nice_ticks(x0, x1, 6):
        p.append(
            f'<text x="{X(t):.1f}" y="{mt + ph + 16}" text-anchor="middle">{t:g}</text>'
        )
    for (i, j), n in sorted(cells.items()):
        a = 0.12 + 0.88 * math.log10(n + 1) / peak
        lo_x, lo_y = x0 + i * (x1 - x0) / nx, y0 + j * (y1 - y0) / ny
        tip = (
            f"{n:,} montage{'s' if n > 1 else ''}\nROI mean {lo_x:.3f}–{lo_x + (x1 - x0) / nx:.3f} V/m"
            f"\nfocality {lo_y:.2f}–{lo_y + (y1 - y0) / ny:.2f}"
        )
        p.append(
            f'<rect x="{ml + i * cw:.1f}" y="{mt + ph - (j + 1) * ch:.1f}" width="{cw:.1f}" height="{ch:.1f}" '
            f'fill="var(--s1)" fill-opacity="{a:.2f}" data-tip="{esc(tip)}"/>'
        )
    best = rows[0]["Composite_Index"]
    for k in (0.5, 0.75) if best > 0 else ():
        level = best * k
        pts = []
        for s in range(61):
            x = x0 + (x1 - x0) * s / 60
            if x > 0 and y0 <= level / x <= y1:
                pts.append(f"{X(x):.1f},{Y(level / x):.1f}")
        if len(pts) > 1:
            p.append(
                f'<polyline points="{" ".join(pts)}" fill="none" stroke="var(--ink-3)" stroke-width="1" stroke-dasharray="3 4"/>'
                f'<text x="{pts[-1].split(",")[0]}" y="{float(pts[-1].split(",")[1]) - 5:.1f}" text-anchor="end">composite {level:.3f}</text>'
            )
    for r in top_distinct(rows, top)[1:]:
        p.append(
            f'<circle cx="{X(r["TImean_ROI"]):.1f}" cy="{Y(r["Focality"]):.1f}" r="4.5" fill="none" stroke="var(--s2)" stroke-width="1.6" '
            f'data-tip="{esc(_montage_name(r["pairs"]))}\n{"/".join(f"{v:g}" for v in channel_currents(r))} mA: ROI mean {r["TImean_ROI"]:.3f} V/m, focality {r["Focality"]:.2f}"/>'
        )
    w = rows[0]
    wx, wy = X(w["TImean_ROI"]), Y(w["Focality"])
    p.append(
        f'<circle cx="{wx:.1f}" cy="{wy:.1f}" r="6" fill="var(--s2)" stroke="var(--paper)" stroke-width="2" '
        f'data-tip="Winner: {esc(_montage_name(w["pairs"]))}"/>'
        f'<line x1="{wx - 5:.1f}" y1="{wy - 5:.1f}" x2="{wx - 30:.1f}" y2="{wy - 30:.1f}" stroke="var(--ink-2)"/>'
        f'<text x="{wx - 34:.1f}" y="{wy - 32:.1f}" text-anchor="end" class="t-ink">best: {esc(_montage_name(w["pairs"]))}</text>'
    )
    p.append(
        f'<line class="axis" x1="{ml}" x2="{ml + pw}" y1="{mt + ph}" y2="{mt + ph}"/>'
        f'<text x="{ml + pw / 2:.1f}" y="{height - 4}" text-anchor="middle">Mean TI envelope in the ROI (V/m)</text>'
        f'<text transform="translate(12 {mt + ph / 2:.1f}) rotate(-90)" text-anchor="middle">Focality (ROI mean ÷ grey-matter mean)</text></svg>'
    )
    return "".join(p)

create_ex_search_report

create_ex_search_report(project_dir: str | Path, subject_id: str, run_dir: str | Path, out_dir: str | Path | None = None) -> Path

Write the report for one ex- or mEx-search run folder; into the project's reports unless out_dir.

Source code in tit/reporting/generators/ex_search.py
def create_ex_search_report(
    project_dir: str | Path,
    subject_id: str,
    run_dir: str | Path,
    out_dir: str | Path | None = None,
) -> Path:
    """Write the report for one ex- or mEx-search run folder; into the project's reports unless
    *out_dir*."""
    from tit.paths import get_path_manager

    t0 = time.time()
    pm = get_path_manager(str(project_dir))
    rec = collect(run_dir, pm.leadfields(subject_id))
    if not rec["rows"]:
        raise ValueError(f"{run_dir}/final_output.csv has no montages")
    if all(r["TImax_ROI"] == 0 and r["TImean_ROI"] == 0 for r in rec["rows"]):
        raise ValueError(
            f"Every montage in {run_dir}/final_output.csv scored 0 in the ROI "
            f"{rec['config'].get('roi_name')!r}: the ROI selected no leadfield mesh elements, "
            "so the ranking is arbitrary and there is no winner to report."
        )
    return common.write_report(
        build_html(rec, subject_id),
        project_dir,
        subject_id,
        REPORT_PREFIX,
        SIZE_BUDGET,
        out_dir,
        t0,
        label="Ex-search report",
    )