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 writes one when a search finishes. 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)."""
    from tit.opt.ex.results import parse_montage_string

    rows = []
    with open(csv_path, newline="") as f:
        for r in csv.DictReader(f):
            try:
                row = {k: float(r[k]) for k in _FIELDS}
                row["I1"], row["I2"] = float(r["Current_Ch1_mA"]), float(
                    r["Current_Ch2_mA"]
                )
            except (KeyError, TypeError, ValueError):
                continue
            e = parse_montage_string(r["Montage"])
            row["pairs"] = [e[i : i + 2] for i in range(0, len(e), 2)]
            rows.append(row)
    rows.sort(key=lambda r: r["Composite_Index"], reverse=True)
    return rows

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+)"),
        "simnibs": first(r"simnibs (\d[\w.]*)"),
    }

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 = {}
    if leadfield_dir:
        logs = sorted(Path(leadfield_dir).glob("simnibs_simulation_*.log"))
        if logs:
            lf = leadfield_facts(logs[-1].read_text(errors="replace"))
    return {
        "run": run_dir.name,
        "config": cfg,
        "rows": read_results(run_dir / "final_output.csv"),
        "roi": common.roi_summary(run_dir),
        "leadfield": lf,
    }

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]
    x0, x1 = 0.0, max(xs) * 1.05
    y0, y1 = min(ys) * 0.97, max(ys) * 1.03
    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):
        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 rows[1:top]:
        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{r["I1"]:g}/{r["I2"]:g} 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-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-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")
    return common.write_report(
        build_html(rec, subject_id),
        project_dir,
        subject_id,
        REPORT_PREFIX,
        SIZE_BUDGET,
        out_dir,
        t0,
        label="Ex-search report",
    )