Spaces:
Running on Zero
Running on Zero
RootScope v4: manuscript model (LightGBM per round, 3 seeds, layer + soft-neighbor context, radial prior)
47c4bf8 verified | """Dac trung ngu canh cua RootScope v4: | |
| - hang xom bon huong, dang MEM (xac suat) thay vi nhan cung | |
| - khoang cach toi cac LOP mo ma chinh model da du doan o vong truoc | |
| CAT NGUYEN VAN tu Working/_figgrid/eval_grid.py (2026-09-21). Khong go lai bang tay: | |
| mot sai lech nho o day cho ket qua sai ma khong bao loi nao. | |
| """ | |
| import os | |
| import numpy as np, pandas as pd, scipy.sparse as sp | |
| from ._eval_honest import _adjacency_for, fill_neighbors, _blank # noqa: F401 | |
| MASK = None # thu muc chua <stem>_masks.npy; predict.py dat truoc khi goi | |
| CL = None # danh sach ten lop; predict.py dat truoc khi goi | |
| DIRS = ["inner", "outer", "cw", "ccw"] | |
| def dir_mats(g): | |
| """4 ma tran thua (n x n), chuan hoa theo hang: hang xom theo tung huong cuc.""" | |
| r, c = {k: [] for k in DIRS}, {k: [] for k in DIRS} | |
| for sf, sub in g.groupby("source_file"): | |
| adj = _adjacency_for(sf, MASK) | |
| row = {int(i): p for i, p in zip(sub.cell_id.values, sub.index.values)} | |
| cy0, cx0 = sub.centroid_y.mean(), sub.centroid_x.mean() | |
| cen = {int(i): (y, x) for i, y, x in zip(sub.cell_id, sub.centroid_y, sub.centroid_x)} | |
| for cid, (cy, cx) in cen.items(): | |
| md = np.hypot(cy - cy0, cx - cx0); ma = np.arctan2(cy - cy0, cx - cx0) | |
| for n in adj.get(cid, ()): | |
| if n not in cen: | |
| continue | |
| ny, nx = cen[n] | |
| dr = np.hypot(ny - cy0, nx - cx0) - md | |
| da = (np.arctan2(ny - cy0, nx - cx0) - ma + np.pi) % (2 * np.pi) - np.pi | |
| arc = abs(da) * md if md > 1.0 else abs(da) | |
| k = ("inner" if dr < 0 else "outer") if abs(dr) >= arc else ("cw" if da < 0 else "ccw") | |
| r[k].append(row[cid]); c[k].append(row[n]) | |
| n = len(g); mats = {} | |
| for k in DIRS: | |
| A = sp.csr_matrix((np.ones(len(r[k])), (r[k], c[k])), shape=(n, n)) | |
| deg = np.asarray(A.sum(1)).ravel() | |
| mats[k] = (sp.diags(1 / np.maximum(deg, 1)) @ A, deg > 0) | |
| return mats | |
| def soft_feats(P, mats, classes): | |
| n = len(mats["cw"][1]); cols = {} | |
| if P is None: | |
| names = [f"nbp_{k}_{c}" for k in DIRS for c in classes] + [f"agree_{k}" for k in DIRS] + ["agree_cw_ccw", "agree_in_out"] | |
| return pd.DataFrame(-1.0, index=range(n), columns=names) | |
| M = {} | |
| for k in DIRS: | |
| A, has = mats[k]; M[k] = A @ P | |
| for j, c in enumerate(classes): | |
| cols[f"nbp_{k}_{c}"] = np.where(has, M[k][:, j], -1.0) | |
| for k in DIRS: | |
| cols[f"agree_{k}"] = np.where(mats[k][1], (P * M[k]).sum(1), -1.0) | |
| cols["agree_cw_ccw"] = np.where(mats["cw"][1] & mats["ccw"][1], (M["cw"] * M["ccw"]).sum(1), -1.0) | |
| cols["agree_in_out"] = np.where(mats["inner"][1] & mats["outer"][1], (M["inner"] * M["outer"]).sum(1), -1.0) | |
| return pd.DataFrame(cols) | |
| LAND = ["epidermis", "exodermis", "endodermis", "pericycle"] | |
| RINGCOLS = ["depth_surf", "frac_r_surf"] + [f"d_{c}" for c in LAND] + ["d_vasc_own", "d_endo_own", "ratio_endo"] | |
| RING = int(os.environ.get("RINGFEAT", "1")) | |
| PERROUND = int(os.environ.get("PERROUND", "1")) # inference uses the model trained FOR each round | |
| SOFTRING = int(os.environ.get("SOFTRING", "0")) # landmarks weighted by probability instead of argmax | |
| DAMP = float(os.environ.get("DAMP", "0")) # P_used = DAMP*P_prev + (1-DAMP)*P_new | |
| _STATIC = {} | |
| def _adiff(a, b): return np.abs((a[:, None] - b[None, :] + np.pi) % (2 * np.pi) - np.pi) | |
| def ring_feats(g, names, key, Pm=None): | |
| """Where is each cell relative to landmarks of ITS OWN section? Static: the tissue surface. Dynamic (from the | |
| previous round's predictions; out-of-fold during training): the predicted epidermis / exodermis / endodermis / | |
| pericycle rings and the outer envelope of the vascular cylinder. Distances are in cell diameters, measured along | |
| the local radius, so a non-circular root or an off-centre stele does not matter.""" | |
| out = np.full((len(g), len(RINGCOLS)), np.nan, dtype=np.float32) | |
| if not RING: return pd.DataFrame(out, columns=RINGCOLS) | |
| if key not in _STATIC: | |
| st = np.full((len(g), 2), np.nan, dtype=np.float32) | |
| for sf, sub in g.groupby("source_file"): | |
| idx = sub.index.values; cy, cx = sub.centroid_y.mean(), sub.centroid_x.mean() | |
| r = np.hypot(sub.centroid_y - cy, sub.centroid_x - cx).values; th = np.arctan2(sub.centroid_y - cy, sub.centroid_x - cx).values | |
| near = _adiff(th, th) < np.deg2rad(15); rs = np.where(near, r[None, :], -1).max(1); dm = np.sqrt(np.median(sub.area_px)) | |
| st[idx, 0] = (rs - r) / dm; st[idx, 1] = r / np.maximum(rs, 1) | |
| _STATIC[key] = st | |
| out[:, :2] = _STATIC[key] | |
| if names is None: return pd.DataFrame(out, columns=RINGCOLS) | |
| names = np.asarray(names) | |
| for sf, sub in g.groupby("source_file"): | |
| idx = sub.index.values; nm = names[idx]; own = np.sqrt(sub.area_px.values) | |
| core = np.isin(nm, ["endodermis", "pericycle", "stele", "xylem", "phloem"]) | |
| cy, cx = (sub.centroid_y[core].mean(), sub.centroid_x[core].mean()) if core.sum() >= 5 else (sub.centroid_y.mean(), sub.centroid_x.mean()) | |
| r = np.hypot(sub.centroid_y - cy, sub.centroid_x - cx).values; th = np.arctan2(sub.centroid_y - cy, sub.centroid_x - cx).values | |
| def ring(sel, how="median", win=40): | |
| m = _adiff(th, th[sel]) < np.deg2rad(win); A = np.where(m, r[sel][None, :], np.nan) | |
| v = np.nanmedian(A, 1) if how == "median" else np.nanmax(A, 1) | |
| fb = np.median(r[sel]) if how == "median" else np.percentile(r[sel], 90) | |
| return np.where(m.sum(1) >= 2, v, fb) | |
| import warnings | |
| with warnings.catch_warnings(): | |
| warnings.simplefilter("ignore") | |
| for j, c in enumerate(LAND): | |
| if SOFTRING and Pm is not None: | |
| w = Pm[idx, CL.index(c)]; sel = w > 0.2 | |
| if w[sel].sum() < 4: continue | |
| ww = w[sel] ** 2; mwin = (_adiff(th, th[sel]) < np.deg2rad(40)) * ww[None, :] | |
| tot = mwin.sum(1); glob = np.average(r[sel], weights=ww) | |
| rc = np.where(tot > ww.max() * 1.5, (mwin * r[sel][None, :]).sum(1) / np.maximum(tot, 1e-9), glob) | |
| dm = np.sqrt(np.average(sub.area_px.values[sel], weights=ww)) | |
| else: | |
| sel = nm == c | |
| if sel.sum() < 6: continue | |
| rc = ring(sel); dm = np.sqrt(np.median(sub.area_px.values[sel])) | |
| out[idx, 2 + j] = (r - rc) / dm | |
| if c == "endodermis": out[idx, 7] = (r - rc) / own; out[idx, 8] = r / np.maximum(rc, 1) | |
| sel = np.isin(nm, ["stele", "xylem", "phloem"]) | |
| if sel.sum() >= 5: out[idx, 6] = (r - ring(sel, "max", 30)) / own | |
| return pd.DataFrame(out, columns=RINGCOLS) | |