Rootscope / rootscope /context.py
ct-tranchau's picture
RootScope v4: manuscript model (LightGBM per round, 3 seeds, layer + soft-neighbor context, radial prior)
47c4bf8 verified
Raw
History Blame Contribute Delete
6.82 kB
"""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)