{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"0e385774-7774-4140-a09e-4055639e44bf","cell_type":"markdown","source":"# RSNA Knee MRI: evidence routing with full-data refit\n\nThis implementation uses multilingual report vocabulary as domain data, but the report\nevidence engine, MRI representation, pooling, and training schedule are newly written.\nA pilot split chooses the epoch count; final models are then refit on every study.\n\n","metadata":{}},{"id":"77c98ea4-fc43-4d86-b7a0-edd9a7f92569","cell_type":"code","source":"from __future__ import annotations\n\nimport gc\nimport hashlib\nimport math\nimport os\nimport random\nimport re\nimport time\nimport traceback\nimport unicodedata\nfrom concurrent.futures import ThreadPoolExecutor\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom sklearn.metrics import roc_auc_score\n\nSEED = 2026\nT0 = time.time()\nTARGETS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\",\n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\",\n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\",\n]\n\nCROP_MM = 130.0\nCACHE_IMAGE = 168\nGROUP_SIZE = 3\nMAX_GROUPS = 8\nN_SLOTS = 6\nCACHE_BUDGET_GB = 17.0\nRESERVE_GB = 7.0\nHEADER_THREADS = 16\nORDER_THREADS = 32\nPIXEL_THREADS = 12\nPILOT_EPOCHS = 14\nTRAIN_GROUPS = 2\nTIME_LIMIT = 8.3 * 3600\nSMOKE = bool(int(os.environ.get(\"SMOKE\", \"0\")))\n\nSLOTS = [\n    (\"SAG_FS\", \"Sagittal\", True, True),\n    (\"COR_FS\", \"Coronal\", True, True),\n    (\"AX_FS\", \"Axial\", True, True),\n    (\"SAG_NFS\", \"Sagittal\", True, False),\n    (\"COR_T1\", \"Coronal\", False, False),\n    (\"SAG_T1\", \"Sagittal\", False, False),\n]\n\nFINAL_MODELS = [\n    {\"name\": \"small_all_a\", \"variant\": \"small\", \"groups\": 8, \"seed\": 2026, \"batch\": 6, \"lr\": 8e-6},\n    {\"name\": \"small_all_b\", \"variant\": \"small\", \"groups\": 4, \"seed\": 73, \"batch\": 6, \"lr\": 7e-6},\n    {\"name\": \"base_all\", \"variant\": \"base\", \"groups\": 8, \"seed\": 19, \"batch\": 4, \"lr\": 5e-6},\n]\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.backends.cudnn.benchmark = True\n\n\ndef log(message):\n    print(f\"[{time.time() - T0:7.1f}s] {message}\", flush=True)\n\n\ndef find_root():\n    candidates = [\n        Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\"),\n        Path(\"/kaggle/input/rsna-knee-abnormality-detection\"), Path(\"data\"), Path(\".\"),\n    ]\n    for candidate in candidates:\n        if (candidate / \"train.csv\").is_file() and (candidate / \"test_series\").is_dir():\n            return candidate\n    for test_csv in Path(\"/kaggle/input\").rglob(\"test.csv\"):\n        if (test_csv.parent / \"test_series\").is_dir():\n            return test_csv.parent\n    raise FileNotFoundError(\"competition data not found\")\n\n\nROOT = find_root()\n\n\ndef save_submission(frame, name=\"submission.csv\"):\n    output = frame[[\"StudyInstanceUID\"] + TARGETS].copy()\n    output.to_csv(name, index=False)\n    working = Path(\"/kaggle/working\")\n    if working.is_dir():\n        output.to_csv(working / name, index=False)\n    return output\n\n\nTEST_FRAME = pd.read_csv(ROOT / \"test.csv\")\n_fallback = TEST_FRAME[[\"StudyInstanceUID\"]].copy()\nfor _target in TARGETS:\n    _fallback[_target] = 0.5\nsave_submission(_fallback)\nlog(f\"fallback submission.csv written; test={len(TEST_FRAME)}\")\n\n\ndef _rx(*patterns):\n    return re.compile(\"|\".join(patterns))\n\n\n# Vocabulary patterns adapted from the supplied 0.847 reference notebook.\n\n# They are data for the independently implemented EvidenceLedger below.\n\n_PRE = str.maketrans({\n    \"ı\": \"i\", \"İ\": \"i\", \"I\": \"i\", \"ß\": \"ss\", \"đ\": \"d\", \"Đ\": \"d\",\n    \"ø\": \"o\", \"Ø\": \"o\", \"æ\": \"ae\", \"Æ\": \"ae\",\n})\n\n_SENT_SPLIT = re.compile(r\"(?<=[.;!?])\\s+|\\n+\")\n\nPRE_NEG = _rx(\n    r\"\\bno\\b\", r\"\\bnot\\b\", r\"\\bwithout\\b\", r\"\\bnegative for\\b\", r\"\\babsence\\b\",\n    r\"\\bno evidence\\b\", r\"\\bfree of\\b\", r\"\\bnone\\b\", r\"\\bneither\\b\", r\"\\bnor\\b\",\n    r\"\\bsin\\b\", r\"\\bno hay\\b\", r\"\\bausencia\\b\", r\"\\bausentes?\\b\", r\"\\bno se\\b\",\n    r\"\\bpas de\\b\", r\"\\bsans\\b\", r\"\\baucune?\\b\",\n    r\"\\bgeen\\b\", r\"\\bzonder\\b\", r\"\\bniet\\b\",\n    r\"\\bkeine?[nmrs]?\\b\", r\"\\bohne\\b\", r\"\\bnicht\\b\", r\"\\bkein\\b\",\n    r\"\\bnema\\b\", r\"\\bbez\\b\", r\"\\bnisu\\b\", r\"\\bnije\\b\",\n    r\"\\bδεν\\b\", r\"\\bχωρις\\b\", r\"ουδεν\", r\"\\bουτε\\b\",\n    r\"\\bбез\\b\", r\"\\bне\\b\", r\"липсва\", r\"\\bняма\\b\",\n)\n\nPOST_NEG = _rx(\n    r\"\\byok\\b\", r\"\\byoktur\\b\", r\"izlenmemekte\", r\"saptanmadi\", r\"\\bdegil\\b\",\n    r\"gozlenmemekte\", r\"mevcut degil\", r\"eslik etmiyor\", r\"\\bizlenmedi\\b\",\n    r\"izlenmemistir\", r\"saptanmamistir\", r\"gorulmemistir\", r\"\\bnema znakova\\b\",\n    r\"bez znakova\",\n)\n\nNEGATION = _rx(PRE_NEG.pattern, POST_NEG.pattern, r\"\\bunremarkable\\b\")\n\nNEG_WINDOW = 90\n\nNORMALITY = _rx(\n    r\"\\bnormal\", r\"\\bintact\\b\", r\"\\bpreserved\\b\", r\"\\bwithin normal limits\\b\",\n    r\"limites normales\", r\"\\bconservad\", r\"\\bintegr\", r\"\\bnormales\\b\",\n    r\"\\bdoga(l|ll)\\b\", r\"korunmus\", r\"\\bnormaldir\\b\", r\"olagan\",\n    r\"\\buredn\", r\"\\bocuvan\", r\"\\bodrzan\", r\"\\bintakt\", r\"\\bprimjeren\",\n    r\"\\bodrzanog kontinuiteta\", r\"\\bodržan\",\n    r\"φυσιολογικ\", r\"ακεραι\", r\"δεν παρατηρουνται\", r\"δεν σημειωνονται\",\n    r\"unauffallig\", r\"regelrecht\", r\"\\bo\\.?b\\.?\\b\",\n    r\"нормал\", r\"запазен\", r\"съхранен\", r\"\\bбез особености\\b\", r\"интактн\",\n    r\"\\bgaaf\\b\", r\"\\bnormaal\\b\",\n)\n\nNORMAL_PHRASE = _rx(\n    r\"\\bsin alteracion\", r\"\\bsin cambios\\b\", r\"\\bsin particularidad\",\n    r\"\\bsin hallazgos\\b\", r\"\\bsin lesion\", r\"\\bsin signos de (rotura|lesion)\",\n    r\"\\bcontinu[oa]s?\\b\", r\"\\bcontinuidad conservada\\b\",\n    r\"\\bno abnormalit\", r\"\\bno significant abnormalit\", r\"\\bunremarkable\\b\",\n    r\"\\bno evidence of (tear|injury|abnormalit)\",\n    r\"\\bohne auffalligkeit\", r\"\\bkein nachweis\\b\", r\"\\bohne befund\\b\",\n    r\"\\bgeen afwijking\", r\"\\bzonder afwijking\",\n    r\"\\bsans anomalie\", r\"\\bpas d[e']anomalie\",\n    r\"\\bbez osobitosti\\b\", r\"\\bbez znakova (rupture|lezije)\\b\",\n    r\"\\bbez patoloskih\\b\",\n    r\"χωρις αλλοιωσ\", r\"χωρις παθολογ\", r\"δεν παρατηρουνται (αξιολογα|παθολογ)\",\n    r\"\\bбез особености\\b\", r\"\\bбез патологич\", r\"\\bбез данни за\\b\",\n    r\"\\bozel bir ozellik yok\", r\"\\bpatolojik bulgu (yok|izlenmemis)\",\n)\n\nUNCERTAIN = _rx(\n    r\"\\bpossible\\b\", r\"\\bprobable\\b\", r\"\\bsuspicious\\b\", r\"\\bsuspected?\\b\",\n    r\"cannot (be )?exclude\", r\"\\bmay\\b\", r\"\\bquestionable\\b\", r\"\\bequivocal\\b\",\n    r\"\\br/o\\b\", r\"\\bdd\\b\", r\"\\blikely\\b\", r\"\\bsuggest\", r\"\\bcompatible with\\b\",\n    r\"\\bposible\\b\", r\"sin criterios categoricos\", r\"\\bdudos\", r\"\\bsugier\",\n    r\"\\bmuhtemel\\b\", r\"\\bolasi\\b\", r\"\\bsupheli\\b\", r\"\\bizlenim\", r\"\\bdusundur\",\n    r\"\\bmoguce\\b\", r\"\\bvjerojatno\\b\", r\"\\bsumnja\\b\", r\"\\bmoze odgovarati\\b\",\n    r\"πιθαν\", r\"υποπτ\",\n    r\"\\bmoglich\", r\"\\bverdachtig\", r\"\\bfraglich\", r\"\\bv\\.?a\\.?\\b\", r\"\\bwohl\\b\",\n    r\"\\bвъзможно\\b\", r\"\\bвероятно\\b\", r\"суспект\",\n    r\"\\bmogelijk\\b\", r\"\\bverdacht\\b\",\n)\n\nTEAR = _rx(\n    r\"\\btear\", r\"\\btorn\\b\", r\"\\brupture\", r\"\\bdisruption\\b\", r\"discontinuit\",\n    r\"\\bavuls\", r\"\\bmacerat\", r\"\\bbuckethandle\\b\", r\"bucket handle\",\n    r\"\\brotura\\b\", r\"\\broturas\\b\", r\"\\bruptura\", r\"\\bdesgarro\", r\"\\broto\\b\",\n    r\"\\bdechirure\", r\"\\bdechire\",\n    r\"\\bscheur\", r\"\\bruptuur\", r\"gescheurd\",\n    r\"\\briss\\b\", r\"einriss\", r\"\\bruptur\", r\"zerreiss\", r\"\\blasion\", r\"\\bausriss\",\n    r\"\\byirtik\", r\"\\byirtig\", r\"\\bkopma\\b\", r\"butunluk kaybi\", r\"\\brupturu\\b\",\n    r\"devamsizlik\", r\"\\brupture\\b\", r\"\\bdevamliligi secilememis\",\n    r\"\\bpuknuce\", r\"\\bprekid\\b\", r\"\\bpukotin\", r\"\\bruptur\",\n    r\"ρηξη\", r\"ρηξις\", r\"ρηγμα\", r\"ασυνεχεια\",\n    r\"руптура\", r\"разкъсв\", r\"разрив\", r\"скъсв\", r\"\\bлезия\\b\",\n)\n\nDEGEN = _rx(\n    r\"degenerat\", r\"\\bmucoid\\b\", r\"\\bmyxoid\\b\", r\"\\bfray\", r\"\\bfissur\",\n    r\"dejeneratif\", r\"\\bmukoid\\b\", r\"degenerativn\", r\"εκφυλ\", r\"дегенерат\",\n    r\"\\bμυξοειδ\", r\"\\bμυξωδ\", r\"\\bmeniskopat\", r\"\\bmeniscopath\",\n    r\"\\bmuco ?ide\\b\", r\"aufgefasert\", r\"\\bdejenerasyon\\b\",\n)\n\nINJURY = _rx(\n    r\"\\binjur\", r\"\\bsprain\", r\"\\blesion\", r\"\\blasion\", r\"\\bedema\\b\", r\"\\boedema\\b\",\n    r\"\\bodem\\b\", r\"\\bedem\\b\", r\"\\bοιδημα\", r\"\\bодем\", r\"\\bедем\", r\"\\bstrain\\b\",\n    r\"\\bhigh signal\\b\", r\"\\bsignal alteration\\b\", r\"\\bhiperintens\", r\"\\bhyperintens\",\n    r\"aumento de senal\", r\"alteracion de senal\", r\"cambio de senal\",\n    r\"\\bsignalanhebung\", r\"\\bsignalalteration\", r\"verhoogd signaal\", r\"sinyal artis\",\n    r\"αυξημενο σημα\", r\"повишен сигнал\", r\"\\besguince\\b\",\n    r\"\\bthicken\", r\"\\bzadebljanje\\b\", r\"\\bverdikking\\b\", r\"\\bdistenzij\",\n    r\"\\blaksite\\b\", r\"\\blaxity\\b\", r\"\\bpartial\\b\", r\"\\bparcijaln\", r\"\\bparcial\",\n    r\"\\bpartiel\", r\"\\bpartiell\",\n)\n\n_GRADE_RX = re.compile(\n    r\"(?:grade|grad|grado|grau|derece|stupnja|stupanj|βαθμ|степен|icrs|outerbridge)\"\n    r\"[\\s:]*(?:grade\\s*)?([1-4]|iv|iii|ii|i)\\b\"\n)\n\n_ROMAN = {\"i\": 1, \"ii\": 2, \"iii\": 3, \"iv\": 4}\n\nANAT = {\n    \"ACL\": _rx(\n        r\"anterior cruciate\", r\"\\bacl\\b\",\n        r\"cruzado anterior\", r\"\\blca\\b\",\n        r\"croise anterieur\",\n        r\"voorste kruisband\", r\"\\bvkb\\b\",\n        r\"vorderes kreuzband\", r\"vorderen kreuzband\", r\"vordere kreuzband\",\n        r\"on capraz\", r\"\\bocb\\b\", r\"anterior capraz\",\n        r\"prednji krizni\", r\"prednjeg krizn\",\n        r\"προσθι[οα][^ ]* χιαστ\", r\"προσθιου χιαστου\", r\"χιαστο[^ ]* συνδεσμ\",\n        r\"\\bχιαστ\\w*\",\n        r\"предна кръстна\", r\"предната кръстна\", r\"предна кръста\",\n        r\"cruciate ligaments\", r\"ligamentos cruzados\", r\"ligaments croises\",\n        r\"kruisbanden\", r\"kreuzbander\", r\"capraz baglar\", r\"krizn[a-z]* ligament[a-z]*\",\n        r\"χιαστοι συνδεσμ\", r\"χιαστων συνδεσμ\", r\"кръстните връзки\", r\"кръстни връзки\",\n    ),\n    \"MCL\": _rx(\n        r\"medial collateral\", r\"\\bmcl\\b\", r\"tibial collateral\",\n        r\"colateral medial\", r\"colateral interno\", r\"\\blcm\\b\",\n        r\"collateral medial\", r\"collateral interne\",\n        r\"mediale collaterale\", r\"binnenband\", r\"\\b(mediale|laterale) banden\\b\",\n        r\"\\bcollaterale banden\\b\",\n        r\"innenband\", r\"mediales? kollateral\",\n        r\"\\bic yan bag\", r\"medial kollateral\", r\"\\biyb\\b\", r\"medyal kollateral\",\n        r\"medijalni kolateraln\", r\"medijalnog kolateraln\",\n        r\"εσω πλαγι\", r\"εσωτερικο πλαγι\", r\"\\bπλαγι\\w* συνδεσμ\", r\"\\bπλαγιοι\\b\",\n        r\"медиален колатерал\", r\"вътрешна странична\", r\"\\bколатерал\\w*\",\n        r\"\\bcolaterales\\b\", r\"\\bcollateraux\\b\", r\"\\bcollateralen\\b\", r\"\\bkolateralni\\b\",\n        r\"collateral ligaments\", r\"ligamentos colaterales\", r\"ligaments collateraux\",\n        r\"collaterale banden\", r\"kollateralbander\", r\"seitenbander\", r\"yan baglar\",\n        r\"kolateraln[a-z]* ligament[a-z]*\", r\"πλαγιοι συνδεσμ\", r\"πλαγιων συνδεσμ\",\n        r\"колатерални връзки\", r\"страничните връзки\",\n    ),\n    \"Medial Meniscus\": _rx(\n        r\"medial meniscus\", r\"\\bmm\\b(?= tear)\", r\"medial menisc\",\n        r\"menisco medial\", r\"menisco interno\",\n        r\"menisque medial\", r\"menisque interne\",\n        r\"mediale meniscus\", r\"binnenmeniscus\",\n        r\"innenmeniskus\", r\"medialen? meniskus\", r\"innenmeniskushinterhorn\",\n        r\"medyal menisk\", r\"\\bic menisk\",\n        r\"medijalni meniskus\", r\"medijalnog meniskusa\", r\"medijalnom meniskusu\",\n        r\"medijaln\\w* menisk\\w*\", r\"\\bmedijalnog meniska\\b\", r\"medijalni menisk\",\n        r\"εσω μηνισκ\", r\"μηνισκ[^ ]* του εσω\", r\"εσω διαμερισμα[^.]{0,40}μηνισκ\",\n        r\"медиалния менискус\", r\"медиален менискус\", r\"вътрешния менискус\",\n        r\"oba meniska\", r\"both menisci\", r\"ambos meniscos\", r\"beide menisci\",\n        r\"her iki menisku\", r\"amfoteroi\\w* mhnisk\", r\"αμφοτερ\\w* μηνισκ\",\n        r\"двата менискуса\", r\"medial (and|&) lateral menisc\",\n    ),\n    \"Lateral Meniscus\": _rx(\n        r\"lateral meniscus\", r\"lateral menisc\",\n        r\"menisco lateral\", r\"menisco externo\",\n        r\"menisque lateral\", r\"menisque externe\",\n        r\"laterale meniscus\", r\"buitenmeniscus\",\n        r\"aussenmeniskus\", r\"lateralen? meniskus\", r\"aussenmeniskushinterhorn\",\n        r\"lateral menisk\", r\"\\bdis menisk\",\n        r\"lateralni meniskus\", r\"lateralnog meniskusa\", r\"lateralnom meniskusu\",\n        r\"lateraln\\w* menisk\\w*\", r\"\\blateralnog meniska\\b\",\n        r\"εξω μηνισκ\", r\"μηνισκ[^ ]* του εξω\", r\"εξω διαμερισμα[^.]{0,40}μηνισκ\",\n        r\"латералния менискус\", r\"латерален менискус\", r\"външния менискус\",\n        r\"oba meniska\", r\"both menisci\", r\"ambos meniscos\", r\"beide menisci\",\n        r\"her iki menisku\", r\"αμφοτερ\\w* μηνισκ\",\n        r\"двата менискуса\", r\"medial (and|&) lateral menisc\",\n    ),\n}\n\nOA_EVIDENCE = _rx(\n    r\"osteoarthrit\", r\"\\barthros\", r\"\\bgonarthros\", r\"\\bosteoarthros\",\n    r\"chondropath\", r\"chondromalac\", r\"condropat\", r\"condromalac\", r\"\\bchondros\",\n    r\"\\bchondrosis\\b\", r\"chondral (loss|defect|ulcer|thinning|injury|fissur|wear)\",\n    r\"cartilage (loss|thinning|defect|fissur|wear|damage|heterogeneity|irregularit)\",\n    r\"(loss|thinning|fissur|defect|ulcer|erosion|denudation) of[^.]{0,20}cartilage\",\n    r\"articular cartilage[^.]{0,30}(loss|thin|fissur|defect|erosion|wear|irregular)\",\n    r\"osteophyt\", r\"osteofit\", r\"osteofyt\", r\"osteofito\", r\"osteophyten\", r\"spurring\",\n    r\"joint space narrowing\", r\"pinzamiento articular\", r\"reduced joint space\",\n    r\"kikirdak kayb\", r\"kikirdak incelme\", r\"kondropati\", r\"kondral\", r\"kikirdak dejener\",\n    r\"eklem aralig\\w* daral\", r\"eklem mesafesi daral\", r\"kikirdak kalinlig\\w* azal\",\n    r\"kraakbeen\", r\"gonartrose\", r\"artrose\", r\"\\bknorpel\", r\"arthrose\", r\"gonarthrose\",\n    r\"hrskavic\", r\"hondromalac\", r\"artroz\", r\"osteoartrit\", r\"artrotsk\", r\"artrotick\",\n    r\"\\boa promjen\", r\"\\boa\\b\", r\"degenerativne promjene hrskav\",\n    r\"χονδρ[^ ]*παθ\", r\"αρθριτ\", r\"αρθρωσ\", r\"οστεοφυτ\", r\"χονδρομαλακ\",\n    r\"αρθρικου χονδρου\", r\"εξαλειψη του αρθρικου χονδρου\", r\"διαβρωση του αρθρικου χονδρ\",\n    r\"λεπτυνση[^.]{0,30}χονδρ\", r\"φθορα[^.]{0,20}χονδρ\",\n    r\"артроз\", r\"хондропат\", r\"остеофит\", r\"хрущял[^.]{0,40}(изтън|увред|дефект|липс)\",\n    r\"изтъняване[^.]{0,30}хрущял\", r\"хондромалац\",\n    r\"ulcera[s]? condral\", r\"cartilago[^.]{0,25}(perdida|adelgaz)\",\n    r\"icrs grade\", r\"icrs\\b\", r\"outerbridge\", r\"\\bdenudation\\b\", r\"denudacij\",\n    r\"erozivne promjene\", r\"\\berosion of[^.]{0,20}cartilage\",\n    r\"kraakbeenlijden\", r\"kraakbeenverlies\",\n)\n\nTF_SITE = _rx(\n    r\"compartment\", r\"compartimento\", r\"compartiment\", r\"kompartman\", r\"kompartiment\",\n    r\"kompartment\", r\"odjelj\", r\"διαμερισμα\", r\"компартм\", r\"\\bотдел\",\n    r\"femorotibial\", r\"tibiofemoral\", r\"femoro tibial\", r\"femorotibiaal\",\n    r\"femorotibijaln\", r\"феморотибиал\", r\"\\bft zglob\", r\"tibiofemoraln\",\n    r\"condyle\", r\"condilo\", r\"kondyl\", r\"kondil\", r\"condyl\", r\"κονδυλ\",\n    r\"кондил\", r\"\\bplateau\", r\"\\bplato\\b\", r\"platillo\", r\"meseta\", r\"плато\",\n    r\"tibiaplateau\", r\"tibijaln\\w* plato\", r\"tibyal plato\", r\"tibia plato\",\n    r\"κνημιαι\", r\"μηριαι\", r\"weightbearing\", r\"weightbaring\", r\"zona de carga\",\n    r\"dragende deel\", r\"agirlik tasiyan\", r\"\\bfemur\\b\", r\"\\btibia\\b\", r\"\\bfemoral\\b\",\n    r\"\\btibial\\b\", r\"\\bfemura\\b\", r\"\\btibije\\b\", r\"\\bmesarthrio\\b\", r\"μεσαρθριο\",\n)\n\nPF_SITE = _rx(\n    r\"patellofemoral\", r\"femoropatellar\", r\"femoropatelar\", r\"patelofemoral\",\n    r\"retropatellar\", r\"retrorotulian\", r\"trochlea\", r\"troclea\", r\"troklea\",\n    r\"trochlear\", r\"trohlej\", r\"τροχιλ\", r\"\\bpatella\", r\"\\bpatellar\", r\"rotulian\",\n    r\"\\brotula\\b\", r\"\\bpatele\\b\", r\"patellofemoraal\", r\"femoropatellair\",\n    r\"επιγονατιδ\", r\"μηροεπιγονατιδ\", r\"пател\", r\"феморопател\",\n    r\"anterior compartment\", r\"compartimento anterior\", r\"prednj\\w* odjeljk\",\n    r\"\\bfp zglob\", r\"\\bpf zglob\", r\"\\bfaset\", r\"\\bfacet\", r\"patellofemoraln\",\n)\n\nSIDE_MEDIAL = _rx(\n    r\"\\bmedial\\w*\", r\"\\bmedyal\\w*\", r\"\\bmedijaln\\w*\", r\"\\bmediaal\\w*\",\n    r\"\\bmediale\\w*\", r\"\\binterno\\b\", r\"\\binterna\\b\", r\"\\binternos\\b\", r\"\\binterne\\b\",\n    r\"\\binnen\\w*\", r\"\\bic\\b\", r\"\\bunutarnj\\w*\", r\"\\bεσω\\w*\", r\"\\bεσωτερικ\\w*\",\n    r\"\\bмедиал\\w*\", r\"\\bвътреш\\w*\", r\"\\bbinnen\\w*\", r\"\\bmediaal\\b\", r\"\\bmediales?\\b\",\n)\n\nSIDE_LATERAL = _rx(\n    r\"\\blateral\\w*\", r\"\\bexterno\\b\", r\"\\bexterna\\b\", r\"\\bexternos\\b\", r\"\\bexterne\\b\",\n    r\"\\bdis\\b\", r\"\\blateraln\\w*\", r\"\\baussen\\w*\", r\"\\bbuiten\\w*\", r\"\\bεξω\\w*\",\n    r\"\\bεξωτερικ\\w*\", r\"\\bлатерал\\w*\", r\"\\bвъншн\\w*\", r\"\\bvanjsk\\w*\",\n)\n\nSIDE_ANTERIOR = _rx(\n    r\"\\banterior\\w*\", r\"\\bant\\b\", r\"\\bon\\b\", r\"\\bprednj\\w*\", r\"\\bvorder\\w*\",\n    r\"\\bvoorste\\b\", r\"\\bπροσθι\\w*\", r\"\\bпредн\\w*\", r\"\\banteriyor\\w*\", r\"\\bavant\\b\",\n    r\"\\banterieur\\w*\",\n)\n\nGLOBAL_OA = _rx(\n    r\"tri ?compartment\", r\"all three compartment\", r\"global(ised)? (oa|osteoarthrit)\",\n    r\"\\bgonarthros\", r\"\\bgonartros\", r\"\\bgonarthrose\", r\"\\bgonartrose\", r\"gonartro\",\n    r\"goanrtrot\", r\"gonartrot\",\n    r\"osteoarthritis of the knee\", r\"artrosis (de |)(la )?rodilla\", r\"knee osteoarthrit\",\n    r\"\\bdiz osteoartrit\", r\"\\bgonartroz\", r\"artroza koljena\",\n    r\"οστεοαρθριτιδα\", r\"αρθριτιδα του γονατος\", r\"εκφυλιστικη οστεοαρθριτ\",\n    r\"артроза на колянната\", r\"гонартроз\",\n    r\"degenerative joint disease\", r\"\\bdjd\\b\", r\"three compartments\",\n    r\"compartmens\", r\"compartments\",\n)\n\nDIRECT = {\n    \"Effusion\": _rx(\n        r\"\\beffusion\", r\"joint fluid\", r\"intra ?articular fluid\", r\"\\bhydrops\\b\",\n        r\"\\bhemarthros\", r\"\\bhaemarthros\",\n        r\"derrame articular\", r\"\\bderrame\\b\", r\"liquido articular\", r\"hemartrosis\",\n        r\"epanchement\",\n        r\"gewrichtsvocht\", r\"\\bvocht\\b\", r\"gewrichtseffusie\", r\"opzetting van suprapatell\",\n        r\"gelenkerguss\", r\"\\berguss\\b\", r\"gelenksergu\", r\"gelenksflussigkeit\",\n        r\"eklem\\w* ic\\w* sivi\", r\"efuzyon\", r\"eklem sivisi\", r\"eklem mesafesinde sivi\",\n        r\"sivi (miktari|artisi|birikimi)\", r\"sivi artis\", r\"\\bsivi\\b[^.]{0,25}artmis\",\n        r\"\\bizljev\", r\"\\bizliv\", r\"zglobn[^ ]* tekucin\", r\"\\bhidrops\\b\",\n        r\"αρθρικ[^ ]* υγρ\", r\"υγρου ενδαρθρικα\", r\"ενδαρθρικ[^ ]* υγρ\", r\"ποσοτητα υγρου\",\n        r\"ενδαρθρικ\", r\"αρθρικη συλλογη\", r\"υγρο στην αρθρωση\", r\"υγρου στην αρθρωση\",\n        r\"συλλογη υγρου\", r\"ενθαρθρικ\",\n        r\"ставен излив\", r\"излив\", r\"ставна течност\", r\"синовиална течност\",\n    ),\n    \"Synovitis\": _rx(\n        r\"synovit\", r\"sinovit\", r\"synovial (thickening|proliferation|hypertroph)\",\n        r\"thicken\\w* synovial\", r\"hypertroph\\w* of the synovium\",\n        r\"synoviale? (verdikking|proliferatie)\", r\"verdikkingen van (het )?synovium\",\n        r\"synovialitis\", r\"synovialis(verdickung|proliferation)\", r\"reizsynovial\",\n        r\"sinovijalitis\", r\"sinovitis\", r\"zadebljanje sinovij\", r\"proliferacij\\w* sinovij\",\n        r\"sinovijaln\\w* proliferacij\",\n        r\"υμενιτιδα\", r\"συνοβιτιδα\", r\"υμενικ[^ ]* υπερτροφ\", r\"αρθρικου υμεν\",\n        r\"παχυνση[^.]{0,20}υμεν\", r\"υμενα\",\n        r\"синовит\", r\"синовиал[^ ]* (задебел|пролифер)\",\n        r\"\\bpannus\\b\", r\"\\bhoffit\", r\"sinovyal\\w* (kalinlas|proliferas)\",\n        r\"sinovyal hipertrof\", r\"\\bartrit\\b\", r\"\\barthritis\\b\",\n    ),\n    \"Baker's\": _rx(\n        r\"baker\", r\"popliteal cyst\", r\"quiste popliteo\", r\"quistes popliteos\",\n        r\"kyste poplite\", r\"popliteale? cyst\", r\"poplitealzyste\", r\"bakerzyste\",\n        r\"popliteal kist\", r\"\\bbakerova\\b\", r\"poplitealn[^ ]* cist\", r\"popliteal\\w* cist\",\n        r\"κυστη baker\", r\"πολυχωρη συνοβιακη κυστη\", r\"κυστη του baker\",\n        r\"συνοβιακη κυστη\", r\"κυστη τυπου baker\",\n        r\"киста на бейкър\", r\"бейкърова киста\", r\"поплитеална киста\", r\"бекеров\",\n        r\"gastrocnemio ?semimembranos\", r\"gastrocnemius semimembranosus burs\",\n    ),\n    \"Contusion\": _rx(\n        r\"\\bcontusion\", r\"bone bruise\", r\"bone marrow (o?edema|contusion)\",\n        r\"marrow o?edema\", r\"\\bkontuz\", r\"medular bone o?edema\", r\"osseous contusion\",\n        r\"contusion osea\", r\"edema oseo\", r\"edema de medula osea\", r\"contusiones oseas\",\n        r\"oedeme osseux\", r\"contusion osseuse\",\n        r\"botcontusie\", r\"botoedeem\", r\"beenmergoedeem\", r\"botmergoedeem\",\n        r\"knochenmarkodem\", r\"knochenodem\", r\"knochenmarksodem\", r\"kontusion\",\n        r\"kemik kontuzyonu\", r\"kemik iligi odemi\", r\"kemik odemi\", r\"kemik iliginde odem\",\n        r\"kontuzyonel kemik\", r\"kemik iligi odemleri\",\n        r\"kostani edem\", r\"edem kosti\", r\"kontuzij\", r\"kostane srzi[^.]{0,20}edem\",\n        r\"οστεομυελικ[^ ]* οιδημα\", r\"οστικο οιδημα\", r\"μυελικο οιδημα\", r\"οστικο μωλωπ\",\n        r\"костномозъчен едем\", r\"костен едем\", r\"контузионен\", r\"костно мозъчен едем\",\n    ),\n    \"Fracture\": _rx(\n        r\"\\bfractur\", r\"\\bfract\\b\",\n        r\"\\bfractura\", r\"\\bfracturas\\b\",\n        r\"\\bfractuur\", r\"\\bbreuk\\b\",\n        r\"\\bfraktur\", r\"\\bbruch\\b\",\n        r\"\\bkirik\\b\", r\"\\bkirigi\\b\", r\"\\bkiri[kg]\\w*\",\n        r\"\\bprijelom\", r\"impresijsk[^ ]* fraktur\", r\"impaktcij\",\n        r\"καταγμα\", r\"καταγματ\",\n        r\"фрактур\", r\"счупван\", r\"фисур\",\n        r\"insufficiency fracture\", r\"stress fracture\", r\"avulsion fracture\",\n        r\"subchondral fracture\", r\"subkondral kiri\", r\"impaction (fracture|injury)\",\n        r\"osteochondral (fracture|impaction)\", r\"\\bsegond\\b\", r\"impactiefractuur\",\n        r\"subchondrale impression\", r\"subchondraler? impress\",\n    ),\n}\n\nDECOY = {\n    \"Fracture\": _rx(r\"microfractur\", r\"\\bfracture (risk|prophyla)\"),\n    \"Baker's\": _rx(r\"meniscal cyst\", r\"quiste meniscal\", r\"parameniscal\"),\n}\n\nPAIRED = {\"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\"}\n\nOA_TARGETS = [\"Medial OA\", \"Lateral OA\", \"PF OA\"]\n\nPLURAL_MENISCI = _rx(\n    r\"\\bmenisci\\b\", r\"\\bmeniscos\\b\", r\"\\bmenisques\\b\", r\"\\bmenisken\\b\",\n    r\"\\bmeniskusi\\b\", r\"\\bmenisk\\w*ler\\b\", r\"\\bμηνισκοι\\b\", r\"\\bμηνισκων\\b\",\n    r\"\\bменискуси\\b\", r\"\\bменискусите\\b\", r\"\\bmenisci\\w*\\b\",\n)\n\nANY_SIDE = _rx(SIDE_MEDIAL.pattern, SIDE_LATERAL.pattern)\n\nSTEM_MENISCUS = _rx(r\"menisc\\w*\", r\"menisk\\w*\", r\"μηνισκ\\w*\", r\"мениск\\w*\")\n\nSTEM_CRUCIATE = _rx(r\"cruciate\", r\"cruzado\", r\"croise\", r\"kruisband\", r\"kreuzband\",\n                    r\"capraz bag\\w*\", r\"krizn\\w*\", r\"χιαστ\\w*\", r\"кръстн\\w*\",\n                    r\"\\bacl\\b\", r\"\\blca\\b\", r\"\\bvkb\\b\", r\"\\bocb\\b\", r\"\\bacb\\b\")\n\nSTEM_COLLATERAL = _rx(r\"collateral\\w*\", r\"colateral\\w*\", r\"kollateral\\w*\",\n                      r\"collaterale\\w*\", r\"kolateraln\\w*\", r\"yan bag\\w*\",\n                      r\"πλαγι\\w*\", r\"колатерал\\w*\", r\"странич\\w*\",\n                      r\"innenband\\w*\", r\"binnenband\\w*\", r\"\\bmcl\\b\", r\"\\blcm\\b\",\n                      r\"\\biyb\\b\")\n\nSTEM_FRACTURE = _rx(r\"fractur\\w*\", r\"fraktur\\w*\", r\"fractuur\\w*\", r\"\\bfract\\b\",\n                    r\"kiri[kgğ]\\w*\", r\"prijelom\\w*\", r\"lom kosti\", r\"\\bbreuk\\w*\",\n                    r\"\\bbruch\\w*\", r\"καταγμα\\w*\", r\"καταγματ\\w*\", r\"фрактур\\w*\",\n                    r\"счупван\\w*\", r\"fisur\\w* (osea|oseas|kost)\", r\"fissur\\w* kost\")\n\nPOSTERIOR_ONLY = _rx(r\"\\bpcl\\b\", r\"\\blcp\\b\", r\"\\bhkb\\b\", r\"\\bacb\\b\",\n                     r\"posterior cruciate\", r\"cruzado posterior\", r\"croise posterieur\",\n                     r\"achterste kruisband\", r\"hinteres kreuzband\", r\"arka capraz\",\n                     r\"straznji krizn\", r\"οπισθι[οα]\\w* χιαστ\", r\"задна кръстн\",\n                     r\"задната кръстн\")\n\nLATERAL_COLL_ONLY = _rx(r\"\\blcl\\b\", r\"\\bfcl\\b\", r\"lateral collateral\",\n                        r\"fibular collateral\", r\"colateral lateral\", r\"colateral externo\",\n                        r\"buitenband\", r\"aussenband\", r\"dis yan bag\",\n                        r\"lateralni kolateraln\", r\"εξω πλαγι\", r\"латерален колатерал\")\n\nSTEM_RULES = {\n    \"ACL\": (STEM_CRUCIATE, SIDE_ANTERIOR),\n    \"MCL\": (STEM_COLLATERAL, SIDE_MEDIAL),\n    \"Medial Meniscus\": (STEM_MENISCUS, SIDE_MEDIAL),\n    \"Lateral Meniscus\": (STEM_MENISCUS, SIDE_LATERAL),\n}\n\nSEV_LOW = _rx(\n    r\"\\bsmall\\b\", r\"\\bminimal\\b\", r\"\\btrace\\b\", r\"\\bmild\\b\", r\"\\bslight\\b\",\n    r\"\\btiny\\b\", r\"\\bscant\\b\", r\"\\bdiscrete\\b\", r\"\\blow ?grade\\b\", r\"\\bincipient\\b\",\n    r\"\\bleve\\b\", r\"\\bminim\", r\"\\bpeque\", r\"\\bfina\\b\", r\"\\bfino\\b\", r\"\\bligero\\b\", r\"\\bescaso\\b\", r\"\\bdiscreto\\b\",\n    r\"\\bhafif\\b\", r\"\\baz miktarda\\b\", r\"\\bsilik\\b\",\n    r\"\\bmanj\\w*\", r\"\\bblago\\b\", r\"\\bdiskretn\", r\"\\bmalo\\b\", r\"\\bpocetn\",\n    r\"\\bgering\", r\"\\bdiskret\", r\"\\bkleine?r?\\b\", r\"\\bwenig\\b\", r\"\\bzarte?\\b\",\n    r\"\\bbeperkte?\\b\", r\"\\bgeringe\\b\", r\"\\bweinig\\b\", r\"\\blichte?\\b\", r\"\\blicht\\b\",\n    r\"\\bηπι\", r\"\\bμικρ\", r\"\\bελαχιστ\", r\"\\bαρχομεν\",\n    r\"\\bминимал\", r\"\\bлек\", r\"\\bмалк\", r\"\\bнеголям\",\n)\n\nSEV_HIGH = _rx(\n    r\"\\blarge\\b\", r\"\\bmarked\\b\", r\"\\bmassive\\b\", r\"\\bsevere\\b\", r\"\\bextensive\\b\",\n    r\"\\bmoderate\\b\", r\"\\bgross\\b\", r\"\\bsignificant\\b\", r\"\\babundant\\b\", r\"\\btense\\b\",\n    r\"\\bcomplete\\b\", r\"\\bfull ?thickness\\b\", r\"\\bhigh ?grade\\b\", r\"\\badvanced\\b\",\n    r\"\\bmoderad\", r\"\\bimportante\\b\", r\"\\bsevera?\\b\", r\"\\bmarcad\", r\"\\bcuantios\",\n    r\"\\bespesor total\\b\", r\"\\bcompleta?\\b\",\n    r\"\\bbelirgin\\b\", r\"\\byaygin\\b\", r\"\\bileri\\b\", r\"\\bciddi\\b\", r\"\\bbol\\b\", r\"\\bkomplet\",\n    r\"\\bopsezan\\b\", r\"\\bveliki\\b\", r\"\\bizrazit\", r\"\\bznacajn\", r\"\\bumjeren\",\n    r\"\\buznapredoval\", r\"\\bpotpun\", r\"\\bkompleksn\",\n    r\"\\bausgepragt\", r\"\\bdeutlich\", r\"\\bmassiv\", r\"\\bmassig\", r\"\\bgross\",\n    r\"\\buitgebreid\", r\"\\bgevorderd\", r\"\\bveel\\b\", r\"\\bmatige?\\b\", r\"\\bvolledig\",\n    r\"\\bμετρι\", r\"\\bμεγαλ\", r\"\\bεκτεταμεν\", r\"\\bευμεγεθ\", r\"\\bσοβαρ\", r\"\\bπληρη\",\n    r\"\\bголям\", r\"\\bизразен\", r\"\\bзначим\", r\"\\bумерен\", r\"\\bобилен\", r\"\\bпълн\",\n)\n\nGRADE_HIGH = re.compile(r\"grade?[ao]?\\s*(3|4|iii|iv)\\b|icrs grade (iii|iv|3|4)|\"\n                        r\"stupnja iv|stupnja iii|\\bgrado (3|4)\\b|\\bgrad (3|4)\\b|\"\n                        r\"\\bgrade (3|4)\\b\")\n\nDEGENERATIVE_MARROW = _rx(\n    r\"subchondral\", r\"subcondral\", r\"subkondral\", r\"supkondraln\", r\"subchondraln\",\n    r\"υποχονδρι\", r\"υπαρθρικ\", r\"субхондрал\", r\"subchondrale?\", r\"subartikuler\",\n    r\"\\bcyst\", r\"\\bquist\", r\"\\bzyste\\b\", r\"\\bcistic\", r\"reactive\", r\"reactivo\",\n    r\"degenerative\", r\"degenerativ\", r\"reaktiv\", r\"\\bcisti\\b\",\n)\n\nTRAUMA = _rx(\n    r\"\\bbruise\\b\", r\"\\bcontusion\", r\"\\bkontuz\", r\"\\btrauma\", r\"\\bimpaction\\b\",\n    r\"\\bpivot shift\\b\", r\"\\bkissing\\b\", r\"\\bacute\\b\", r\"\\bagudo\\b\", r\"\\bakut\",\n    r\"\\bpivot kaymasi\\b\", r\"\\bcontusion osseuse\\b\", r\"\\bbone bruise\\b\",\n    r\"\\bbotcontusie\\b\", r\"\\bконтузион\", r\"\\bμωλωπ\", r\"\\bkontuzij\", r\"\\bimpaktcij\",\n    r\"\\bimpakcij\", r\"\\bfall\\b\", r\"\\binjury\\b\", r\"\\bimpression\\b\",\n)\n\nSYNOVIAL_PROXY = _rx(\n    r\"bursit\", r\"burzit\", r\"\\bbursa\\b[^.]{0,30}(fluid|distend|sivi|tekucin|opzetting)\",\n    r\"suprapatellar (bursitis|effusion|recess)\", r\"suprapatellar bursa\",\n    r\"suprapatellar bursada\", r\"suprapatelarno\", r\"suprapatellaire recessus\",\n    r\"hoffa\", r\"hoffit\", r\"plica\", r\"plika\", r\"πλικα\", r\"fat pad[^.]{0,20}(edema|oedema)\",\n    r\"kapsul\", r\"capsul\", r\"καψ\", r\"капсул\", r\"\\bpannus\\b\", r\"\\bsinov\", r\"\\bsynov\",\n)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:36:46.851238Z","iopub.execute_input":"2026-08-08T11:36:46.851667Z","iopub.status.idle":"2026-08-08T11:36:52.886329Z","shell.execute_reply.started":"2026-08-08T11:36:46.851636Z","shell.execute_reply":"2026-08-08T11:36:52.885742Z"}},"outputs":[],"execution_count":null},{"id":"6d7c48bb-5dc8-4d12-874f-98ec8acc854b","cell_type":"markdown","source":"## Multilingual evidence ledger\n\nVocabulary matching and assertion reasoning are separated. Each report becomes a ledger\nof positive, negative and uncertain evidence, then the ledger becomes a graded target.\n\n","metadata":{}},{"id":"83453088-1a3d-44e5-942d-1648c07f7409","cell_type":"code","source":"def canonical_text(text):\n    if not isinstance(text, str):\n        return \"\"\n    text = text.translate(_PRE).casefold()\n    text = unicodedata.normalize(\"NFKD\", text)\n    text = \"\".join(ch for ch in text if not unicodedata.combining(ch))\n    text = re.sub(r\"[_/\\\\-]+\", \" \", text)\n    return re.sub(r\"[ \\t]+\", \" \", text)\n\n\ndef report_clauses(text):\n    raw_lines = [line.strip() for line in str(text).splitlines() if line.strip()]\n    reflowed = []\n    for line in raw_lines:\n        if (reflowed and len(reflowed[-1].split()) >= 4\n                and not re.search(r\"[.;:!?>*•]$\", reflowed[-1])\n                and line and not line[:1].isupper()):\n            reflowed[-1] += \" \" + line\n        else:\n            reflowed.append(line)\n    joined = [line.strip() for line in canonical_text(\"\\n\".join(reflowed)).splitlines() if line.strip()]\n    fragments = []\n    for line_index, line in enumerate(joined):\n        parts = [p.strip() for p in re.split(r\"(?<=[.;!?])\\s+\", line) if p.strip()]\n        for part in parts:\n            fragments.append(part)\n            if len(part.split()) > 24:\n                fragments.extend(x.strip() for x in part.split(\",\") if len(x.split()) > 2)\n        if line.endswith(\":\") and line_index + 1 < len(joined):\n            fragments.append(line + \" \" + joined[line_index + 1])\n    return fragments\n\n\ndef close_terms(text, first, second, distance=55):\n    left = list(first.finditer(text))\n    right = list(second.finditer(text))\n    return any(\n        min(abs(a.start() - b.end()), abs(b.start() - a.end())) <= distance\n        for a in left for b in right\n    )\n\n\ndef grade_number(text):\n    hit = _GRADE_RX.search(text)\n    if not hit:\n        return None\n    value = hit.group(1).lower()\n    return _ROMAN.get(value, int(value) if value.isdigit() else None)\n\n\nCONTRAST = re.compile(r\"\\b(?:but|however|ancak|fakat|pero|maar|aber|no i|ali|ωστοσο|αλλα|но)\\b\")\n\n\ndef span_negated(text, start, end):\n    for match in PRE_NEG.finditer(text):\n        if match.end() <= start and start - match.end() <= NEG_WINDOW:\n            if not CONTRAST.search(text[match.end():start]):\n                return True\n    for match in POST_NEG.finditer(text):\n        if match.start() >= end and match.start() - end <= NEG_WINDOW:\n            return True\n    return False\n\n\ndef assertion(text, span=None):\n    if UNCERTAIN.search(text):\n        return \"uncertain\"\n    if span is None:\n        negated = NEGATION.search(text) is not None\n    else:\n        start, end = span\n        negated = span_negated(text, start, end)\n    if negated:\n        return \"negative\"\n    if NORMALITY.search(text) or NORMAL_PHRASE.search(text):\n        return \"positive\" if TEAR.search(text) or GRADE_HIGH.search(text) else \"negative\"\n    return \"positive\"\n\n\ndef evidence_score(positive, negative, uncertain, strength):\n    if positive or uncertain:\n        score = min(0.97, 0.50 + 0.45 * strength + 0.015 * min(positive, 3))\n        confidence = min(1.0, 0.55 + 0.15 * positive)\n    elif negative:\n        score = max(0.04, 0.20 - 0.04 * negative)\n        confidence = min(0.90, 0.45 + 0.12 * negative)\n    else:\n        score, confidence = 0.28, 0.05\n    return float(score), float(confidence)\n\n\nclass EvidenceLedger:\n    def __init__(self, report):\n        self.clauses = report_clauses(report)\n\n    @staticmethod\n    def _severity(text):\n        high = SEV_HIGH.search(text) is not None\n        low = SEV_LOW.search(text) is not None\n        if high != low:\n            return 1.0 if high else 0.45\n        return 0.80 if high else 0.75\n\n    @staticmethod\n    def _paired_strength(text, meniscus):\n        grade = grade_number(text)\n        if TEAR.search(text):\n            value = 1.0\n        elif grade is not None:\n            value = (0.95 if grade >= 3 else 0.30) if meniscus else (0.85 if grade >= 2 else 0.30)\n        elif DEGEN.search(text):\n            value = 0.35 if meniscus else 0.40\n        else:\n            value = 0.45 if meniscus else 0.55\n        if SEV_HIGH.search(text) and not SEV_LOW.search(text):\n            value = min(1.0, value * 1.2)\n        elif SEV_LOW.search(text) and not SEV_HIGH.search(text):\n            value *= 0.7\n        return value\n\n    def paired(self, target):\n        pathology = _rx(TEAR.pattern, DEGEN.pattern, INJURY.pattern)\n        stem, side = STEM_RULES[target]\n        positive = negative = uncertain = 0\n        strength = 0.0\n        meniscus = \"Meniscus\" in target\n        for clause in self.clauses:\n            anatomy = ANAT[target].search(clause)\n            if anatomy is None and close_terms(clause, stem, side):\n                anatomy = stem.search(clause)\n            if anatomy is None and meniscus and PLURAL_MENISCI.search(clause) and not ANY_SIDE.search(clause):\n                anatomy = PLURAL_MENISCI.search(clause)\n            if anatomy is None:\n                continue\n            path = pathology.search(clause)\n            if path is None and grade_number(clause) is None:\n                if NORMALITY.search(clause) or NORMAL_PHRASE.search(clause):\n                    negative += 1\n                continue\n            state = assertion(clause, path.span() if path else None)\n            if state == \"positive\":\n                positive += 1\n                strength = max(strength, self._paired_strength(clause, meniscus))\n            elif state == \"negative\":\n                negative += 1\n            else:\n                uncertain += 1\n                strength = max(strength, 0.45 * self._paired_strength(clause, meniscus))\n        return (*evidence_score(positive, negative, uncertain, strength), positive, negative)\n\n    def direct(self, target):\n        positive = negative = uncertain = 0\n        strength = 0.0\n        matcher = DIRECT[target]\n        if target == \"Fracture\":\n            matcher = _rx(matcher.pattern, STEM_FRACTURE.pattern)\n        for clause in self.clauses:\n            hit = matcher.search(clause)\n            if not hit or (target in DECOY and DECOY[target].search(clause)):\n                continue\n            state = assertion(clause, hit.span())\n            value = self._severity(clause)\n            if target == \"Contusion\":\n                if DEGENERATIVE_MARROW.search(clause):\n                    value *= 0.45\n                if TRAUMA.search(clause):\n                    value = min(1.0, value * 1.35)\n            if state == \"positive\":\n                positive += 1\n                strength = max(strength, value)\n            elif state == \"negative\":\n                negative += 1\n            else:\n                uncertain += 1\n                strength = max(strength, 0.30)\n        return (*evidence_score(positive, negative, uncertain, strength), positive, negative)\n\n    def osteoarthritis(self):\n        local = {target: [0, 0, 0, 0.0] for target in OA_TARGETS}\n        global_pos = global_neg = 0\n        global_strength = 0.0\n        for clause in self.clauses:\n            hit = OA_EVIDENCE.search(clause)\n            if not hit:\n                continue\n            state = assertion(clause, hit.span())\n            severity = self._severity(clause)\n            targets = []\n            if close_terms(clause, TF_SITE, SIDE_MEDIAL, 45):\n                targets.append(\"Medial OA\")\n            if close_terms(clause, TF_SITE, SIDE_LATERAL, 45):\n                targets.append(\"Lateral OA\")\n            if PF_SITE.search(clause):\n                targets.append(\"PF OA\")\n            if not targets:\n                if state == \"positive\":\n                    global_pos += 1\n                    global_strength = max(global_strength, severity if GLOBAL_OA.search(clause) else severity * 0.70)\n                elif state == \"negative\":\n                    global_neg += 1\n                continue\n            for target in targets:\n                bucket = local[target]\n                if state == \"positive\":\n                    bucket[0] += 1\n                    bucket[3] = max(bucket[3], severity)\n                elif state == \"negative\":\n                    bucket[1] += 1\n                else:\n                    bucket[2] += 1\n                    bucket[3] = max(bucket[3], 0.30)\n        result = {}\n        for target, (pos, neg, unc, best) in local.items():\n            if not (pos or unc) and global_pos:\n                if neg:\n                    score, confidence = evidence_score(0, neg, 0, 0.0)\n                    score, confidence = max(score, 0.35), confidence * 0.70\n                else:\n                    score, confidence = evidence_score(global_pos, 0, 0, global_strength * 0.92)\n                    confidence *= 0.75\n            else:\n                score, confidence = evidence_score(pos, neg + global_neg, unc, best)\n            result[target] = (score, confidence, pos, neg)\n        return result\n\n    def targets(self):\n        output = {}\n        for target in PAIRED:\n            output[target] = self.paired(target)\n        output.update(self.osteoarthritis())\n        for target in (\"Effusion\", \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"):\n            output[target] = self.direct(target)\n        syn_score, syn_conf, syn_pos, syn_neg = output[\"Synovitis\"]\n        if syn_pos == 0 and syn_neg == 0:\n            proxy = sum(bool(SYNOVIAL_PROXY.search(c)) and assertion(c) == \"positive\" for c in self.clauses)\n            effusion = output[\"Effusion\"][0]\n            syn_score = min(0.72, 0.30 + 0.30 * max(0.0, (effusion - 0.5) / 0.45) + 0.06 * min(proxy, 3))\n            output[\"Synovitis\"] = (syn_score, 0.18, 0, 0)\n        row = {}\n        for target, (score, confidence, positive, negative) in output.items():\n            row[target] = score\n            row[target + \"__conf\"] = confidence\n            row[target + \"__npos\"] = positive\n            row[target + \"__nneg\"] = negative\n        return row\n\n\nTRAIN_FRAME = pd.read_csv(ROOT / \"train.csv\")\nREPORT_ROWS = pd.DataFrame(\n    [EvidenceLedger(report).targets() for report in TRAIN_FRAME[\"Report\"].fillna(\"\")],\n    index=TRAIN_FRAME[\"StudyInstanceUID\"].astype(str),\n)\nGOLD = TRAIN_FRAME.dropna(subset=TARGETS).set_index(\"StudyInstanceUID\")[TARGETS]\nlog(f\"reports={len(REPORT_ROWS)} gold={len(GOLD)}\")\nif len(GOLD):\n    report_auc = []\n    for target in TARGETS:\n        y = GOLD[target].astype(int).values\n        p = REPORT_ROWS.loc[GOLD.index, target].values\n        report_auc.append(roc_auc_score(y, p) if len(np.unique(y)) > 1 else np.nan)\n    log(f\"report-gold macro={np.nanmean(report_auc):.4f}\")\n    for target, score in zip(TARGETS, report_auc):\n        log(f\"  report {target:18s} auc={score:.4f}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:36:52.887688Z","iopub.execute_input":"2026-08-08T11:36:52.887934Z","iopub.status.idle":"2026-08-08T11:37:17.442019Z","shell.execute_reply.started":"2026-08-08T11:36:52.887912Z","shell.execute_reply":"2026-08-08T11:37:17.441196Z"}},"outputs":[],"execution_count":null},{"id":"dd064384-3e85-4b4d-bb2b-912b7ddb86e3","cell_type":"markdown","source":"## Geometry-aware study store\n\nOne protocol-aware series is retained for each of six complementary views. DICOM\ngeometry orders slices, a fixed physical crop normalises field of view, and uint8 keeps\nenough slice coverage in memory for end-to-end fine-tuning.\n\n","metadata":{}},{"id":"8595b498-f5d1-4c2d-8846-b416018212f3","cell_type":"code","source":"HEADER_TAGS = [\n    \"SeriesDescription\", \"SequenceName\", \"ScanOptions\", \"ScanningSequence\",\n    \"RepetitionTime\", \"EchoTime\", \"Laterality\", \"PixelSpacing\", \"Rows\", \"Columns\",\n]\nFATSAT_TOKENS = {\"FS\", \"FATSAT\", \"FAT_SAT\", \"FSAT\"}\nFATSAT_PATTERN = re.compile(r\"\\bfs\\b|fatsat|fat sat|\\bstir\\b|\\bspair\\b|\\bspir\\b|water excit|\\btirm\\b|fatsup\")\n\n\ndef scan_series(split):\n    jobs = []\n    base = ROOT / f\"{split}_series\"\n    for study in os.scandir(base):\n        if study.is_dir():\n            for series in os.scandir(study.path):\n                if series.is_dir():\n                    jobs.append((study.name, series.name, series.path))\n\n    def read(job):\n        study, series, directory = job\n        row = {\"StudyInstanceUID\": study, \"SeriesInstanceUID\": series, \"dir\": directory}\n        try:\n            files = sorted(entry.name for entry in os.scandir(directory) if entry.name.lower().endswith(\".dcm\"))\n            row[\"files\"] = files\n            row[\"n_slices\"] = len(files)\n            if files:\n                ds = pydicom.dcmread(os.path.join(directory, files[len(files) // 2]), stop_before_pixels=True, force=True)\n                for tag in HEADER_TAGS:\n                    value = getattr(ds, tag, None)\n                    if value is None:\n                        row[tag] = None\n                    elif isinstance(value, (list, tuple)) or type(value).__name__ == \"MultiValue\":\n                        row[tag] = \"|\".join(map(str, value))\n                    else:\n                        row[tag] = str(value)\n        except Exception as exc:\n            row[\"error\"] = str(exc)[:100]\n        return row\n\n    with ThreadPoolExecutor(max_workers=HEADER_THREADS) as pool:\n        frame = pd.DataFrame(pool.map(read, jobs))\n    log(f\"{split} inventory={len(frame)}\")\n    return frame\n\n\ndef protocol_features(frame, plane_lookup):\n    frame = frame.copy()\n    frame[\"plane\"] = frame[\"SeriesInstanceUID\"].map(plane_lookup)\n    description = (frame[\"SeriesDescription\"].fillna(\"\") + \" \" + frame[\"SequenceName\"].fillna(\"\"))\n    description = description.str.lower().str.replace(r\"[_\\-.]+\", \" \", regex=True)\n    options = frame[\"ScanOptions\"].fillna(\"\").str.upper().str.split(\"|\")\n    frame[\"fatsat\"] = description.str.contains(FATSAT_PATTERN) | options.apply(\n        lambda values: any(value.strip() in FATSAT_TOKENS for value in values)\n    )\n    tr = pd.to_numeric(frame[\"RepetitionTime\"], errors=\"coerce\")\n    te = pd.to_numeric(frame[\"EchoTime\"], errors=\"coerce\")\n    t1 = description.str.contains(r\"\\bt1w?\\b\", regex=True)\n    t2 = description.str.contains(r\"\\bt2w?\\b\", regex=True)\n    pdw = description.str.contains(r\"\\bpdw?\\b|proton|\\bdp\\b|dens\", regex=True)\n    gre = frame[\"ScanningSequence\"].fillna(\"\").str.upper().str.contains(\"GR\")\n    frame[\"weighting\"] = np.where(t1 & ~t2 & ~pdw, \"T1\", np.where(\n        t2 & ~pdw, \"T2\", np.where(pdw, \"PD\", np.where(\n            gre, \"GRE\", np.where(tr < 800, \"T1\", np.where(te > 60, \"T2\", \"PD\"))\n        ))\n    ))\n    frame[\"fluid\"] = frame[\"weighting\"].isin([\"PD\", \"T2\"])\n    frame[\"spacing\"] = pd.to_numeric(frame[\"PixelSpacing\"].fillna(\"\").str.split(\"|\").str[0], errors=\"coerce\")\n    return frame\n\n\ndef choose_views(frame):\n    studies = {}\n    for study, group in frame.groupby(\"StudyInstanceUID\"):\n        selected = {}\n        for name, plane, fluid, fatsat in SLOTS:\n            candidate = group[(group[\"plane\"] == plane) & (group[\"fatsat\"] == fatsat)]\n            if fluid is not None:\n                candidate = candidate[candidate[\"fluid\"] == fluid]\n            if candidate.empty and fluid is False:\n                candidate = group[(group[\"plane\"] == plane) & (~group[\"fatsat\"])]\n            if not candidate.empty:\n                selected[name] = candidate.sort_values([\"n_slices\", \"spacing\"], ascending=[False, True]).iloc[0].to_dict()\n        studies[str(study)] = selected\n    return studies\n\n\ndef laterality(frame):\n    result = {}\n    for study, group in frame.groupby(\"StudyInstanceUID\"):\n        values = [str(x).strip().upper() for x in group[\"Laterality\"].dropna()]\n        values = [x[0] for x in values if x and x[0] in \"LR\"]\n        result[str(study)] = values[0] if values else None\n    return result\n\n\ndef free_memory_gb():\n    try:\n        for line in Path(\"/proc/meminfo\").read_text().splitlines():\n            if line.startswith(\"MemAvailable:\"):\n                return int(line.split()[1]) / 1024 ** 2\n    except Exception:\n        pass\n    return CACHE_BUDGET_GB + RESERVE_GB\n\n\ndef cache_layout(n_studies):\n    budget = min(CACHE_BUDGET_GB, max(3.0, free_memory_gb() - RESERVE_GB))\n    image = CACHE_IMAGE\n    while image > 154 and n_studies * N_SLOTS * GROUP_SIZE * image * image > budget * 1024 ** 3:\n        image -= 14\n    bytes_per_group = n_studies * N_SLOTS * GROUP_SIZE * image * image\n    groups = int(budget * 1024 ** 3 // max(bytes_per_group, 1))\n    return max(1, min(MAX_GROUPS, groups)), image\n\n\nGEOMETRY_TAGS = [(0x0020, 0x0032), (0x0020, 0x0037), (0x0020, 0x0013)]\n\n\ndef geometry_order(record):\n    keyed = []\n    last_ds = None\n    for filename in record[\"files\"]:\n        key = None\n        try:\n            last_ds = pydicom.dcmread(\n                os.path.join(record[\"dir\"], filename), force=True, stop_before_pixels=True,\n                specific_tags=GEOMETRY_TAGS,\n            )\n            orientation = np.asarray(last_ds.ImageOrientationPatient, dtype=float)\n            position = np.asarray(last_ds.ImagePositionPatient, dtype=float)\n            key = float(np.dot(position, np.cross(orientation[:3], orientation[3:])))\n        except Exception:\n            try:\n                key = float(last_ds.InstanceNumber)\n            except Exception:\n                key = None\n        keyed.append((key, filename))\n    if any(key is None for key, _ in keyed):\n        return record[\"files\"]\n    return [filename for _, filename in sorted(keyed)]\n\n\ndef decode_view(record, slices, image_size, side):\n    files = record.get(\"ordered\", record[\"files\"])\n    if not files:\n        return None\n    low = int(0.20 * (len(files) - 1))\n    high = int(0.80 * (len(files) - 1))\n    indices = np.round(np.linspace(low, max(low, high), slices)).astype(int)\n    planes = []\n    shape = None\n    for index in indices:\n        try:\n            ds = pydicom.dcmread(os.path.join(record[\"dir\"], files[int(index)]), force=True)\n            image = ds.pixel_array.astype(np.float32)\n            image = image * float(getattr(ds, \"RescaleSlope\", 1) or 1) + float(getattr(ds, \"RescaleIntercept\", 0) or 0)\n            if str(getattr(ds, \"PhotometricInterpretation\", \"\")).upper() == \"MONOCHROME1\":\n                image = image.max() + image.min() - image\n            shape = image.shape if shape is None else shape\n            if image.shape != shape:\n                image = None\n        except Exception:\n            image = None\n        planes.append(image)\n    if shape is None:\n        return None\n    volume = np.stack([p if p is not None else np.zeros(shape, np.float32) for p in planes])\n    spacing = record.get(\"spacing\")\n    if spacing is not None and np.isfinite(spacing) and spacing > 0:\n        crop = int(round(CROP_MM / spacing))\n        height, width = shape\n        if 16 < crop < min(height, width):\n            y0 = (height - crop) // 2\n            x0 = (width - crop) // 2\n            volume = volume[:, y0:y0 + crop, x0:x0 + crop]\n    lo, hi = np.percentile(volume, [1, 99])\n    volume = np.clip((volume - lo) / max(hi - lo, 1e-6), 0, 1)\n    tensor = F.interpolate(\n        torch.from_numpy(np.ascontiguousarray(volume)).unsqueeze(0),\n        size=(image_size, image_size), mode=\"bilinear\", align_corners=False,\n    ).squeeze(0)\n    if side == \"R\":\n        if record[\"plane\"] in (\"Coronal\", \"Axial\"):\n            tensor = torch.flip(tensor, [-1])\n        else:\n            tensor = torch.flip(tensor, [0])\n    return (tensor * 255).round().clamp(0, 255).to(torch.uint8).numpy()\n\n\ndef build_store(view_map, side_map, split, groups, image_size):\n    studies = sorted(view_map)\n    index = {study: row for row, study in enumerate(studies)}\n    slices = groups * GROUP_SIZE\n    pixels = np.zeros((len(studies), N_SLOTS, slices, image_size, image_size), np.uint8)\n    mask = np.zeros((len(studies), N_SLOTS), np.float32)\n    jobs = []\n    for study in studies:\n        for slot_index, (slot_name, _, _, _) in enumerate(SLOTS):\n            if slot_name in view_map[study]:\n                jobs.append((study, slot_index, view_map[study][slot_name]))\n    log(f\"{split}: ordering {len(jobs)} chosen series\")\n    with ThreadPoolExecutor(max_workers=ORDER_THREADS) as pool:\n        for (_, _, record), ordered in zip(jobs, pool.map(lambda job: geometry_order(job[2]), jobs)):\n            record[\"ordered\"] = ordered\n    log(f\"{split}: decoding cache {pixels.shape} ({pixels.nbytes / 1024 ** 3:.2f} GB)\")\n    with ThreadPoolExecutor(max_workers=PIXEL_THREADS) as pool:\n        for start in range(0, len(jobs), 384):\n            block = jobs[start:start + 384]\n            decoded = pool.map(\n                lambda job: decode_view(job[2], slices, image_size, side_map.get(job[0])), block\n            )\n            for (study, slot_index, _), value in zip(block, decoded):\n                if value is not None:\n                    row = index[study]\n                    pixels[row, slot_index] = value\n                    mask[row, slot_index] = 1.0\n            if start % 3072 == 0:\n                log(f\"  {split} decoded {min(start + len(block), len(jobs))}/{len(jobs)}\")\n    return studies, pixels, mask\n\n\nTRAIN_SERIES = pd.read_csv(ROOT / \"train_series.csv\")\nTEST_SERIES = pd.read_csv(ROOT / \"test_series.csv\")\nplane_lookup = dict(zip(\n    pd.concat([TRAIN_SERIES, TEST_SERIES])[\"SeriesInstanceUID\"],\n    pd.concat([TRAIN_SERIES, TEST_SERIES])[\"Anatomical_Plane\"],\n))\ntest_headers = protocol_features(scan_series(\"test\"), plane_lookup)\ntrain_headers = protocol_features(scan_series(\"train\"), plane_lookup)\ntrain_views, test_views = choose_views(train_headers), choose_views(test_headers)\nN_GROUPS, CACHE_IMAGE = cache_layout(len(train_views) + len(test_views))\nif SMOKE:\n    train_views = dict(list(train_views.items())[:240])\n    N_GROUPS = min(N_GROUPS, 2)\nlog(f\"cache groups={N_GROUPS} image={CACHE_IMAGE} train views={np.mean([len(v) for v in train_views.values()]):.2f}\")\nTEST_STUDIES, TEST_CACHE, TEST_MASK = build_store(test_views, laterality(test_headers), \"test\", N_GROUPS, CACHE_IMAGE)\nTRAIN_STUDIES, TRAIN_CACHE, TRAIN_MASK = build_store(train_views, laterality(train_headers), \"train\", N_GROUPS, CACHE_IMAGE)\n\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-08T12:10:02.622Z"}},"outputs":[],"execution_count":null},{"id":"e2a0dc36-388d-47dc-9ccd-048261151227","cell_type":"markdown","source":"## Hierarchical evidence router\n\nA target query first pools six sequence views inside each slice group. A learned focality\ngate then interpolates between stable mean evidence and soft maximum evidence across\ngroups. Training sees two groups at once, so this second pooling level is learned rather\nthan selected after validation.\n\n","metadata":{}},{"id":"e599501c-2f2a-4504-a05a-3ed9a46a8910","cell_type":"code","source":"def gpu_status():\n    if not torch.cuda.is_available():\n        return False, \"CUDA unavailable\"\n    try:\n        left = torch.randn(32, 32, device=\"cuda\")\n        float((left @ left).sum())\n        return True, torch.cuda.get_device_name(0)\n    except Exception as exc:\n        return False, str(exc)[:160]\n\n\nGPU_OK, GPU_NAME = gpu_status()\nDEVICE = torch.device(\"cuda\" if GPU_OK else \"cpu\")\nlog(f\"device={DEVICE} gpu={GPU_NAME}\")\nif not GPU_OK and not SMOKE:\n    raise RuntimeError(\"A working GPU is required; fallback submission remains available\")\n\n\ndef locate_backbone(variant):\n    matches = []\n    for config in Path(\"/kaggle/input\").rglob(\"config.json\"):\n        path = config.parent\n        low = str(path).replace(\"\\\\\", \"/\").lower()\n        if \"dinov2\" in low and (f\"/{variant}/\" in low or low.rstrip(\"/\").endswith(variant)):\n            matches.append(path)\n    if not matches:\n        raise FileNotFoundError(f\"attach DINOv2 {variant} checkpoint\")\n    return sorted(matches, key=str)[0]\n\n\nclass EvidenceRouter(nn.Module):\n    def __init__(self, input_dim, hidden=256):\n        super().__init__()\n        self.hidden = hidden\n        self.project = nn.Sequential(nn.LayerNorm(input_dim), nn.Linear(input_dim, hidden), nn.GELU())\n        self.slot_embedding = nn.Parameter(torch.randn(N_SLOTS, hidden) * 0.02)\n        self.group_embedding = nn.Parameter(torch.randn(MAX_GROUPS, hidden) * 0.02)\n        self.queries = nn.Parameter(torch.randn(len(TARGETS), hidden) * 0.02)\n        self.classifier = nn.Parameter(torch.randn(len(TARGETS), hidden) * 0.02)\n        self.bias = nn.Parameter(torch.zeros(len(TARGETS)))\n        focal_prior = torch.tensor([0.45, 0.42, 0.52, 0.52, 0.18, 0.18, 0.20, 0.12, 0.20, 0.38, 0.65, 0.72])\n        self.focal_logit = nn.Parameter(torch.logit(focal_prior.clamp(0.02, 0.98)))\n        self.temperature_raw = nn.Parameter(torch.full((len(TARGETS),), 0.25))\n        self.dropout = nn.Dropout(0.18)\n\n    def group_evidence(self, feature, mask, group_ids):\n        # feature [B, G, S, D]\n        hidden = self.project(feature)\n        hidden = hidden + self.slot_embedding.view(1, 1, N_SLOTS, self.hidden)\n        hidden = hidden + self.group_embedding[group_ids].view(1, len(group_ids), 1, self.hidden)\n        attention = torch.einsum(\"bgsh,th->bgts\", hidden, self.queries) / math.sqrt(self.hidden)\n        attention = attention.masked_fill(mask[:, None, None, :] < 0.5, -1e4).softmax(-1)\n        context = torch.einsum(\"bgts,bgsh->bgth\", attention, hidden)\n        context = self.dropout(context)\n        return (context * self.classifier.view(1, 1, len(TARGETS), self.hidden)).sum(-1) + self.bias\n\n    def pool(self, evidence):\n        mean = evidence.mean(1)\n        temperature = F.softplus(self.temperature_raw).view(1, 1, -1) + 0.25\n        weights = (evidence * temperature).softmax(1)\n        focused = (weights * evidence).sum(1)\n        gate = torch.sigmoid(self.focal_logit).view(1, -1)\n        return mean + gate * (focused - mean)\n\n\nclass KneeEvidenceModel(nn.Module):\n    def __init__(self, backbone, hidden_size):\n        super().__init__()\n        self.backbone = backbone\n        self.router = EvidenceRouter(hidden_size * 2)\n        self.register_buffer(\"mean\", torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1))\n        self.register_buffer(\"std\", torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1))\n\n    def forward(self, images, mask, group_ids, image_size):\n        # images [B, G, S, 3, H, W]\n        batch, groups, slots = images.shape[:3]\n        flat = images.reshape(batch * groups * slots, *images.shape[3:]).float().div_(255.0)\n        if flat.shape[-1] != image_size:\n            flat = F.interpolate(flat, (image_size, image_size), mode=\"bilinear\", align_corners=False)\n        flat = (flat - self.mean) / self.std\n        tokens = self.backbone(pixel_values=flat).last_hidden_state\n        feature = torch.cat([tokens[:, 0], tokens[:, 1:].mean(1)], 1)\n        feature = feature.reshape(batch, groups, slots, -1)\n        evidence = self.router.group_evidence(feature, mask, group_ids)\n        return self.router.pool(evidence)\n\n\ndef build_model(variant, unfreeze=6):\n    from transformers import AutoModel\n    checkpoint = locate_backbone(variant)\n    backbone = AutoModel.from_pretrained(str(checkpoint), local_files_only=True)\n    for parameter in backbone.parameters():\n        parameter.requires_grad = False\n    layers = backbone.encoder.layer\n    for layer in layers[max(0, len(layers) - unfreeze):]:\n        for parameter in layer.parameters():\n            parameter.requires_grad = True\n    for parameter in backbone.layernorm.parameters():\n        parameter.requires_grad = True\n    model = KneeEvidenceModel(backbone, int(backbone.config.hidden_size))\n    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\n    log(f\"backbone={variant} path={checkpoint} trainable={trainable / 1e6:.1f}M\")\n    return model\n\n\ndef group_tensor(cache, rows, group_ids):\n    chunks = [cache[rows, :, group * GROUP_SIZE:(group + 1) * GROUP_SIZE] for group in group_ids]\n    return torch.from_numpy(np.stack(chunks, axis=1)).to(DEVICE)\n\n\ndef active_group_ids(count):\n    count = max(1, min(int(count), N_GROUPS))\n    return np.unique(np.round(np.linspace(0, N_GROUPS - 1, count)).astype(int)).tolist()\n\n\ndef augment_bag(images):\n    if torch.rand(1).item() < 0.5:\n        images = torch.flip(images, [-2])\n    scale = 1.0 + float(torch.rand(1).item() - 0.5) * 0.18\n    return (images.float() * scale).clamp(0, 255).to(torch.uint8)\n\n\n@torch.inference_mode()\ndef predict(model, cache, mask, indices, groups, image_size, batch_size=4):\n    model.eval()\n    output = []\n    group_ids = active_group_ids(groups)\n    for start in range(0, len(indices), batch_size):\n        rows = indices[start:start + batch_size]\n        mask_tensor = torch.from_numpy(mask[rows]).to(DEVICE)\n        evidence = []\n        for group in group_ids:\n            images = group_tensor(cache, rows, [group])\n            with torch.autocast(\"cuda\", dtype=torch.float16, enabled=DEVICE.type == \"cuda\"):\n                batch, _, slots = images.shape[:3]\n                flat = images.reshape(batch * slots, *images.shape[3:]).float().div_(255.0)\n                if flat.shape[-1] != image_size:\n                    flat = F.interpolate(flat, (image_size, image_size), mode=\"bilinear\", align_corners=False)\n                flat = (flat - model.mean) / model.std\n                tokens = model.backbone(pixel_values=flat).last_hidden_state\n                features = torch.cat([tokens[:, 0], tokens[:, 1:].mean(1)], 1).reshape(batch, 1, slots, -1)\n                evidence.append(model.router.group_evidence(features, mask_tensor, [group]).float())\n        logits = model.router.pool(torch.cat(evidence, 1))\n        output.append(torch.sigmoid(logits).cpu().numpy())\n    return np.concatenate(output) if output else np.empty((0, len(TARGETS)), np.float32)\n\n\ndef macro_auc(y_true, prediction):\n    scores = []\n    for target in range(y_true.shape[1]):\n        y = y_true[:, target]\n        scores.append(roc_auc_score(y, prediction[:, target]) if len(np.unique(y)) > 1 else np.nan)\n    return float(np.nanmean(scores)), np.asarray(scores)\n\n\nrow_lookup = {str(uid): row for row, uid in enumerate(TRAIN_FRAME[\"StudyInstanceUID\"].astype(str))}\nY = np.zeros((len(TRAIN_STUDIES), len(TARGETS)), np.float32)\nW = np.zeros_like(Y)\nreports_by_id = TRAIN_FRAME.set_index(\"StudyInstanceUID\")[\"Report\"].fillna(\"\")\nfor row, study in enumerate(TRAIN_STUDIES):\n    if study in GOLD.index:\n        Y[row] = GOLD.loc[study].values\n        W[row] = 4.0\n    else:\n        derived = REPORT_ROWS.loc[study]\n        Y[row] = derived[TARGETS].values\n        W[row] = 0.20 + 0.80 * derived[[target + \"__conf\" for target in TARGETS]].values\n\nfolds = np.array([\n    int(hashlib.sha1(str(reports_by_id.get(study, study)).encode(\"utf-8\", errors=\"ignore\")).hexdigest()[:8], 16) % 5\n    for study in TRAIN_STUDIES\n])\npilot_valid = np.where(folds == 0)[0]\npilot_train = np.where(folds != 0)[0]\nall_train = np.arange(len(TRAIN_STUDIES))\nlog(f\"targets train={len(all_train)} pilot={len(pilot_train)}/{len(pilot_valid)}\")\n\n\ndef train_network(config, indices, epochs, validation=None):\n    seed = int(config[\"seed\"])\n    torch.manual_seed(seed)\n    np.random.seed(seed)\n    group_ids_available = active_group_ids(config[\"groups\"])\n    groups = len(group_ids_available)\n    batch_size = int(config[\"batch\"])\n    model = build_model(config[\"variant\"]).to(DEVICE)\n    optimizer = torch.optim.AdamW([\n        {\"params\": [p for p in model.backbone.parameters() if p.requires_grad], \"lr\": config[\"lr\"]},\n        {\"params\": model.router.parameters(), \"lr\": 8e-4},\n    ], weight_decay=0.025)\n    steps_per_epoch = max(1, len(indices) // batch_size)\n    scheduler = torch.optim.lr_scheduler.OneCycleLR(\n        optimizer, max_lr=[config[\"lr\"], 8e-4], epochs=epochs,\n        steps_per_epoch=steps_per_epoch, pct_start=0.15,\n    )\n    scaler = torch.amp.GradScaler(\"cuda\", enabled=DEVICE.type == \"cuda\")\n    best_epoch, best_score, best_state = epochs, -np.inf, None\n    late_states = []\n\n    for epoch in range(epochs):\n        model.train()\n        permutation = np.random.permutation(indices)\n        losses = []\n        for start in range(0, len(permutation) - batch_size + 1, batch_size):\n            rows = permutation[start:start + batch_size]\n            count = min(TRAIN_GROUPS, groups)\n            group_ids = sorted(np.random.choice(group_ids_available, size=count, replace=False).tolist())\n            images = augment_bag(group_tensor(TRAIN_CACHE, rows, group_ids))\n            mask_tensor = torch.from_numpy(TRAIN_MASK[rows]).to(DEVICE)\n            target = torch.from_numpy(Y[rows]).to(DEVICE)\n            weight = torch.from_numpy(W[rows]).to(DEVICE)\n            optimizer.zero_grad(set_to_none=True)\n            with torch.autocast(\"cuda\", dtype=torch.float16, enabled=DEVICE.type == \"cuda\"):\n                logits = model(images, mask_tensor, group_ids, CACHE_IMAGE)\n                loss = (F.binary_cross_entropy_with_logits(logits, target, reduction=\"none\") * weight).sum() / weight.sum().clamp_min(1.0)\n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_([p for p in model.parameters() if p.requires_grad], 2.0)\n            scaler.step(optimizer)\n            scaler.update()\n            scheduler.step()\n            losses.append(float(loss.detach().cpu()))\n            if time.time() - T0 > TIME_LIMIT:\n                break\n\n        score = np.nan\n        if validation is not None:\n            val_prediction = predict(model, TRAIN_CACHE, TRAIN_MASK, validation, groups, CACHE_IMAGE)\n            score, _ = macro_auc((Y[validation] > 0.5).astype(int), val_prediction)\n            if score > best_score:\n                best_score, best_epoch = score, epoch + 1\n                best_state = {key: value.detach().cpu().clone() for key, value in model.state_dict().items()}\n        elif epoch + 1 >= max(1, epochs - 1):\n            late_states.append({key: value.detach().cpu().clone() for key, value in model.state_dict().items()})\n        log(f\"  {config['name']} epoch={epoch + 1}/{epochs} loss={np.mean(losses):.4f} val={score:.4f}\")\n        if time.time() - T0 > TIME_LIMIT:\n            break\n\n    if validation is not None and best_state is not None:\n        model.load_state_dict(best_state)\n    elif late_states:\n        averaged = {}\n        for key in late_states[0]:\n            if late_states[0][key].is_floating_point():\n                averaged[key] = torch.stack([state[key].float() for state in late_states]).mean(0)\n            else:\n                averaged[key] = late_states[-1][key]\n        model.load_state_dict(averaged)\n    return model, best_epoch, best_score\n\n\n# Pilot chooses only a training duration. Its weights are never submitted.\npilot_config = {\"name\": \"pilot_small\", \"variant\": \"small\", \"groups\": 8, \"seed\": SEED, \"batch\": 6, \"lr\": 8e-6}\nif SMOKE:\n    PILOT_EPOCHS = 1\n    FINAL_MODELS = FINAL_MODELS[:1]\npilot_model, selected_epochs, pilot_score = train_network(\n    pilot_config, pilot_train, PILOT_EPOCHS, pilot_valid\n)\nselected_epochs = int(np.clip(selected_epochs, 6 if not SMOKE else 1, PILOT_EPOCHS))\nlog(f\"pilot selected epochs={selected_epochs} derived_holdout={pilot_score:.4f}\")\ndel pilot_model\ngc.collect()\nif DEVICE.type == \"cuda\":\n    torch.cuda.empty_cache()\n\n\ntest_predictions = []\ncompleted = []\nfor config in FINAL_MODELS:\n    if time.time() - T0 > TIME_LIMIT - 900:\n        log(f\"skip {config['name']}: time reserve\")\n        continue\n    try:\n        model, _, _ = train_network(config, all_train, selected_epochs, validation=None)\n        prediction = predict(\n            model, TEST_CACHE, TEST_MASK, np.arange(len(TEST_STUDIES)),\n            min(config[\"groups\"], N_GROUPS), CACHE_IMAGE,\n        )\n        test_predictions.append(prediction)\n        completed.append(config[\"name\"])\n        single = pd.DataFrame(prediction, columns=TARGETS)\n        single.insert(0, \"StudyInstanceUID\", TEST_STUDIES)\n        single = TEST_FRAME[[\"StudyInstanceUID\"]].merge(single, on=\"StudyInstanceUID\", how=\"left\")\n        single[TARGETS] = single[TARGETS].fillna(0.5)\n        save_submission(single, f\"submission_{config['name']}.csv\")\n        del model\n        gc.collect()\n        if DEVICE.type == \"cuda\":\n            torch.cuda.empty_cache()\n    except Exception:\n        traceback.print_exc()\n        log(f\"model {config['name']} failed; continuing\")\n\nif not test_predictions:\n    raise RuntimeError(\"no final model completed; fallback submission remains\")\n\nranked = [pd.DataFrame(prediction).rank(axis=0, pct=True).values for prediction in test_predictions]\nensemble = np.mean(ranked, axis=0)\nsubmission = pd.DataFrame(ensemble, columns=TARGETS)\nsubmission.insert(0, \"StudyInstanceUID\", TEST_STUDIES)\nsubmission = TEST_FRAME[[\"StudyInstanceUID\"]].merge(submission, on=\"StudyInstanceUID\", how=\"left\")\nsubmission[TARGETS] = submission[TARGETS].fillna(0.5)\nsubmission = save_submission(submission)\nlog(f\"submission.csv={submission.shape} models={completed} nulls={submission[TARGETS].isna().sum().sum()}\")\nprint(submission.head().to_string(index=False))\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-08T12:10:02.622Z"}},"outputs":[],"execution_count":null}]}