{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import torch, torch.nn as nn, torch.nn.functional as F\nimport numpy as np, time, os, json\nfrom pathlib import Path\nfrom scipy.ndimage import binary_dilation\nfrom sklearn.metrics import f1_score, precision_score, recall_score\nimport tifffile\nfrom PIL import Image\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'✓ {DEVICE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:52:06.638117Z","iopub.execute_input":"2026-06-29T23:52:06.638416Z","iopub.status.idle":"2026-06-29T23:52:13.069613Z","shell.execute_reply.started":"2026-06-29T23:52:06.638392Z","shell.execute_reply":"2026-06-29T23:52:13.068713Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================================================================\n# SUBIT-BELNAP COMBINER v2 — Vesuvius Challenge\n# ================================================================\n# Виправлено:\n#   - truth table (A^B = BOTH, A&B = INK)\n#   - soft-Belnap через evidence accumulation\n#   - 3-domain combiner (central / upper / lower layers)\n#   - Belnap entropy map\n#   - SUBIT-INK score\n# ================================================================\n \n \n# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 1 — Середовище\n# ════════════════════════════════════════════════════════════════\nimport os, torch, numpy as np\nfrom pathlib import Path\n \nprint(f'PyTorch: {torch.__version__}')\nif torch.cuda.is_available():\n    for i in range(torch.cuda.device_count()):\n        print(f'  GPU {i}: {torch.cuda.get_device_name(i)}  '\n              f'({torch.cuda.get_device_properties(i).total_memory/1e9:.1f} GB)')\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device: {DEVICE}')\n \nINK_PATHS = list(Path('/kaggle/input').rglob('inklabels.png'))\nTIF_DIRS  = list(set(p.parent for p in Path('/kaggle/input').rglob('*.tif')))\nassert INK_PATHS, \"Add Data → vesuvius-challenge-ink-detection\"\nINK_PATH  = INK_PATHS[0]\nVOL_DIR   = sorted(TIF_DIRS, key=lambda p: len(list(p.glob('*.tif'))), reverse=True)[0]\ntif_files = sorted(VOL_DIR.glob('*.tif'))\nprint(f'✓ Шарів: {len(tif_files)}  INK: {INK_PATH}')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:52:13.071442Z","iopub.execute_input":"2026-06-29T23:52:13.071889Z","iopub.status.idle":"2026-06-29T23:52:14.077348Z","shell.execute_reply.started":"2026-06-29T23:52:13.071862Z","shell.execute_reply":"2026-06-29T23:52:14.076520Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 2 — Дані + три домени\n# ════════════════════════════════════════════════════════════════\nimport tifffile\nfrom PIL import Image\n \nLAYERS    = 65\nCROP_SIZE = 2048\nDEPTH     = 7\n \nink_full = (np.array(Image.open(str(INK_PATH)).convert('L')) > 128).astype(np.float32)\nH_ink, W_ink = ink_full.shape\nsample   = tifffile.imread(str(tif_files[0]))\nH_tif, W_tif = sample.shape[-2], sample.shape[-1]\nscale_y, scale_x = H_tif/H_ink, W_tif/W_ink\niy, ix   = np.where(ink_full > 0)\ncy_ink, cx_ink = int(iy.mean()), int(ix.mean())\ncy_tif = int(cy_ink*scale_y); cx_tif = int(cx_ink*scale_x)\nhc = CROP_SIZE//2\ny1 = max(0,min(cy_tif-hc,H_tif-CROP_SIZE)); x1 = max(0,min(cx_tif-hc,W_tif-CROP_SIZE))\ny2,x2 = y1+CROP_SIZE,x1+CROP_SIZE\ny1i,y2i = int(y1/scale_y),int(y2/scale_y)\nx1i,x2i = int(x1/scale_x),int(x2/scale_x)\n \nprint(f'Завантажуємо {min(LAYERS,len(tif_files))} шарів...')\nlayers = []\nfor i,f in enumerate(tif_files[:LAYERS]):\n    raw = tifffile.imread(str(f))\n    if raw.ndim==3: raw=raw[0]\n    layers.append(raw[y1:y2,x1:x2].astype(np.float32))\n    if (i+1)%10==0: print(f'  {i+1}/{min(LAYERS,len(tif_files))}')\n \nvolume = np.stack(layers)\nv_min,v_max = float(volume.min()),float(volume.max())\nvolume = (volume-v_min)/(v_max-v_min+1e-8)\nink_crop = ink_full[y1i:y2i,x1i:x2i]\nink = np.array(Image.fromarray(ink_crop).resize(\n    (volume.shape[2],volume.shape[1]),Image.NEAREST))\n \n# ── Три незалежні домени ──\nmid  = volume.shape[0]//2\nhalf = DEPTH//2\nshift = 8\n \n# Domain A: центральні шари (core signal)\nvol_A = volume[mid-half : mid+half+1].mean(axis=0).astype(np.float32)\n# Domain B: верхні шари (upper surface)\nvol_B = volume[mid-half-shift : mid+half+1-shift].mean(axis=0).astype(np.float32)\n# Domain C: нижні шари (lower surface)\nvol_C = volume[mid-half+shift : mid+half+1+shift].mean(axis=0).astype(np.float32)\n \nprint(f'\\nVolume: {volume.shape}')\nprint(f'Domain A: шари {mid-half}:{mid+half+1} (central)')\nprint(f'Domain B: шари {mid-half-shift}:{mid+half+1-shift} (upper)')\nprint(f'Domain C: шари {mid-half+shift}:{mid+half+1+shift} (lower)')\nprint(f'Ink coverage: {ink.mean()*100:.1f}%')\n \nimport matplotlib.pyplot as plt\nfig,axes=plt.subplots(1,4,figsize=(22,5)); fig.patch.set_facecolor('#111')\nfor ax in axes: ax.set_facecolor('#111'); ax.axis('off')\naxes[0].imshow(vol_A,cmap='bone'); axes[0].set_title('Domain A (central)',color='white')\naxes[1].imshow(vol_B,cmap='bone'); axes[1].set_title('Domain B (upper)',color='white')\naxes[2].imshow(vol_C,cmap='bone'); axes[2].set_title('Domain C (lower)',color='white')\naxes[3].imshow(ink,  cmap='gray'); axes[3].set_title('Ground Truth',color='white')\nplt.tight_layout(); plt.show()\nprint('✓ Три домени готові')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:52:14.078656Z","iopub.execute_input":"2026-06-29T23:52:14.078889Z","iopub.status.idle":"2026-06-29T23:52:57.002947Z","shell.execute_reply.started":"2026-06-29T23:52:14.078867Z","shell.execute_reply":"2026-06-29T23:52:57.002128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nfrom scipy.ndimage import binary_dilation\nfrom sklearn.metrics import f1_score, precision_score, recall_score\nimport time\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nfrom scipy.ndimage import binary_dilation\nfrom sklearn.metrics import f1_score, precision_score, recall_score\nimport time\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'✓ Device: {DEVICE}')\nprint('✓ Імпорти готові')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:52:57.004028Z","iopub.execute_input":"2026-06-29T23:52:57.004361Z","iopub.status.idle":"2026-06-29T23:52:57.010287Z","shell.execute_reply.started":"2026-06-29T23:52:57.004337Z","shell.execute_reply":"2026-06-29T23:52:57.009358Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 3 — Модель + ВИПРАВЛЕНИЙ Belnap Combiner\n# ════════════════════════════════════════════════════════════════\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom scipy.ndimage import binary_dilation\nfrom sklearn.metrics import f1_score, precision_score, recall_score\nimport time\n \nclass DC2d(nn.Module):\n    def __init__(self,i,o):\n        super().__init__()\n        self.b=nn.Sequential(\n            nn.Conv2d(i,o,3,padding=1,bias=False),nn.BatchNorm2d(o),nn.ReLU(inplace=True),\n            nn.Conv2d(o,o,3,padding=1,bias=False),nn.BatchNorm2d(o),nn.ReLU(inplace=True))\n    def forward(self,x): return self.b(x)\n \nclass InkNet(nn.Module):\n    def __init__(self,base=32):\n        super().__init__()\n        self.e1=DC2d(1,base)\n        self.e2=nn.Sequential(nn.MaxPool2d(2),DC2d(base,base*2))\n        self.e3=nn.Sequential(nn.MaxPool2d(2),DC2d(base*2,base*4))\n        self.bot=DC2d(base*4,base*8)\n        self.d3=DC2d(base*8+base*4,base*4)\n        self.d2=DC2d(base*4+base*2,base*2)\n        self.d1=DC2d(base*2+base,base)\n        self.head=nn.Conv2d(base,1,1)\n    def forward(self,x):\n        e1=self.e1(x);e2=self.e2(e1);e3=self.e3(e2);b=self.bot(e3)\n        d3=self.d3(torch.cat([F.interpolate(b, size=e3.shape[2:],mode='bilinear',align_corners=False),e3],1))\n        d2=self.d2(torch.cat([F.interpolate(d3,size=e2.shape[2:],mode='bilinear',align_corners=False),e2],1))\n        d1=self.d1(torch.cat([F.interpolate(d2,size=e1.shape[2:],mode='bilinear',align_corners=False),e1],1))\n        return self.head(d1)\n \n# ── ВИПРАВЛЕНИЙ Hard Belnap Combiner ──\n# Правильна truth table:\n#   A=1, B=1 → INK  (2)   — cross-domain agreement\n#   A=1, B=0 → BOTH (3)   — domain conflict\n#   A=0, B=1 → BOTH (3)   — domain conflict\n#   A=0, B=0 → VOID (1)   — agreement negative\n#   both uncertain → UNKNOWN (0)\ndef belnap_combine_hard(prob_A: np.ndarray, prob_B: np.ndarray,\n                         thr: float = 0.5) -> np.ndarray:\n    A = prob_A > thr\n    B = prob_B > thr\n    uncertain = ((prob_A > 0.3) & (prob_A < 0.7) &\n                 (prob_B > 0.3) & (prob_B < 0.7))\n    result = np.ones(prob_A.shape, dtype=np.int32)  # default: VOID\n    result[A & B]  = 2   # INK\n    result[A ^ B]  = 3   # BOTH (XOR = один каже так, інший ні)\n    result[uncertain] = 0  # UNKNOWN\n    return result\n \n# ── Soft Belnap через evidence accumulation ──\ndef belnap_combine_soft(prob_A: np.ndarray, prob_B: np.ndarray,\n                         *probs_extra) -> dict:\n    \"\"\"\n    Soft-Belnap via Dempster-Shafer evidence accumulation.\n    Не втрачає інформацію на порозі.\n \n    evidence_ink(A)  = prob_A\n    evidence_void(A) = 1 - prob_A\n \n    belief_ink  = min(ev_ink_A,  ev_ink_B)   — обидва кажуть \"є\"\n    belief_void = min(ev_void_A, ev_void_B)  — обидва кажуть \"нема\"\n    conflict    = взаємовиключні свідчення\n \n    Повертає:\n      belief_ink, belief_void, conflict, uncertainty\n    \"\"\"\n    all_probs = [prob_A, prob_B] + list(probs_extra)\n \n    # Ініціалізуємо з першої моделі\n    ev_ink  = all_probs[0].copy()\n    ev_void = 1.0 - all_probs[0]\n \n    for p in all_probs[1:]:\n        ev_ink_new  = p\n        ev_void_new = 1.0 - p\n \n        # Акумулюємо мінімальне свідчення (консервативна комбінація)\n        belief_ink   = np.minimum(ev_ink,  ev_ink_new)\n        belief_void  = np.minimum(ev_void, ev_void_new)\n        # Конфлікт: A каже є, B каже немає (або навпаки)\n        conflict     = (np.minimum(ev_ink,  ev_void_new) +\n                        np.minimum(ev_void, ev_ink_new))\n        # Невизначеність: ніхто не впевнений\n        uncertainty  = 1.0 - belief_ink - belief_void - conflict\n \n        ev_ink  = belief_ink\n        ev_void = belief_void\n \n    return {\n        'belief_ink':   ev_ink,\n        'belief_void':  ev_void,\n        'conflict':     conflict,\n        'uncertainty':  np.clip(uncertainty, 0, 1),\n    }\n \ndef soft_to_belnap(soft: dict, thr: float = 0.4) -> np.ndarray:\n    \"\"\"Перетворює soft evidence у дискретні стани Белнапа.\"\"\"\n    result = np.ones(soft['belief_ink'].shape, dtype=np.int32)  # VOID\n    result[soft['belief_ink']  > thr] = 2  # INK\n    result[soft['conflict']    > thr] = 3  # BOTH\n    result[soft['uncertainty'] > thr] = 0  # UNKNOWN\n    return result\n \n# ── Belnap Entropy ──\ndef belnap_entropy(soft: dict) -> np.ndarray:\n    \"\"\"\n    H_B = -Σ p(s) * log2(p(s))  для s ∈ {INK, BOTH, VOID, UNKNOWN}\n    Висока ентропія = нестабільна зона.\n    \"\"\"\n    total = (soft['belief_ink'] + soft['belief_void'] +\n             soft['conflict'] + soft['uncertainty'] + 1e-8)\n    p_ink  = soft['belief_ink']   / total\n    p_void = soft['belief_void']  / total\n    p_both = soft['conflict']     / total\n    p_unk  = soft['uncertainty']  / total\n \n    def xlogx(p): return -p * np.log2(np.clip(p, 1e-8, 1))\n    return xlogx(p_ink) + xlogx(p_void) + xlogx(p_both) + xlogx(p_unk)\n \n# ── SUBIT-INK Score ──\ndef subit_ink_score(belnap_map: np.ndarray, gt: np.ndarray,\n                     border: np.ndarray) -> dict:\n    \"\"\"\n    SUBIT-INK Score = Precision(INK) × ConflictLocalization(BOTH)\n \n    ConflictLocalization = частка BOTH пікселів що знаходяться\n    на реальних межах букв або у зонах артефактів.\n    Якщо BOTH хаотично розкиданий → низький score.\n    \"\"\"\n    ink_pred = (belnap_map == 2).astype(int)\n    both     = (belnap_map == 3)\n \n    prec = precision_score(gt.flatten().astype(int),\n                           ink_pred.flatten(), zero_division=0)\n    # Частка BOTH що припадає на межі букв\n    conflict_loc = (both & border).sum() / (both.sum() + 1e-8)\n \n    return {\n        'precision_ink':    float(prec),\n        'conflict_loc':     float(conflict_loc),\n        'subit_ink_score':  float(prec * conflict_loc),\n    }\n \nBELNAP_COLORS = {2:(0,200,255), 3:(255,165,0), 1:(20,20,20), 0:(128,0,128)}\nBELNAP_LABELS = {\n    2:'INK (T) — cross-domain agreement',\n    3:'BOTH (B) — domain conflict',\n    1:'VOID (F) — agreement negative',\n    0:'UNKNOWN (N) — all uncertain',\n}\n \ndef make_labels(ink_mask, dil=4):\n    ink = ink_mask.astype(bool)\n    border = binary_dilation(ink, iterations=dil) & ~ink\n    soft = ink.astype(np.float32)\n    soft[border] = 0.7\n    return soft\n \ndef train_model(model, img, labels, epochs=50, pw=5.0, bs=32, ps=64):\n    from torch.utils.data import Dataset, DataLoader, SubsetRandomSampler\n    class PatchDS(Dataset):\n        def __init__(self,img,labels,n=3000,ps=64):\n            self.img=img;self.labels=labels;self.ps=ps;self.hp=ps//2\n            H,W=img.shape;hp=self.hp;rng=np.random.default_rng(42)\n            iy,ix=np.where(labels>0.5);vy,vx=np.where(labels<0.3)\n            n_ink=int(n*0.4);n_void=n-n_ink\n            def sc(ya,xa,nn):\n                if len(ya)==0: return []\n                idx=rng.integers(0,len(ya),nn*3);res=[]\n                for i in idx:\n                    y,x=ya[i],xa[i]\n                    if hp<=y<H-hp and hp<=x<W-hp: res.append((y,x))\n                    if len(res)>=nn: break\n                return res[:nn]\n            self.coords=sc(iy,ix,n_ink)+sc(vy,vx,n_void)\n        def __len__(self): return len(self.coords)\n        def __getitem__(self,idx):\n            py,px=self.coords[idx];hp=self.hp\n            return (torch.from_numpy(self.img[py-hp:py+hp,px-hp:px+hp]).float().unsqueeze(0),\n                    torch.from_numpy(self.labels[py-hp:py+hp,px-hp:px+hp]).float().unsqueeze(0))\n \n    ds=PatchDS(img,labels,n=3000,ps=ps)\n    n=len(ds);idx=list(range(n));np.random.shuffle(idx);split=int(0.8*n)\n    tl=DataLoader(ds,bs,sampler=SubsetRandomSampler(idx[:split]),num_workers=2,pin_memory=True)\n    vl=DataLoader(ds,bs,sampler=SubsetRandomSampler(idx[split:]),num_workers=2,pin_memory=True)\n    opt=torch.optim.Adam(model.parameters(),lr=1e-3,weight_decay=1e-4)\n    sched=torch.optim.lr_scheduler.CosineAnnealingLR(opt,epochs)\n    pw_t=torch.tensor([pw]).to(DEVICE);model.to(DEVICE)\n    best_val=float('inf');best_st=None;th=[];vh=[];t0=time.time()\n    for ep in range(epochs):\n        model.train();tot=0;nb=0\n        for x,y in tl:\n            opt.zero_grad()\n            loss=nn.BCEWithLogitsLoss(pos_weight=pw_t)(model(x.to(DEVICE)),y.to(DEVICE))\n            loss.backward();opt.step();tot+=loss.item();nb+=1\n        tl_=tot/max(nb,1)\n        model.eval();tot=0;nb=0\n        with torch.no_grad():\n            for x,y in vl:\n                loss=nn.BCEWithLogitsLoss(pos_weight=pw_t)(model(x.to(DEVICE)),y.to(DEVICE))\n                tot+=loss.item();nb+=1\n        vl_=tot/max(nb,1);sched.step();th.append(tl_);vh.append(vl_)\n        if vl_<best_val:\n            best_val=vl_;best_st={k:v.cpu().clone() for k,v in model.state_dict().items()}\n        eta=(time.time()-t0)/(ep+1)*(epochs-ep-1)\n        print(f'  ep {ep+1:3d}/{epochs}  train={tl_:.4f}  val={vl_:.4f}  ETA={eta/60:.1f}хв')\n    model.load_state_dict(best_st)\n    print(f'  ✓ Best val={best_val:.4f}')\n    return th,vh\n \n@torch.no_grad()\ndef predict_full(model,img,ps=64,step=None):\n    H,W=img.shape;hp=ps//2;step=step or ps//2\n    pm=np.zeros((H,W),np.float32);cnt=np.zeros((H,W),np.float32)\n    model.eval()\n    tiles=[(py,px) for py in range(hp,H-hp,step) for px in range(hp,W-hp,step)]\n    bs=128\n    for i in range(0,len(tiles),bs):\n        bt=tiles[i:i+bs];imgs=[];valid=[]\n        for py,px in bt:\n            p=img[py-hp:py+hp,px-hp:px+hp]\n            if p.shape==(ps,ps): imgs.append(torch.from_numpy(p).float());valid.append((py,px))\n        if not imgs: continue\n        bt2=torch.stack(imgs).unsqueeze(1).to(DEVICE)\n        prob=torch.sigmoid(model(bt2)).squeeze(1).cpu().numpy()\n        for j,(py,px) in enumerate(valid):\n            pm[py-hp:py+hp,px-hp:px+hp]+=prob[j]\n            cnt[py-hp:py+hp,px-hp:px+hp]+=1\n        if (i//bs)%20==0: print(f'  {min(i+bs,len(tiles))}/{len(tiles)} ({100*min(i+bs,len(tiles))/len(tiles):.0f}%)')\n    return pm/np.maximum(cnt,1)\n \ntest=InkNet(32).to(DEVICE)\nx=torch.zeros(4,1,64,64).to(DEVICE)\nprint(f'✓ InkNet: {tuple(test(x).shape)}  params={sum(p.numel() for p in test.parameters()):,}')\ndel test,x; torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:52:57.012067Z","iopub.execute_input":"2026-06-29T23:52:57.012385Z","iopub.status.idle":"2026-06-29T23:52:58.064193Z","shell.execute_reply.started":"2026-06-29T23:52:57.012362Z","shell.execute_reply":"2026-06-29T23:52:58.063501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Виправляємо crop — беремо центр тексту правильно\nfrom PIL import Image\n\nH_ink, W_ink = ink_full.shape\nprint(f'ink_full: {H_ink}×{W_ink}')\n\n# Центр тексту в координатах ink\ncy_ink_real = int(iy.mean())\ncx_ink_real = int(ix.mean())\nprint(f'Центр тексту (ink): y={cy_ink_real}, x={cx_ink_real}')\n\n# Crop у координатах ink (не tif!)\nCROP_INK = 1500  # розмір crop в пікселях ink\nhc = CROP_INK // 2\ny1_new = max(0, min(cy_ink_real - hc, H_ink - CROP_INK))\ny2_new = y1_new + CROP_INK\nx1_new = max(0, min(cx_ink_real - hc, W_ink - CROP_INK))\nx2_new = x1_new + CROP_INK\nprint(f'Новий crop (ink): y={y1_new}:{y2_new}, x={x1_new}:{x2_new}')\n\n# Відповідні координати в tif\ny1_tif = int(y1_new * scale_y); y2_tif = int(y2_new * scale_y)\nx1_tif = int(x1_new * scale_x); x2_tif = int(x2_new * scale_x)\nprint(f'Новий crop (tif): y={y1_tif}:{y2_tif}, x={x1_tif}:{x2_tif}')\n\n# Нова ink маска\nink_new = ink_full[y1_new:y2_new, x1_new:x2_new]\nprint(f'Ink у новому crop: {ink_new.mean()*100:.1f}%')\n\n# Завантажуємо новий volume\nprint(f'\\nЗавантажуємо шари з правильним crop...')\nlayers_new = []\nfor i, f in enumerate(tif_files[:LAYERS]):\n    raw = tifffile.imread(str(f))\n    if raw.ndim == 3: raw = raw[0]\n    layers_new.append(raw[y1_tif:y2_tif, x1_tif:x2_tif].astype(np.float32))\n    if (i+1) % 10 == 0: print(f'  {i+1}/{LAYERS}')\n\nvolume_new = np.stack(layers_new)\nv_min, v_max = float(volume_new.min()), float(volume_new.max())\nvolume_new = (volume_new - v_min) / (v_max - v_min + 1e-8)\n\n# Resize ink під розмір volume\nink = np.array(Image.fromarray(ink_new).resize(\n    (volume_new.shape[2], volume_new.shape[1]), Image.NEAREST))\nvolume = volume_new\n\n# Три домени\nmid = volume.shape[0] // 2; half = DEPTH // 2; shift = 8\nvol_A = volume[mid-half : mid+half+1].mean(axis=0).astype(np.float32)\nvol_B = volume[max(0,mid-half-shift) : max(DEPTH,mid+half+1-shift)].mean(axis=0).astype(np.float32)\nvol_C = volume[min(volume.shape[0]-DEPTH,mid-half+shift) : mid+half+1+shift].mean(axis=0).astype(np.float32)\n\nprint(f'\\nVolume: {volume.shape}')\nprint(f'Ink: {ink.mean()*100:.1f}%')\n\n# Перевірка\nimport matplotlib.pyplot as plt\nfig, axes = plt.subplots(1, 3, figsize=(15, 5))\nfig.patch.set_facecolor('#111')\nfor ax in axes: ax.set_facecolor('#111'); ax.axis('off')\naxes[0].imshow(vol_A, cmap='bone'); axes[0].set_title('Domain A', color='white')\naxes[1].imshow(ink,   cmap='gray'); axes[1].set_title(f'Ink GT ({ink.mean()*100:.1f}%)', color='white')\naxes[2].imshow(np.abs(vol_A - vol_C), cmap='hot'); axes[2].set_title('|A-C| conflict', color='white')\nplt.tight_layout(); plt.show()\nprint('✓ Новий crop готовий — тепер запускай клітинки 4→5→6→7')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:52:58.065354Z","iopub.execute_input":"2026-06-29T23:52:58.065564Z","iopub.status.idle":"2026-06-29T23:53:01.572114Z","shell.execute_reply.started":"2026-06-29T23:52:58.065543Z","shell.execute_reply":"2026-06-29T23:53:01.571140Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 4 — Тренування трьох незалежних детекторів\n# ════════════════════════════════════════════════════════════════\ntorch.cuda.empty_cache()\nratio=(1-ink).sum()/(ink.sum()+1e-8)\nlabels=make_labels(ink,dil=4)\n \nprint('='*55+'\\nModel A (central layers)\\n'+'='*55)\nmodel_A=InkNet(32); hA_t,hA_v=train_model(model_A,vol_A,labels,50,ratio)\n \nprint('\\n'+'='*55+'\\nModel B (upper layers)\\n'+'='*55)\nmodel_B=InkNet(32); hB_t,hB_v=train_model(model_B,vol_B,labels,50,ratio)\n \nprint('\\n'+'='*55+'\\nModel C (lower layers)\\n'+'='*55)\nmodel_C=InkNet(32); hC_t,hC_v=train_model(model_C,vol_C,labels,50,ratio)\n \ntorch.save(model_A.state_dict(),'/kaggle/working/model_A.pth')\ntorch.save(model_B.state_dict(),'/kaggle/working/model_B.pth')\ntorch.save(model_C.state_dict(),'/kaggle/working/model_C.pth')\nprint('\\n✓ Три моделі збережено')\n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-29T23:53:01.574332Z","iopub.execute_input":"2026-06-29T23:53:01.574596Z","iopub.status.idle":"2026-06-30T00:02:06.364281Z","shell.execute_reply.started":"2026-06-29T23:53:01.574571Z","shell.execute_reply":"2026-06-30T00:02:06.363464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 5 — Inference + Hard і Soft Belnap\n# ════════════════════════════════════════════════════════════════\nprint('Inference A...'); prob_A=predict_full(model_A,vol_A,64,32)\nprint('Inference B...'); prob_B=predict_full(model_B,vol_B,64,32)\nprint('Inference C...'); prob_C=predict_full(model_C,vol_C,64,32)\n \nnp.save('/kaggle/working/prob_A.npy',prob_A)\nnp.save('/kaggle/working/prob_B.npy',prob_B)\nnp.save('/kaggle/working/prob_C.npy',prob_C)\n \n# Hard Belnap (2 домени: A vs B)\nhard_AB = belnap_combine_hard(prob_A, prob_B, thr=0.5)\n \n# Hard Belnap (3 домени: majority vote + conflict)\ndef belnap_combine_3(pA, pB, pC, thr=0.5):\n    A=(pA>thr); B=(pB>thr); C=(pC>thr)\n    votes = A.astype(int)+B.astype(int)+C.astype(int)\n    uncertain = ((pA>0.3)&(pA<0.7)&(pB>0.3)&(pB<0.7)&(pC>0.3)&(pC<0.7))\n    result = np.ones(pA.shape, dtype=np.int32)     # VOID\n    result[votes == 3] = 2                          # INK: всі три згодні\n    result[(votes == 2) | (votes == 1)] = 3        # BOTH: є розбіжність\n    result[votes == 0] = 1                          # VOID: всі кажуть немає\n    result[uncertain]  = 0                          # UNKNOWN\n    return result\n \nhard_3 = belnap_combine_3(prob_A, prob_B, prob_C, thr=0.5)\n \n# Soft Belnap (3 домени)\nsoft = belnap_combine_soft(prob_A, prob_B, prob_C)\nsoft_map = soft_to_belnap(soft, thr=0.4)\n \n# Entropy\nentropy_map = belnap_entropy(soft)\n \n# Standard ensemble baseline\nensemble = ((prob_A + prob_B + prob_C) / 3 > 0.5).astype(np.int32)\n \nnp.save('/kaggle/working/hard_AB.npy',   hard_AB)\nnp.save('/kaggle/working/hard_3.npy',    hard_3)\nnp.save('/kaggle/working/soft_map.npy',  soft_map)\nnp.save('/kaggle/working/entropy.npy',   entropy_map)\nprint('✓ Всі карти збережено')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-30T00:02:06.365894Z","iopub.execute_input":"2026-06-30T00:02:06.366519Z","iopub.status.idle":"2026-06-30T00:02:07.057635Z","shell.execute_reply.started":"2026-06-30T00:02:06.366489Z","shell.execute_reply":"2026-06-30T00:02:07.056844Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 6 — Метрики: порівняння всіх підходів\n# ════════════════════════════════════════════════════════════════\nfrom sklearn.metrics import f1_score, precision_score, recall_score, fbeta_score\nfrom scipy.ndimage import binary_dilation\n\ngt     = ink.astype(int).flatten()\nborder = binary_dilation(ink.astype(bool), iterations=4) & ~ink.astype(bool)\n\ndef eval_belnap(bmap, name):\n    ink_pred = (bmap == 2).astype(int)\n    relaxed  = ((bmap == 2) | (bmap == 3)).astype(int)\n    f1   = f1_score(gt,   ink_pred.flatten(), zero_division=0)\n    f05  = fbeta_score(gt, ink_pred.flatten(), beta=0.5, zero_division=0)\n    prec = precision_score(gt, ink_pred.flatten(), zero_division=0)\n    rec  = recall_score(gt,    ink_pred.flatten(), zero_division=0)\n    f1r  = f1_score(gt, relaxed.flatten(), zero_division=0)\n    total = bmap.size\n    sc   = subit_ink_score(bmap, ink, border)\n    print(f'  {name}:')\n    print(f'    F0.5={f05:.4f}  F1={f1:.4f}  prec={prec:.4f}  rec={rec:.4f}')\n    print(f'    F1(INK+BOTH)={f1r:.4f}')\n    print(f'    INK={100*(bmap==2).sum()/total:.1f}%  '\n          f'BOTH={100*(bmap==3).sum()/total:.1f}%  '\n          f'VOID={100*(bmap==1).sum()/total:.1f}%  '\n          f'UNK={100*(bmap==0).sum()/total:.1f}%')\n    print(f'    SUBIT-INK score={sc[\"subit_ink_score\"]:.4f}  '\n          f'(prec={sc[\"precision_ink\"]:.3f} × loc={sc[\"conflict_loc\"]:.3f})')\n    return f05\n\n# Single models\nf1_A   = f1_score(gt, (prob_A > 0.5).astype(int).flatten(), zero_division=0)\nf1_B   = f1_score(gt, (prob_B > 0.5).astype(int).flatten(), zero_division=0)\nf1_C   = f1_score(gt, (prob_C > 0.5).astype(int).flatten(), zero_division=0)\nf05_ens = fbeta_score(gt, ensemble.flatten(), beta=0.5, zero_division=0)\n\nprint(f'\\n{\"=\"*60}')\nprint(f'  ПОРІВНЯННЯ МЕТОДІВ (F0.5 = Vesuvius метрика)')\nprint(f'{\"=\"*60}')\nprint(f'  Single model A:  F1={f1_A:.4f}')\nprint(f'  Single model B:  F1={f1_B:.4f}')\nprint(f'  Single model C:  F1={f1_C:.4f}')\nprint(f'  Mean ensemble:   F0.5={f05_ens:.4f}')\nprint()\n\nf05_AB = eval_belnap(hard_AB,  'Hard Belnap (A vs B)')\nprint()\nf05_3  = eval_belnap(hard_3,   'Hard Belnap (3 domains)')\nprint()\nf05_s  = eval_belnap(soft_map, 'Soft Belnap (3 domains)')\n\nprint(f'\\n{\"=\"*60}')\nprint(f'  Entropy: mean={entropy_map.mean():.3f}  max={entropy_map.max():.3f}')\nprint(f'  High entropy zones (>1.5 bits): {(entropy_map > 1.5).mean()*100:.1f}%')\nprint(f'{\"=\"*60}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-30T00:02:07.059093Z","iopub.execute_input":"2026-06-30T00:02:07.059490Z","iopub.status.idle":"2026-06-30T00:02:09.348602Z","shell.execute_reply.started":"2026-06-30T00:02:07.059460Z","shell.execute_reply":"2026-06-30T00:02:09.347756Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 7 — Візуалізація\n# ════════════════════════════════════════════════════════════════\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\n \niy2,ix2=np.where(ink>0); cy2,cx2=int(iy2.mean()),int(ix2.mean())\nSZ=512; sy1=max(0,min(cy2-SZ//2,ink.shape[0]-SZ)); sx1=max(0,min(cx2-SZ//2,ink.shape[1]-SZ))\ndef crop(a): return a[sy1:sy1+SZ,sx1:sx1+SZ]\n \ntA=crop(vol_A); tB=crop(vol_B); tC=crop(vol_C); ink_c=crop(ink)\npAc=crop(prob_A); pBc=crop(prob_B); pCc=crop(prob_C)\nhAB_c=crop(hard_AB); h3_c=crop(hard_3); sm_c=crop(soft_map)\nens_c=crop(ensemble); ent_c=crop(entropy_map)\n \ndef to_rgb(bmap):\n    rgb=np.zeros((SZ,SZ,3),dtype=np.uint8)\n    for s,c in BELNAP_COLORS.items(): rgb[bmap==s]=c\n    return rgb\n \nfig,axes=plt.subplots(3,4,figsize=(22,17))\nfig.patch.set_facecolor('#080808')\nfor ax in axes.flat:\n    ax.set_facecolor('#0d0d0d'); [sp.set_edgecolor('#2a2a2a') for sp in ax.spines.values()]\ndef show(ax,img,t,cmap='gray',vmin=None,vmax=None):\n    ax.imshow(img,cmap=cmap,interpolation='nearest',vmin=vmin,vmax=vmax)\n    ax.set_title(t,color='#ddd',fontsize=8,pad=4,fontweight='bold'); ax.axis('off')\n \n# Рядок 1: домени + prob maps\nshow(axes[0,0],tA,'Domain A (central)','bone')\nshow(axes[0,1],tB,'Domain B (upper)','bone')\nshow(axes[0,2],tC,'Domain C (lower)','bone')\nshow(axes[0,3],ink_c,'Ground Truth','gray')\n \n# Рядок 2: Belnap maps\nshow(axes[1,0],ens_c,'Mean Ensemble\\n(baseline)','gray')\naxes[1,1].imshow(to_rgb(hAB_c),interpolation='nearest')\naxes[1,1].set_title('Hard Belnap (A vs B)\\n[ВИПРАВЛЕНА truth table]',color='#ddd',fontsize=8,pad=4,fontweight='bold')\naxes[1,1].axis('off')\naxes[1,2].imshow(to_rgb(h3_c),interpolation='nearest')\naxes[1,2].set_title('Hard Belnap (3 domains)\\nmajority vote',color='#ddd',fontsize=8,pad=4,fontweight='bold')\naxes[1,2].axis('off')\naxes[1,3].imshow(to_rgb(sm_c),interpolation='nearest')\naxes[1,3].set_title('Soft Belnap (3 domains)\\nevidence accumulation',color='#ddd',fontsize=8,pad=4,fontweight='bold')\naxes[1,3].axis('off')\npatches=[mpatches.Patch(color=np.array(c)/255,label=BELNAP_LABELS[s])\n         for s,c in BELNAP_COLORS.items()]\naxes[1,3].legend(handles=patches,loc='lower right',fontsize=6,\n                 facecolor='#1a1a1a',edgecolor='#555',labelcolor='white')\n \n# Рядок 3: аналіз\nshow(axes[2,0],(soft['belief_ink'] [:SZ,:SZ] if sm_c.shape==(SZ,SZ) else\n                crop(soft['belief_ink'])),'Soft belief_ink\\n(evidence accumulation)','Blues',0,1)\nshow(axes[2,1],(crop(soft['conflict'])),'Soft conflict map\\n(domain disagreement)','hot',0,1)\nshow(axes[2,2],ent_c,'Belnap Entropy H_B\\n(нестабільні зони)','plasma',0,2)\n \nax=axes[2,3]; ax.axis('off')\nsc3=subit_ink_score(h3_c,ink_c,crop(border.astype(float)).astype(bool))\nsc_s=subit_ink_score(sm_c,ink_c,crop(border.astype(float)).astype(bool))\ntxt=(f'  SUBIT-BELNAP v2\\n  {\"─\"*30}\\n'\n     f'  Метрика Vesuvius: F0.5\\n  (precision > recall)\\n\\n'\n     f'  Mean ensemble:  F0.5={f05_ens:.4f}\\n'\n     f'  Hard Belnap AB: F0.5={f05_AB:.4f}\\n'\n     f'  Hard Belnap 3:  F0.5={f05_3:.4f}\\n'\n     f'  Soft Belnap 3:  F0.5={f05_s:.4f}\\n\\n'\n     f'  SUBIT-INK score:\\n'\n     f'   Hard 3: {sc3[\"subit_ink_score\"]:.4f}\\n'\n     f'   Soft 3: {sc_s[\"subit_ink_score\"]:.4f}\\n\\n'\n     f'  Entropy: mean={entropy_map.mean():.3f}\\n'\n     f'  H>1.5bit: {(entropy_map>1.5).mean()*100:.1f}% pixels')\nax.text(0.03,0.97,txt,transform=ax.transAxes,color='#e0e0e0',\n        fontsize=8.5,va='top',fontfamily='monospace',\n        bbox=dict(boxstyle='round,pad=0.8',facecolor='#151515',edgecolor='#00c8ff',lw=1.5))\n \nplt.suptitle('SUBIT-BELNAP COMBINER v2 — Hard + Soft + 3-Domain + Entropy × Vesuvius Challenge',\n             color='white',fontsize=11,fontweight='bold',y=0.998)\nplt.tight_layout(rect=[0,0,1,0.985])\nplt.savefig('/kaggle/working/subit_belnap_v2.png',dpi=150,bbox_inches='tight',facecolor='#080808')\nplt.show()\nprint('✓ Збережено: subit_belnap_v2.png')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-30T00:02:09.349592Z","iopub.execute_input":"2026-06-30T00:02:09.349921Z","iopub.status.idle":"2026-06-30T00:02:13.066768Z","shell.execute_reply.started":"2026-06-30T00:02:09.349898Z","shell.execute_reply":"2026-06-30T00:02:13.065576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ════════════════════════════════════════════════════════════════\n# КЛІТИНКА 8 — Збереження і summary\n# ════════════════════════════════════════════════════════════════\nimport json\n \nresults = {\n    'f05_ensemble':  float(f05_ens),\n    'f05_hard_AB':   float(f05_AB),\n    'f05_hard_3':    float(f05_3),\n    'f05_soft_3':    float(f05_s),\n    'subit_score_hard3': float(sc3['subit_ink_score']),\n    'subit_score_soft3': float(sc_s['subit_ink_score']),\n    'entropy_mean':  float(entropy_map.mean()),\n    'entropy_high_pct': float((entropy_map>1.5).mean()*100),\n}\nwith open('/kaggle/working/results_v2.json','w') as f:\n    json.dump(results, f, indent=2)\n \nprint('\\nФайли у /kaggle/working/:')\nfor ff in sorted(os.listdir('/kaggle/working')):\n    size=os.path.getsize(f'/kaggle/working/{ff}')/1e6\n    print(f'  {ff}  ({size:.1f} MB)')\nprint('\\n✓ Готово! Output → Download')\n ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-30T00:02:13.068116Z","iopub.execute_input":"2026-06-30T00:02:13.068443Z","iopub.status.idle":"2026-06-30T00:02:13.078343Z","shell.execute_reply.started":"2026-06-30T00:02:13.068405Z","shell.execute_reply":"2026-06-30T00:02:13.077354Z"}},"outputs":[],"execution_count":null}]}