Shape2Force / experiments /evaluate.py
kaveh's picture
run_all: auto-resume crashed jobs; spheroid colour scale 50 Pa
3c48411
Raw History Blame
11.8 kB
"""
Evaluate released checkpoints, baselines and trained ablation runs on every split manifest.
python -m experiments.evaluate --released --meanmap # released models + input-free baseline
python -m experiments.evaluate --released --sweep # + substrate-setting sensitivity
python -m experiments.evaluate --stage1 <path>/last_checkpoint_final.pth # image-only stage-1 model
python -m experiments.evaluate --runs # every experiments/runs/*/best_checkpoint.pth
Metrics are computed PER IMAGE (mean and SD are then taken across images):
mse, ms_ssim, pixel_r (Pearson), rme (per-image WFM-RME), mean_gt / mean_pred (heatmap units),
mean_gt_Pa / mean_pred_Pa (x colour-scale limit of the set, 500 Pa single cells, 50 Pa spheroids),
gt_frac_saturated (pixels >= 0.98 of the colour limit), inference time.
Outputs (experiments/results/eval/ by default; existing rows of other models are kept):
per_image.csv, summary.csv
"""
import argparse
import glob
import json
import os
import time
import numpy as np
import pandas as pd
import torch
from experiments.ablation_models import build_generator, n_params
from experiments.layout import DEFAULT_DATASET_ROOT, RESULTS_DIR, RUNS_DIR, S2F_ROOT, list_manifests
from experiments.losses import PerImageWFMRMELoss
from experiments.manifest_data import make_loader
from models.s2f_model import create_s2f_model, create_settings_channels
from utils.metrics import TorchMetricsWrapper
from utils.substrate_settings import compute_settings_normalization
CONFIG_PATH = os.path.join(S2F_ROOT, "config", "substrate_settings.json")
SWEEP_KEYS = ["PDMS_1kPa_3um", "fibroblasts_PDMS", "PDMS_10kPa_3um"]
def released_models(device):
out = {}
g, _ = create_s2f_model(in_channels=3, model_type="s2f")
g.load_state_dict(torch.load(os.path.join(S2F_ROOT, "ckp/single_cell/ckp_singlecell_GN.pth"), map_location="cpu",
weights_only=False)["generator_state_dict"], strict=True)
out["released_singlecell"] = dict(model=g.to(device).eval(), kind="singlecell", in_channels=3, tanh=True)
g, _ = create_s2f_model(in_channels=1, model_type="s2f")
g.load_state_dict(torch.load(os.path.join(S2F_ROOT, "ckp/spheroid/ckp_spheroid_GN.pth"), map_location="cpu",
weights_only=False)["generator_state_dict"], strict=True)
out["released_spheroid_GN"] = dict(model=g.to(device).eval(), kind="spheroid", in_channels=1, tanh=True)
fn = os.path.join(S2F_ROOT, "ckp/spheroid/ckp_spheroid_FN.pth")
if os.path.exists(fn):
g, _ = create_s2f_model(in_channels=1, model_type="s2f_spheroid")
g.load_state_dict(torch.load(fn, map_location="cpu", weights_only=False)["model_state_dict"], strict=True)
g.set_output_mode(use_tanh=False)
out["supervised_spheroid_FN"] = dict(model=g.to(device).eval(), kind="spheroid", in_channels=1, tanh=False)
return out
def stage1_model(path, device):
g, _ = create_s2f_model(in_channels=1, model_type="s2f")
sd = torch.load(path, map_location="cpu", weights_only=False)["generator_state_dict"]
g.load_state_dict({k.replace("denoise_module", "initial_conv"): v for k, v in sd.items()}, strict=True)
return dict(model=g.to(device).eval(), kind="singlecell", in_channels=1, tanh=True)
def run_models(device, pattern):
out = {}
for p in sorted(glob.glob(pattern)):
h = os.path.join(os.path.dirname(p), "history.json")
ck = torch.load(p, map_location="cpu", weights_only=False)
cfg = ck["config"]
done = os.path.exists(h) and len(json.load(open(h))) >= cfg.get("epochs", 0)
if not done and not os.environ.get("S2F_EVAL_UNFINISHED"):
print(f" skipping unfinished run {os.path.basename(os.path.dirname(p))} (set S2F_EVAL_UNFINISHED=1 to include)")
continue
g = build_generator(**cfg); g.load_state_dict(ck["generator_state_dict"], strict=True)
name = os.path.basename(os.path.dirname(p))
out[name] = dict(model=g.to(device).eval(), kind="singlecell", in_channels=cfg["in_channels"], tanh=True,
substrate_mode=cfg["substrate_mode"], input_mode=cfg["input_mode"],
epoch=ck.get("epoch"), val_loss=ck.get("val_loss"))
return out
def mean_map(manifest, dataset_root):
acc, n = None, 0
for b in make_loader(manifest, dataset_root, batch_size=8, substrate_mode="none"):
acc = b[1].sum(0, keepdim=True) if acc is None else acc + b[1].sum(0, keepdim=True); n += b[1].size(0)
return acc / n
def evaluate_one(name, spec, manifest, dataset_root, device, mw, norm, substrate_mode, batch, mean_pred=None):
input_mode = spec.get("input_mode", "bf")
use_settings = spec.get("in_channels", 1) == 3
sm = substrate_mode if use_settings else "none"
forced = None
if sm and sm.startswith("force:"):
forced, sm = sm.split(":", 1)[1], "reference"
try:
loader = make_loader(manifest, dataset_root, batch_size=batch, substrate_mode=sm, input_mode=input_mode)
except FileNotFoundError as e:
print(f" [skip] {name} on {manifest}: {e}"); return []
rows_meta, rec, out, k = loader.dataset.rows, PerImageWFMRMELoss(), [], 0
with torch.no_grad():
for b in loader:
x, y = b[0].to(device), b[1].to(device)
t0 = time.time()
if mean_pred is not None:
p = mean_pred.to(device).expand_as(y)
else:
if use_settings:
meta = b[4]
if forced:
meta = dict(meta, substrate=[forced] * x.size(0))
x = torch.cat([x, create_settings_channels(meta, norm, device, x.shape, config_path=CONFIG_PATH)], 1)
p = spec["model"](x)
p = (p + 1) / 2 if spec.get("tanh", True) else p
dt = (time.time() - t0) / x.size(0)
for i in range(y.size(0)):
r = rows_meta[k]; k += 1
t, q = y[i:i + 1], p[i:i + 1]
yt = float(r.get("ythreshold_Pa") or (50 if "spheroid" in manifest else 500))
out.append({"model": name, "manifest": manifest, "substrate_mode": substrate_mode if use_settings else "n/a",
"sample_dir": r["sample_dir"], "set": r.get("set", ""), "condition": r.get("condition", ""),
"mse": mw.compute_mse(t, q), "ms_ssim": mw.compute_ms_ssim(t, q),
"pixel_r": float(np.corrcoef(t.flatten().cpu().numpy(), q.flatten().cpu().numpy())[0, 1]),
"rme": float(rec(q, t)), "mean_gt": float(t.mean()), "mean_pred": float(q.mean()),
"mean_gt_Pa": float(t.mean()) * yt, "mean_pred_Pa": float(q.mean()) * yt,
"gt_max": float(t.max()), "gt_frac_saturated": float((t >= 0.98).float().mean()),
"seconds_per_image": dt})
return out
def summarise(df):
df = df.assign(substrate_mode=df["substrate_mode"].fillna("none")) # "n/a" is read back from CSV as NaN
g = df.groupby(["model", "manifest", "substrate_mode"], dropna=False)
s = g.agg(n=("mse", "size"), **{f"{m}_{f}": (m, f) for m in ["mse", "ms_ssim", "pixel_r", "rme", "mean_gt_Pa",
"mean_pred_Pa", "gt_frac_saturated"] for f in ["mean", "std"]},
seconds_per_image=("seconds_per_image", "mean")).reset_index()
s["pred_over_gt"] = s["mean_pred_Pa_mean"] / s["mean_gt_Pa_mean"]
return s
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--released", action="store_true", help="released single-cell + spheroid checkpoints")
ap.add_argument("--sweep", action="store_true", help="released single-cell model with 1 / 6 / 10 kPa settings")
ap.add_argument("--meanmap", action="store_true", help="input-free baseline (mean training map)")
ap.add_argument("--stage1", default=None, help="path to last_checkpoint_final.pth (image-only stage 1)")
ap.add_argument("--runs", nargs="?", const=os.path.join(RUNS_DIR, "*", "best_checkpoint.pth"), default=None,
help="glob of trained run checkpoints (default experiments/runs/*/best_checkpoint.pth)")
ap.add_argument("--manifests", default="", help="comma-separated manifest names (default: all in experiments/splits)")
ap.add_argument("--dataset-root", default=DEFAULT_DATASET_ROOT)
ap.add_argument("--out", default=os.path.join(RESULTS_DIR, "eval"))
ap.add_argument("--batch", type=int, default=2)
ap.add_argument("--device", default=None)
a = ap.parse_args()
device = torch.device(a.device or ("cuda" if torch.cuda.is_available() else "cpu"))
mans = [m for m in (a.manifests.split(",") if a.manifests else list_manifests()) if m]
sc_mans = [m for m in mans if not m.startswith("spheroid") and m != "alltune_train" and m != "zenodo_v1_train"]
sph_mans = [m for m in mans if m.startswith("spheroid") and m != "spheroid_train"]
print(f"device {device}; single-cell manifests {sc_mans}; spheroid manifests {sph_mans}")
mw, norm = TorchMetricsWrapper(device=device), compute_settings_normalization(config_path=CONFIG_PATH)
models = {}
if a.released:
models.update(released_models(device))
if a.stage1:
models["stage1_image_only"] = stage1_model(a.stage1, device)
if a.runs:
models.update(run_models(device, a.runs))
rows, params = [], []
for name, spec in models.items():
params.append({"model": name, "n_params_M": n_params(spec["model"]) / 1e6, "epoch": spec.get("epoch"),
"val_loss": spec.get("val_loss")})
targets = sc_mans if spec["kind"] == "singlecell" else sph_mans
modes = [spec.get("substrate_mode", "legacy")]
if name == "released_singlecell":
modes = ["legacy", "nominal"] + ([f"force:{k}" for k in SWEEP_KEYS] if a.sweep else [])
for m in targets:
for sm in modes:
print(f" {name:28s} {m:28s} {sm}", flush=True)
rows += evaluate_one(name, spec, m, a.dataset_root, device, mw, norm, sm, a.batch)
if a.meanmap:
for train_m, targets, name in [("alltune_train", sc_mans, "baseline_mean_map_singlecell"),
("spheroid_train", sph_mans, "baseline_mean_map_spheroid")]:
if train_m not in list_manifests():
continue
M = mean_map(train_m, a.dataset_root)
for m in targets:
print(f" {name:28s} {m}", flush=True)
rows += evaluate_one(name, {"in_channels": 1}, m, a.dataset_root, device, mw, norm, "n/a", a.batch, mean_pred=M)
os.makedirs(a.out, exist_ok=True)
new = pd.DataFrame(rows)
pi = os.path.join(a.out, "per_image.csv")
if os.path.exists(pi) and len(new): # keep rows of models not re-evaluated now
old = pd.read_csv(pi)
new = pd.concat([old[~old.model.isin(new.model.unique())], new], ignore_index=True)
if len(new):
new.to_csv(pi, index=False)
summarise(new).to_csv(os.path.join(a.out, "summary.csv"), index=False)
pp = os.path.join(a.out, "model_params.csv")
if params:
P = pd.DataFrame(params)
if os.path.exists(pp):
old = pd.read_csv(pp); P = pd.concat([old[~old.model.isin(P.model)], P], ignore_index=True)
P.to_csv(pp, index=False)
print(f"wrote {pi}, summary.csv, model_params.csv in {a.out}")
if __name__ == "__main__":
main()