clean up scripts: drop banners, version markers, unused imports; add reproducibility check
Browse files- scripts/extra_ablations.py +7 -8
- scripts/figures.py +1 -2
- scripts/gears_compare.py +2 -3
- scripts/gears_ranking.py +1 -1
- scripts/timing_and_scaling.py +8 -13
- scripts/train_all.py +1 -1
- scripts/verify_reproducibility.py +79 -0
scripts/extra_ablations.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
"""extra experiments
|
| 2 |
-
|
| 3 |
-
import sys, os, json, time
|
| 4 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 5 |
import numpy as np, torch
|
| 6 |
from src.data.perturb_data import load_dataset
|
|
@@ -16,7 +15,7 @@ gpu = int(os.environ.get("PIVOT_GPU", "3"))
|
|
| 16 |
data = load_dataset("norman")
|
| 17 |
out = {}
|
| 18 |
|
| 19 |
-
#
|
| 20 |
sp = load_split(data.dir, "perturbation")
|
| 21 |
cands = [p for p in data.perturbations if len(data.parse(p)) == 1]
|
| 22 |
targets = [p for p in sp["test_perts"] if len(data.parse(p)) == 1 and p in cands][:30]
|
|
@@ -53,7 +52,7 @@ out["guidance_no_rerank"] = {"mse": ff["mse"], "de_corr": ff["de_corr"], "mmd":
|
|
| 53 |
print("ranking_only", out["ranking_only"], flush=True)
|
| 54 |
print("guidance_no_rerank", out["guidance_no_rerank"], flush=True)
|
| 55 |
|
| 56 |
-
#
|
| 57 |
bl = BaselinePredictor(build_baseline("AvgPerturbationEffect").fit(data, sp["train_perts"], sp["train_idx"]))
|
| 58 |
ra = evaluate_nomination(bl, data, targets, cands, data.control_idx, reward_kind="cosine",
|
| 59 |
method="ranking", gene_cluster=gc, device=dev)
|
|
@@ -61,7 +60,7 @@ out["avg_effect_ranking"] = {k: ra[k] for k in ["top1", "top5", "ndcg", "func_to
|
|
| 61 |
out["avg_effect_ranking"]["med_rank"] = float(np.median(ra["_per"]["rank"]))
|
| 62 |
print("avg_effect_ranking", out["avg_effect_ranking"], flush=True)
|
| 63 |
|
| 64 |
-
#
|
| 65 |
torch.cuda.reset_peak_memory_stats(dev)
|
| 66 |
c0 = torch.as_tensor(data.emb[data.control_idx[:256]], dtype=torch.float32, device=dev)
|
| 67 |
from src.evaluation import inference as inf
|
|
@@ -70,7 +69,7 @@ _ = inf.endpoint_ranking(mf, data, cands, c0, __import__("src.evaluation.rewards
|
|
| 70 |
out["gpu_mem_mb"] = round(torch.cuda.max_memory_allocated(dev) / 1e6, 1)
|
| 71 |
print("gpu_mem_mb", out["gpu_mem_mb"], flush=True)
|
| 72 |
|
| 73 |
-
#
|
| 74 |
spc = load_split(data.dir, "combination")
|
| 75 |
combo_cands = data.combos
|
| 76 |
ctgt = [p for p in spc["test_perts"] if len(data.parse(p)) == 2][:26]
|
|
@@ -107,4 +106,4 @@ n=len(ctgt); out["combo_random"]={"exact1":r_e1/n,"exact5":r_e5/n,"overlap":r_ov
|
|
| 107 |
print("combo_additive", out["combo_additive"], "combo_random", out["combo_random"], flush=True)
|
| 108 |
|
| 109 |
save_json(out, "experiments/results/norman_extra_ablations.json")
|
| 110 |
-
print("
|
|
|
|
| 1 |
+
"""extra ablation experiments for a few of the appendix table cells."""
|
| 2 |
+
import sys, os
|
|
|
|
| 3 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 4 |
import numpy as np, torch
|
| 5 |
from src.data.perturb_data import load_dataset
|
|
|
|
| 15 |
data = load_dataset("norman")
|
| 16 |
out = {}
|
| 17 |
|
| 18 |
+
# core-ablation extras (centroid reward, held-out perturbation)
|
| 19 |
sp = load_split(data.dir, "perturbation")
|
| 20 |
cands = [p for p in data.perturbations if len(data.parse(p)) == 1]
|
| 21 |
targets = [p for p in sp["test_perts"] if len(data.parse(p)) == 1 and p in cands][:30]
|
|
|
|
| 52 |
print("ranking_only", out["ranking_only"], flush=True)
|
| 53 |
print("guidance_no_rerank", out["guidance_no_rerank"], flush=True)
|
| 54 |
|
| 55 |
+
# inverse-table baseline: average perturbation effect + ranking (cosine)
|
| 56 |
bl = BaselinePredictor(build_baseline("AvgPerturbationEffect").fit(data, sp["train_perts"], sp["train_idx"]))
|
| 57 |
ra = evaluate_nomination(bl, data, targets, cands, data.control_idx, reward_kind="cosine",
|
| 58 |
method="ranking", gene_cluster=gc, device=dev)
|
|
|
|
| 60 |
out["avg_effect_ranking"]["med_rank"] = float(np.median(ra["_per"]["rank"]))
|
| 61 |
print("avg_effect_ranking", out["avg_effect_ranking"], flush=True)
|
| 62 |
|
| 63 |
+
# gpu memory for compute table
|
| 64 |
torch.cuda.reset_peak_memory_stats(dev)
|
| 65 |
c0 = torch.as_tensor(data.emb[data.control_idx[:256]], dtype=torch.float32, device=dev)
|
| 66 |
from src.evaluation import inference as inf
|
|
|
|
| 69 |
out["gpu_mem_mb"] = round(torch.cuda.max_memory_allocated(dev) / 1e6, 1)
|
| 70 |
print("gpu_mem_mb", out["gpu_mem_mb"], flush=True)
|
| 71 |
|
| 72 |
+
# combination table: additive + random + pivot guidance (combination split)
|
| 73 |
spc = load_split(data.dir, "combination")
|
| 74 |
combo_cands = data.combos
|
| 75 |
ctgt = [p for p in spc["test_perts"] if len(data.parse(p)) == 2][:26]
|
|
|
|
| 106 |
print("combo_additive", out["combo_additive"], "combo_random", out["combo_random"], flush=True)
|
| 107 |
|
| 108 |
save_json(out, "experiments/results/norman_extra_ablations.json")
|
| 109 |
+
print("done", flush=True)
|
scripts/figures.py
CHANGED
|
@@ -8,7 +8,6 @@ import matplotlib
|
|
| 8 |
matplotlib.use("Agg")
|
| 9 |
from matplotlib import font_manager as fm
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
-
from matplotlib.patches import Patch
|
| 12 |
from matplotlib.lines import Line2D
|
| 13 |
|
| 14 |
# helvetica-family font (nimbus sans = urw helvetica clone)
|
|
@@ -169,4 +168,4 @@ if __name__ == "__main__":
|
|
| 169 |
combos = [p for p in data.perturbations if len(data.parse(p)) == 2]
|
| 170 |
figure1(model, data, [singles[0], singles[7], combos[0]], dev)
|
| 171 |
figure2_results()
|
| 172 |
-
print("
|
|
|
|
| 8 |
matplotlib.use("Agg")
|
| 9 |
from matplotlib import font_manager as fm
|
| 10 |
import matplotlib.pyplot as plt
|
|
|
|
| 11 |
from matplotlib.lines import Line2D
|
| 12 |
|
| 13 |
# helvetica-family font (nimbus sans = urw helvetica clone)
|
|
|
|
| 168 |
combos = [p for p in data.perturbations if len(data.parse(p)) == 2]
|
| 169 |
figure1(model, data, [singles[0], singles[7], combos[0]], dev)
|
| 170 |
figure2_results()
|
| 171 |
+
print("done")
|
scripts/gears_compare.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
"""real gears head-to-head on norman, aligned to our held-out-perturbation test set.
|
| 2 |
runs in the isolated pivot_gears env (torch cu118 + pyg + cell-gears, gpu-capable)."""
|
| 3 |
-
import
|
| 4 |
import numpy as np
|
| 5 |
-
import torch
|
| 6 |
|
| 7 |
# our held-out perturbation test labels (no src import; read the npz directly)
|
| 8 |
split = np.load("data/processed/norman/splits/perturbation.npz", allow_pickle=True)
|
|
@@ -55,4 +54,4 @@ for k, v in sorted(keep.items()):
|
|
| 55 |
print(f" {k:24s} {v:.4f}", flush=True)
|
| 56 |
json.dump({"n_test_perts": len(gears_test), "test_perts": gears_test, "metrics": keep},
|
| 57 |
open("experiments/results/gears_norman.json", "w"), indent=2, default=float)
|
| 58 |
-
print("
|
|
|
|
| 1 |
"""real gears head-to-head on norman, aligned to our held-out-perturbation test set.
|
| 2 |
runs in the isolated pivot_gears env (torch cu118 + pyg + cell-gears, gpu-capable)."""
|
| 3 |
+
import os, json
|
| 4 |
import numpy as np
|
|
|
|
| 5 |
|
| 6 |
# our held-out perturbation test labels (no src import; read the npz directly)
|
| 7 |
split = np.load("data/processed/norman/splits/perturbation.npz", allow_pickle=True)
|
|
|
|
| 54 |
print(f" {k:24s} {v:.4f}", flush=True)
|
| 55 |
json.dump({"n_test_perts": len(gears_test), "test_perts": gears_test, "metrics": keep},
|
| 56 |
open("experiments/results/gears_norman.json", "w"), indent=2, default=float)
|
| 57 |
+
print("done", flush=True)
|
scripts/gears_ranking.py
CHANGED
|
@@ -141,4 +141,4 @@ for grp, d in res.items():
|
|
| 141 |
agg["n_candidates_single"] = len(cand_genes)
|
| 142 |
agg["n_candidates_combo"] = len(cc) if cc else 0
|
| 143 |
json.dump(agg, open("experiments/results/gears_ranking.json", "w"), indent=2, default=float)
|
| 144 |
-
print(
|
|
|
|
| 141 |
agg["n_candidates_single"] = len(cand_genes)
|
| 142 |
agg["n_candidates_combo"] = len(cc) if cc else 0
|
| 143 |
json.dump(agg, open("experiments/results/gears_ranking.json", "w"), indent=2, default=float)
|
| 144 |
+
print(json.dumps(agg), flush=True)
|
scripts/timing_and_scaling.py
CHANGED
|
@@ -1,13 +1,8 @@
|
|
| 1 |
-
"""
|
| 2 |
-
a. pivot reward-guidance for combinatorial nomination (Table 7).
|
| 3 |
-
b. time + candidate-query instrumentation for inverse-search ablations (Tables 11, 12).
|
| 4 |
-
c. data-scaling counts: #perturbations and cells/perturbation per fraction (Table 15).
|
| 5 |
-
d. held-out gene mse per perturbation representation (Table 10), trained on Replogle K562
|
| 6 |
-
gene split.
|
| 7 |
writes experiments/results/norman_timing_scaling.json."""
|
| 8 |
-
import sys, os,
|
| 9 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 10 |
-
import numpy as np
|
| 11 |
from src.data.perturb_data import load_dataset
|
| 12 |
from src.data.splits import load_split
|
| 13 |
from src.training.train import TrainConfig, train
|
|
@@ -21,7 +16,7 @@ data = load_dataset("norman")
|
|
| 21 |
gc = data.functional_clusters(seed=0)
|
| 22 |
out = {}
|
| 23 |
|
| 24 |
-
#
|
| 25 |
spc = load_split(data.dir, "combination")
|
| 26 |
combo_cands = data.combos
|
| 27 |
ctgt = [p for p in spc["test_perts"] if len(data.parse(p)) == 2][:26]
|
|
@@ -36,7 +31,7 @@ out["combo_guidance"] = {"top1": g["top1"], "top5": g["top5"], "ndcg": g["ndcg"]
|
|
| 36 |
"endpoint_dist": g["endpoint_dist"]}
|
| 37 |
print("combo_guidance", out["combo_guidance"], flush=True)
|
| 38 |
|
| 39 |
-
#
|
| 40 |
sp = load_split(data.dir, "perturbation")
|
| 41 |
cands = [p for p in data.perturbations if len(data.parse(p)) == 1]
|
| 42 |
targets = [p for p in sp["test_perts"] if len(data.parse(p)) == 1 and p in cands][:30]
|
|
@@ -81,7 +76,7 @@ for s in [0, 5, 10, 25, 50, 100]:
|
|
| 81 |
out["guidance_step_time"][str(s)] = round(dt, 3)
|
| 82 |
print("step_time", s, round(dt, 3), flush=True)
|
| 83 |
|
| 84 |
-
#
|
| 85 |
# train_frac selects the first int(frac * n_train_perts) training perturbations.
|
| 86 |
pert_train = [str(p) for p in sp["train_perts"]]
|
| 87 |
n_train = len(pert_train)
|
|
@@ -93,7 +88,7 @@ out["data_scaling_counts"] = {str(f): {"n_perts": max(1, int(f * n_train)),
|
|
| 93 |
print("data_scaling_counts", out["data_scaling_counts"], flush=True)
|
| 94 |
save_json(out, "experiments/results/norman_timing_scaling.json") # checkpoint before slow part
|
| 95 |
|
| 96 |
-
#
|
| 97 |
rep_data = load_dataset("replogle_k562")
|
| 98 |
spg = load_split(rep_data.dir, "gene")
|
| 99 |
gene_targets = list(spg["test_perts"])[:60]
|
|
@@ -112,4 +107,4 @@ for rep in ["op_only", "gene_only", "random_id", "gene_op", "gene_pathway_op"]:
|
|
| 112 |
print("heldout_gene_mse", rep, out["heldout_gene_mse"][rep], flush=True)
|
| 113 |
save_json(out, "experiments/results/norman_timing_scaling.json")
|
| 114 |
|
| 115 |
-
print("
|
|
|
|
| 1 |
+
"""timing, data-scaling counts and held-out-gene mse for a few appendix tables.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
writes experiments/results/norman_timing_scaling.json."""
|
| 3 |
+
import sys, os, time
|
| 4 |
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 5 |
+
import numpy as np
|
| 6 |
from src.data.perturb_data import load_dataset
|
| 7 |
from src.data.splits import load_split
|
| 8 |
from src.training.train import TrainConfig, train
|
|
|
|
| 16 |
gc = data.functional_clusters(seed=0)
|
| 17 |
out = {}
|
| 18 |
|
| 19 |
+
# pivot reward-guidance for combinatorial nomination (Table 7)
|
| 20 |
spc = load_split(data.dir, "combination")
|
| 21 |
combo_cands = data.combos
|
| 22 |
ctgt = [p for p in spc["test_perts"] if len(data.parse(p)) == 2][:26]
|
|
|
|
| 31 |
"endpoint_dist": g["endpoint_dist"]}
|
| 32 |
print("combo_guidance", out["combo_guidance"], flush=True)
|
| 33 |
|
| 34 |
+
# time + query instrumentation for inverse search (Tables 11, 12)
|
| 35 |
sp = load_split(data.dir, "perturbation")
|
| 36 |
cands = [p for p in data.perturbations if len(data.parse(p)) == 1]
|
| 37 |
targets = [p for p in sp["test_perts"] if len(data.parse(p)) == 1 and p in cands][:30]
|
|
|
|
| 76 |
out["guidance_step_time"][str(s)] = round(dt, 3)
|
| 77 |
print("step_time", s, round(dt, 3), flush=True)
|
| 78 |
|
| 79 |
+
# data-scaling counts (Table 15)
|
| 80 |
# train_frac selects the first int(frac * n_train_perts) training perturbations.
|
| 81 |
pert_train = [str(p) for p in sp["train_perts"]]
|
| 82 |
n_train = len(pert_train)
|
|
|
|
| 88 |
print("data_scaling_counts", out["data_scaling_counts"], flush=True)
|
| 89 |
save_json(out, "experiments/results/norman_timing_scaling.json") # checkpoint before slow part
|
| 90 |
|
| 91 |
+
# held-out gene mse per representation (Table 10), Replogle K562 gene split
|
| 92 |
rep_data = load_dataset("replogle_k562")
|
| 93 |
spg = load_split(rep_data.dir, "gene")
|
| 94 |
gene_targets = list(spg["test_perts"])[:60]
|
|
|
|
| 107 |
print("heldout_gene_mse", rep, out["heldout_gene_mse"][rep], flush=True)
|
| 108 |
save_json(out, "experiments/results/norman_timing_scaling.json")
|
| 109 |
|
| 110 |
+
print("done", flush=True)
|
scripts/train_all.py
CHANGED
|
@@ -46,4 +46,4 @@ for ms in MATCH_STRATEGIES:
|
|
| 46 |
if ms != "batch":
|
| 47 |
go("%s/match_%s" % (A, ms), "norman", data, split="perturbation", epochs=45, match=ms)
|
| 48 |
|
| 49 |
-
print("
|
|
|
|
| 46 |
if ms != "batch":
|
| 47 |
go("%s/match_%s" % (A, ms), "norman", data, split="perturbation", epochs=45, match=ms)
|
| 48 |
|
| 49 |
+
print("done", flush=True)
|
scripts/verify_reproducibility.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# reproduce every model-dependent number in experiments/results/ from the saved
|
| 2 |
+
# checkpoints and diff against the committed json. any mismatch = a fake or a
|
| 3 |
+
# reproducibility break. loads the released weights, re-runs the exact eval.
|
| 4 |
+
import os, sys, json
|
| 5 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
| 6 |
+
import torch
|
| 7 |
+
from src.data.perturb_data import load_dataset, MATCH_STRATEGIES
|
| 8 |
+
from src.data.splits import load_split
|
| 9 |
+
from src.training.train import TrainConfig, make_model
|
| 10 |
+
from src.experiments.predictors import PivotPredictor
|
| 11 |
+
from src.experiments.forward_eval import evaluate_forward
|
| 12 |
+
from src.experiments.run_ablations import _fwd_inv
|
| 13 |
+
|
| 14 |
+
RES = "experiments/results"
|
| 15 |
+
GPU = "cuda:%d" % int(os.environ.get("PIVOT_GPU", "3"))
|
| 16 |
+
TOL = 1e-3
|
| 17 |
+
checks = [] # (label, ok, detail)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def load(path, data):
|
| 21 |
+
cfg = TrainConfig(**json.load(open(os.path.join(path, "config.json"))))
|
| 22 |
+
m = make_model(data, cfg, GPU)
|
| 23 |
+
m.load_state_dict(torch.load(os.path.join(path, "model.pt"), map_location=GPU))
|
| 24 |
+
m.eval()
|
| 25 |
+
return m
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def cmp(label, got, exp):
|
| 29 |
+
keys = [k for k in exp if isinstance(exp[k], (int, float))
|
| 30 |
+
and isinstance(got.get(k), (int, float))]
|
| 31 |
+
diff = {k: (round(got[k], 4), round(exp[k], 4)) for k in keys if abs(got[k] - exp[k]) > TOL}
|
| 32 |
+
checks.append((label, not diff, diff))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def fwd_pivot(data, model, split, max_perts=80):
|
| 36 |
+
sp = load_split(data.dir, split)
|
| 37 |
+
test = list(sp["test_perts"]) if split != "cell" else [p for p in data.perturbations if len(data.parse(p)) == 1]
|
| 38 |
+
cp = sp["test_idx"][data.is_control[sp["test_idx"]]]
|
| 39 |
+
if len(cp) < 50:
|
| 40 |
+
cp = data.control_idx
|
| 41 |
+
return evaluate_forward(PivotPredictor(model, data, GPU), data, test, cp, max_perts=max_perts)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ---- main forward tables (PIVOT row) ----
|
| 45 |
+
for ds, splits in [("norman", ["cell", "perturbation", "combination"]),
|
| 46 |
+
("replogle_k562", ["cell", "perturbation", "gene"])]:
|
| 47 |
+
data = load_dataset(ds)
|
| 48 |
+
for sp in splits:
|
| 49 |
+
j = json.load(open("%s/%s_forward_%s.json" % (RES, ds, sp)))["models"]["PIVOT"]
|
| 50 |
+
m = load("models/%s/%s" % (ds, sp), data)
|
| 51 |
+
cmp("forward %s/%s PIVOT" % (ds, sp), fwd_pivot(data, m, sp), j)
|
| 52 |
+
|
| 53 |
+
# ---- ablation tables (each row -> its checkpoint), norman/perturbation ----
|
| 54 |
+
data = load_dataset("norman")
|
| 55 |
+
A = "models/ablations/norman_perturbation"
|
| 56 |
+
comp = {"flow-map-only": "comp_map", "no-tangent": "comp_map_semi",
|
| 57 |
+
"no-semigroup": "comp_map_tan", "PIVOT-full": "default"}
|
| 58 |
+
rep = {"gene_op": "default", "op_only": "rep_op_only", "gene_only": "rep_gene_only",
|
| 59 |
+
"random_id": "rep_random_id", "gene_pathway_op": "rep_gene_pathway_op"}
|
| 60 |
+
frac = {"0.1": "frac_0.1", "0.25": "frac_0.25", "0.5": "frac_0.5", "0.75": "frac_0.75", "1.0": "default"}
|
| 61 |
+
match = {ms: ("default" if ms == "batch" else "match_%s" % ms) for ms in MATCH_STRATEGIES}
|
| 62 |
+
|
| 63 |
+
for jname, mapping in [("components", comp), ("representation", rep), ("datascale", frac), ("matching", match)]:
|
| 64 |
+
rows = json.load(open("%s/norman_ablation_%s.json" % (RES, jname)))["rows"]
|
| 65 |
+
for row, folder in mapping.items():
|
| 66 |
+
if row not in rows:
|
| 67 |
+
continue
|
| 68 |
+
m = load("%s/%s" % (A, folder), data)
|
| 69 |
+
r = _fwd_inv(data, m, "perturbation")
|
| 70 |
+
cmp("ablation %s[%s] forward" % (jname, row), r["forward"], rows[row]["forward"])
|
| 71 |
+
cmp("ablation %s[%s] inverse" % (jname, row), r["inverse"], rows[row]["inverse"])
|
| 72 |
+
|
| 73 |
+
# ---- summary ----
|
| 74 |
+
npass = sum(1 for _, ok, _ in checks if ok)
|
| 75 |
+
print()
|
| 76 |
+
for label, ok, diff in checks:
|
| 77 |
+
print(("PASS " if ok else "FAIL ") + label + ("" if ok else " mismatch=%s" % diff))
|
| 78 |
+
print("\n%d/%d checks reproduced within tol=%g" % (npass, len(checks), TOL))
|
| 79 |
+
print("RESULT:", "ALL REPRODUCED - no fakes" if npass == len(checks) else "MISMATCHES FOUND")
|