Add new regression and ensemble experiment configurations for V2-M and OrthoBridge

- Introduced multiple regression experiment configurations targeting vf_md, including:
  - cd_solo_reg_set.json: CD tower only regression setup.
  - img_solo_reg_set.json: Image tower only regression setup.
  - reg_head_epoch_sweep.json: Baseline regression sweeps at different epochs (50, 75, 100).
  - reg_head_set.json: Various regression setups including baseline and OrthoBridge configurations.
  - single_eye_reg.json: Single-eye regression setup for worst-eye aggregation analysis.

- Added ensemble configurations for OrthoBridge with different inner bridges:
  - ortho_alts_ensemble.json: Ensemble tests with ConcatBridge, PairwiseAdditiveBridge, and GatedAdditiveBridge.
  - ortho_alts_tritower.json: Tritower tests with the same inner bridges.

- Created V2-M specific configurations:
  - baseline_reg_nt50.json: Regression baseline with V2-M backbone.
  - geom_vec_gt.json and geom_vec_unet.json: Geometry vector injection experiments with V2-M.
  - single_l1_bridges.json: Single-eye ensemble experiments with various bridge types.
  - tritower_geom_gt.json: Tritower setup with GT contour-rasterized masks.

- Promoted existing experiments to higher repetitions for robustness.
This commit is contained in:
rpotter6298
2026-06-11 15:08:20 +02:00
parent 32a801a572
commit 280060db82
343 changed files with 8558 additions and 57747 deletions
+189
View File
@@ -0,0 +1,189 @@
"""F1 — System architecture diagram.
Bilateral multimodal fusion architecture. Modelled on v3's architecture_fused_head
but adapted for v4 + manuscript terminology:
* "OD HyperTower" / "OS HyperTower" -> "OD Fusion" / "OS Fusion"
* Bridge boxes show their math explicitly (image projection, clinical
projection, fusion operation), no longer abbreviated "Bridge"
* Title drops the HyperTower brand
Re-run anytime:
python -m v4.figures.F1_architecture
"""
from __future__ import annotations
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch
OUT = Path(__file__).parent / "output" / "F1_architecture.png"
# ── Palette (matches v3 plot_architecture.py for visual consistency) ────────
C_IMG = "#4e8d3a" # green — image / CNN
C_MD = "#4c72b0" # blue — clinical network
C_BRIDGE = "#c44e52" # red — fusion bridge
C_HEAD = "#d4a017" # gold — patient-level head
C_OUT = "#8c6bb1" # purple — output classes
C_INPUT = "#a0a0a0" # grey — raw inputs
C_BG = "#e8e8e8"
C_ARROW = "#444444"
FONT = "DejaVu Sans"
# ── Primitives ───────────────────────────────────────────────────────────────
def _box(ax, cx, cy, w, h, color, text="", fontsize=9, text_color="white",
bold=False, alpha=0.92, radius=0.12, lw=1.5):
patch = FancyBboxPatch(
(cx - w / 2, cy - h / 2), w, h,
boxstyle=f"round,pad=0,rounding_size={radius}",
facecolor=color, edgecolor="white", linewidth=lw, alpha=alpha, zorder=3,
transform=ax.transData,
)
ax.add_patch(patch)
if text:
ax.text(cx, cy, text, ha="center", va="center",
fontsize=fontsize, color=text_color,
fontweight="bold" if bold else "normal",
fontfamily=FONT, zorder=4)
return patch
def _arrow(ax, x0, y0, x1, y1, lw=1.4, color=C_ARROW, style="-|>"):
ax.annotate("", xy=(x1, y1), xytext=(x0, y0),
arrowprops=dict(arrowstyle=style, color=color, lw=lw),
zorder=2)
def _text(ax, x, y, s, fontsize=9, color="#333", ha="center", va="center", bold=False):
ax.text(x, y, s, ha=ha, va=va, fontsize=fontsize, color=color,
fontfamily=FONT, fontweight="bold" if bold else "normal", zorder=5)
def _bracket(ax, x, y0, y1, text="", pad=0.20, fontsize=9, badge_color="#555"):
mid = (y0 + y1) / 2
ax.plot([x, x + pad, x + pad, x], [y1, y1, y0, y0],
color=badge_color, lw=1.4, solid_capstyle="round", zorder=2)
if text:
ax.text(x + pad * 1.4, mid, text, ha="left", va="center",
fontsize=fontsize, color="white", fontfamily=FONT, fontweight="bold",
zorder=6,
bbox=dict(facecolor=badge_color, edgecolor="none", pad=3.5,
boxstyle="round,pad=0.3"))
def _draw_output(ax, x, y, classes=("Glaucoma", "Normal")):
bw, bh, gap = 1.10, 0.38, 0.08
n = len(classes)
total = n * bh + (n - 1) * gap
y_top = y + total / 2 - bh / 2
for i, cls in enumerate(classes):
cy = y_top - i * (bh + gap)
_box(ax, x + bw / 2, cy, bw, bh, C_OUT, cls, fontsize=8.5, radius=0.08)
_arrow(ax, x, y, x, cy, lw=1.1, style="-|>")
_text(ax, x + bw / 2, y - total / 2 - 0.20, "Softmax",
fontsize=7.5, color=C_OUT)
def _draw_eye_fusion(ax, x_left, y_img, y_md, eye_label):
"""One eye's row: Image Network box + Clinical Network box -> Fusion bridge box.
Returns (x_right_of_bridge, y_bridge_center).
"""
bw_img, bh_img = 1.45, 0.72
bw_md, bh_md = 1.48, 0.66
bw_br, bh_br = 1.75, 1.30
# Image network box
_box(ax, x_left + bw_img / 2, y_img, bw_img, bh_img, C_IMG,
f"{eye_label}\nImage Network", fontsize=8.5, radius=0.08)
# Clinical network box
_box(ax, x_left + bw_md / 2, y_md, bw_md, bh_md, C_MD,
f"{eye_label}\nClinical Network", fontsize=8.5, radius=0.08)
# Fusion bridge — with math detail (replaces compact "Bridge" label)
br_x = x_left + max(bw_img, bw_md) + 1.40
cy_br = (y_img + y_md) / 2
_arrow(ax, x_left + bw_img, y_img, br_x - bw_br / 2, cy_br, lw=1.2, style="-|>")
_arrow(ax, x_left + bw_md, y_md, br_x - bw_br / 2, cy_br, lw=1.2, style="-|>")
_box(ax, br_x, cy_br, bw_br, bh_br, C_BRIDGE,
"Fusion Bridge\nFC(img → 256)\nFC(md → 256)\nHadamard product",
fontsize=8, radius=0.10)
return br_x + bw_br / 2, cy_br
# ── Main figure ──────────────────────────────────────────────────────────────
def main() -> None:
W, H = 13.0, 7.8
fig, ax = plt.subplots(figsize=(W, H))
ax.set_xlim(0, W); ax.set_ylim(0, H); ax.axis("off")
ax.set_facecolor(C_BG); fig.patch.set_facecolor(C_BG)
ax.set_title("Bilateral Multimodal Fusion Architecture",
fontsize=13, fontweight="bold", fontfamily=FONT, pad=10, color="#222")
x_left = 3.2
inp_cx = 1.85
inp_w = 0.95
inp_h = 0.55
# OD (top)
od_y_img, od_y_md = 6.10, 4.80
br_od_x, cy_od = _draw_eye_fusion(ax, x_left, od_y_img, od_y_md, "OD")
_text(ax, 0.50, (od_y_img + od_y_md) / 2, "OD\n(Right Eye)",
fontsize=9.5, color="#444", bold=True)
_box(ax, inp_cx, od_y_img, inp_w, inp_h, C_INPUT, "Fundus\nImage",
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
_box(ax, inp_cx, od_y_md, inp_w, inp_h, C_INPUT, "Clinical\nData",
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
_arrow(ax, inp_cx + inp_w / 2, od_y_img, x_left, od_y_img, lw=1.2, style="-|>")
_arrow(ax, inp_cx + inp_w / 2, od_y_md, x_left, od_y_md, lw=1.2, style="-|>")
# OS (bottom)
os_y_img, os_y_md = 2.95, 1.65
br_os_x, cy_os = _draw_eye_fusion(ax, x_left, os_y_img, os_y_md, "OS")
_text(ax, 0.50, (os_y_img + os_y_md) / 2, "OS\n(Left Eye)",
fontsize=9.5, color="#444", bold=True)
_box(ax, inp_cx, os_y_img, inp_w, inp_h, C_INPUT, "Fundus\nImage",
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
_box(ax, inp_cx, os_y_md, inp_w, inp_h, C_INPUT, "Clinical\nData",
fontsize=8.5, radius=0.08, alpha=0.78, text_color="#333")
_arrow(ax, inp_cx + inp_w / 2, os_y_img, x_left, os_y_img, lw=1.2, style="-|>")
_arrow(ax, inp_cx + inp_w / 2, os_y_md, x_left, os_y_md, lw=1.2, style="-|>")
# Side brackets — re-labelled "OD Fusion" / "OS Fusion"
_bracket(ax, x=br_od_x + 0.05,
y0=od_y_md - 0.50, y1=od_y_img + 0.50,
text="OD Fusion", pad=0.22, fontsize=9, badge_color="#555")
_bracket(ax, x=br_os_x + 0.05,
y0=os_y_md - 0.50, y1=os_y_img + 0.50,
text="OS Fusion", pad=0.22, fontsize=9, badge_color="#555")
# Patient-level head (Fused Head)
head_y = (cy_od + cy_os) / 2
head_x = max(br_od_x, br_os_x) + 2.55
head_w, head_h = 1.95, 1.30
_arrow(ax, br_od_x + 0.05, cy_od, head_x - head_w / 2, head_y, lw=1.4, style="-|>")
_arrow(ax, br_os_x + 0.05, cy_os, head_x - head_w / 2, head_y, lw=1.4, style="-|>")
_box(ax, head_x, head_y, head_w, head_h, C_HEAD,
"Patient Head\ncat(z_OD, z_OS)\n→ FC(256) → FC(2)",
fontsize=8.5, radius=0.10)
# Output nodes
out_x = head_x + head_w / 2 + 0.55
_arrow(ax, head_x + head_w / 2, head_y, out_x, head_y, lw=1.5, style="-|>")
_draw_output(ax, out_x, head_y)
OUT.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(OUT, dpi=180, bbox_inches="tight")
plt.close(fig)
print(f"saved {OUT}")
if __name__ == "__main__":
main()