Spaces:
Sleeping
Sleeping
Update spine_coder/spine_coder_core.py
Browse files- spine_coder/spine_coder_core.py +83 -47
spine_coder/spine_coder_core.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# spine_coder_core_pro.py
|
| 2 |
-
# Vertebro FINAL-v2.2-PRO
|
| 3 |
# Core utilities, text normalization, level parsing, region classification
|
| 4 |
|
| 5 |
import re, json
|
|
@@ -18,17 +18,16 @@ def _norm(s: str) -> str:
|
|
| 18 |
def _has(text: str, pat: str) -> bool:
|
| 19 |
return re.search(pat, text, flags=re.I) is not None
|
| 20 |
|
| 21 |
-
|
| 22 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
# LEVEL AND REGION LOGIC
|
| 24 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 25 |
-
_ORDER
|
| 26 |
_MAXNUM = {"C": 7, "T": 12, "L": 5, "S": 5}
|
| 27 |
|
| 28 |
def _level_sort_key(lv: str) -> Tuple[int, int]:
|
| 29 |
band_rank = {"C": 100, "T": 200, "L": 300, "S": 400}
|
| 30 |
band = band_rank.get(lv[0].upper(), 999)
|
| 31 |
-
num
|
| 32 |
return (band, num)
|
| 33 |
|
| 34 |
_SPAN = re.compile(r"\b([CTLS])\s?(\d{1,2})\s*[-β]\s*([CTLS])?\s?(\d{1,2})\b", re.I)
|
|
@@ -36,7 +35,7 @@ _SINGLE = re.compile(r"\b([CTLS])\s?(\d{1,2})\b", re.I)
|
|
| 36 |
|
| 37 |
def _expand_across_regions(p1: str, n1: int, p2: str, n2: int) -> List[str]:
|
| 38 |
p1, p2 = p1.upper(), p2.upper()
|
| 39 |
-
out,
|
| 40 |
while True:
|
| 41 |
out.append(f"{_ORDER[r]}{num}")
|
| 42 |
if _ORDER[r] == p2 and num == n2:
|
|
@@ -68,8 +67,7 @@ def _extract_levels(t: str) -> List[str]:
|
|
| 68 |
return sorted(levels, key=_level_sort_key)
|
| 69 |
|
| 70 |
def _count_interspaces(levels: List[str]) -> int:
|
| 71 |
-
if not levels:
|
| 72 |
-
return 0
|
| 73 |
lv_sorted = sorted(set(levels), key=_level_sort_key)
|
| 74 |
return max(0, len(lv_sorted) - 1)
|
| 75 |
|
|
@@ -87,9 +85,9 @@ def _classify_region(levels: List[str]) -> str:
|
|
| 87 |
if s and not (c or t or l): return "sacral"
|
| 88 |
return "mixed"
|
| 89 |
|
| 90 |
-
# laterality/modifiers stub (
|
| 91 |
def _laterality_and_modifiers(note: str) -> Tuple[str, List[str]]:
|
| 92 |
-
return "midline", []
|
| 93 |
|
| 94 |
# Base keyword groups
|
| 95 |
FUSION_KW = r"\b(arthrodesis|fusion|t?lif|alif|plif|xlif|interbody\s+cage|peek\s+cage|structural\s+cage)\b"
|
|
@@ -100,8 +98,8 @@ AUTO_LOCAL_KW = r"\b(local\s+autograft|spinous\s+process\s+bone|lamina\s+bone\
|
|
| 100 |
AUTO_SEP_KW = r"\b(iliac\s+crest|separate\s+incision|rib\s+graft|iliac crest bone|icbg)\b"
|
| 101 |
|
| 102 |
# spine_coder_core_pro.py
|
| 103 |
-
# Vertebro FINAL-v2.2-PRO
|
| 104 |
-
# CPT reasoning engine
|
| 105 |
|
| 106 |
def _inst_code_by_span(span: int, anterior: bool) -> str:
|
| 107 |
if span <= 1: return ""
|
|
@@ -109,6 +107,17 @@ def _inst_code_by_span(span: int, anterior: bool) -> str:
|
|
| 109 |
return "22845" if span <= 3 else ("22846" if span <= 7 else "22847")
|
| 110 |
return "22842" if span <= 3 else ("22843" if span <= 7 else "22844")
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any]]:
|
| 113 |
t = _norm(note)
|
| 114 |
out: List[Dict[str, Any]] = []
|
|
@@ -116,13 +125,21 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 116 |
span = max(inters + 1, 2)
|
| 117 |
|
| 118 |
def add(cpt: str, desc: str, rationale: str, cat: str, conf: float = 0.85, primary: bool = False):
|
| 119 |
-
out.append({
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
# Washout/I&D guard β prevents false fusion/instrumentation
|
| 123 |
washout_only = _has(t, r"(washout|irrigation and debridement|i\&d)") and not _has(t, FUSION_KW)
|
| 124 |
|
| 125 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
if _has(t, r"laminectomy|decompression|facetectomy|foraminotomy"):
|
| 127 |
base_map = {"cervical": "63045", "thoracic": "63046", "lumbar": "63047"}
|
| 128 |
base = base_map.get(region, "63047")
|
|
@@ -132,12 +149,12 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 132 |
add("63048", f"Each additional level Γ{inters}",
|
| 133 |
"Multi-level decompression inferred.", "decompression add-on", 0.82)
|
| 134 |
|
| 135 |
-
#
|
| 136 |
tlif_like = (
|
| 137 |
-
_has(t, r"\btlif\b|\bplif\b|posterior interbody fusion")
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
)
|
| 142 |
if tlif_like and not washout_only and region in {"lumbar", "thoracic"}:
|
| 143 |
add("22633", "Posterior/posterolateral + posterior interbody, single level",
|
|
@@ -152,7 +169,7 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 152 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 153 |
add(code, desc, "Posterior instrumentation detected.", "instrumentation", 0.83)
|
| 154 |
|
| 155 |
-
#
|
| 156 |
if _has(t, r"\balif\b|anterior lumbar interbody fusion") and not washout_only:
|
| 157 |
add("22558", "Anterior lumbar interbody fusion, single interspace",
|
| 158 |
"ALIF detected.", "ALIF", 0.9, True)
|
|
@@ -167,7 +184,7 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 167 |
"22847":"Anterior instrumentation (8+ segments)"}[code]
|
| 168 |
add(code, desc, "Anterior plate present; span estimated from levels.", "instrumentation", 0.8)
|
| 169 |
|
| 170 |
-
#
|
| 171 |
if _has(t, r"posterolateral\b.*\bfusion|posterior\b.*\bfusion|in situ\b.*\bfusion") \
|
| 172 |
and not _has(t, r"\btlif\b|\bplif\b|posterior interbody") and not washout_only:
|
| 173 |
base_map = {"cervical":"22600", "thoracic":"22610", "lumbar":"22612", "lumbosacral":"22612", "cervicothoracic":"22600"}
|
|
@@ -185,7 +202,7 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 185 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 186 |
add(code, desc, "Posterior instrumentation detected.", "instrumentation", 0.8)
|
| 187 |
|
| 188 |
-
#
|
| 189 |
if _has(t, INSTR_KW) and not washout_only:
|
| 190 |
code = _inst_code_by_span(span, anterior=False)
|
| 191 |
if code and not any(r["category"] == "instrumentation" for r in out):
|
|
@@ -194,12 +211,12 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 194 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 195 |
add(code, desc, "Posterior instrumentation documented.", "instrumentation", 0.82)
|
| 196 |
|
| 197 |
-
#
|
| 198 |
if _has(t, NAV_KW):
|
| 199 |
add("61783", "Intraoperative navigation (image-guided)",
|
| 200 |
"Navigation terms detected (O-arm/3D spin/Stealth/7D).", "navigation", 0.82)
|
| 201 |
|
| 202 |
-
#
|
| 203 |
if _has(t, AUTO_SEP_KW):
|
| 204 |
add("20937", "Autograft (separate incision)", "Iliac crest or separate-site autograft.", "graft", 0.8)
|
| 205 |
elif _has(t, AUTO_LOCAL_KW):
|
|
@@ -207,7 +224,7 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 207 |
if _has(t, ALLO_KW):
|
| 208 |
add("20930", "Allograft, morselized / DBM", "Allograft/DBM used.", "graft", 0.8)
|
| 209 |
|
| 210 |
-
#
|
| 211 |
if _has(t, r"(remov(ed|al)|explant).*(instrument|hardware|plate|rod|screw)"):
|
| 212 |
if _has(t, r"\banterior\b|acdf|plate"):
|
| 213 |
add("22855", "Removal of anterior instrumentation", "Anterior hardware removal.", "hardware_removal", 0.84, True)
|
|
@@ -216,13 +233,19 @@ def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any
|
|
| 216 |
|
| 217 |
return out
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
def _apply_specialty_packs(note: str, region: str, inters: int, levels: List[str]) -> List[Dict[str, Any]]:
|
| 220 |
t = _norm(note)
|
| 221 |
extra: List[Dict[str, Any]] = []
|
| 222 |
|
| 223 |
def add(cpt, desc, rationale, cat, conf=0.84, primary=False):
|
| 224 |
-
extra.append({
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
|
| 227 |
# Tumor / corpectomy
|
| 228 |
if _has(t, r"corpectomy|tumou?r|metastatic|metastasis|en bloc"):
|
|
@@ -264,7 +287,7 @@ def _apply_specialty_packs(note: str, region: str, inters: int, levels: List[str
|
|
| 264 |
return extra
|
| 265 |
|
| 266 |
# spine_coder_core_pro.py
|
| 267 |
-
# Vertebro FINAL-v2.2-PRO
|
| 268 |
# Case-level modifiers & complications detector
|
| 269 |
|
| 270 |
def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str, str]], Dict[str, bool], List[str]]:
|
|
@@ -273,36 +296,44 @@ def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str,
|
|
| 273 |
complications_map: Dict[str, bool] = {}
|
| 274 |
complications_list: List[str] = []
|
| 275 |
|
| 276 |
-
#
|
| 277 |
mod_patterns = [
|
| 278 |
("22", r"(complex|technically (difficult|demanding)|difficult dissection|extensive adhesiolysis|"
|
| 279 |
r"severe deformity|morbid obesity|revision (case|exposure)|re-?operative field|"
|
| 280 |
r"dense scar tissue|prolonged (exposure|procedure)|ossified p(?:l|ll)\b)",
|
| 281 |
"Increased procedural service (complexity)."),
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
("53", r"\b(aborted|terminated|discontinued|stopped prior to completion)\b",
|
| 284 |
"Procedure discontinued for patient safety."),
|
|
|
|
| 285 |
("62", r"\b(co[- ]?surgeon|two surgeons|co-surgeons)\b",
|
| 286 |
"Two surgeons (co-surgeons) documented."),
|
|
|
|
| 287 |
("76", r"\brepeat(ed)? procedure\b.*\b(same (surgeon|physician))\b",
|
| 288 |
"Repeat procedure/service by the same physician."),
|
| 289 |
("77", r"\brepeat(ed)? procedure\b.*\b(another|different) (surgeon|physician)\b",
|
| 290 |
"Repeat procedure by another physician."),
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
| 293 |
"Unplanned return to OR during postoperative period."),
|
| 294 |
-
|
|
|
|
| 295 |
"Unrelated procedure during postoperative period."),
|
| 296 |
-
# -50, -59 suppressed to avoid noise in spine
|
| 297 |
]
|
| 298 |
for code, pat, reason in mod_patterns:
|
| 299 |
if _has(t, pat):
|
| 300 |
modifiers.append({"modifier": code, "reason": reason})
|
| 301 |
|
| 302 |
-
# Assistant logic: AS vs -80 (mutually exclusive); -82 supersedes -80
|
| 303 |
has_pa_np = _has(t, r"\b(pa[- ]?c|physician assistant|pa-c|nurse practitioner|np|advanced practice provider|app)\b")
|
| 304 |
has_md_do = _has(t, r"\b(dr\.?|m\.?d\.?|d\.?o\.?)\b") or _has(t, r"\bassistant surgeon\b")
|
| 305 |
-
if _has(t, r"assistant\(s\):") or _has(t, r"\bassistant(s)?[:\-]"):
|
| 306 |
has_md_do = True
|
| 307 |
if has_pa_np:
|
| 308 |
modifiers.append({"modifier": "AS", "reason": "Non-physician assistant at surgery."})
|
|
@@ -315,7 +346,12 @@ def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str,
|
|
| 315 |
if any(m["modifier"] == "53" for m in modifiers):
|
| 316 |
modifiers = [m for m in modifiers if m["modifier"] != "52"]
|
| 317 |
|
| 318 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
comp_rules = [
|
| 320 |
("dural_tear_or_durotomy", r"\b(dural tear|durotom(y|ies)|csf leak|cerebrospinal fluid leak)\b"),
|
| 321 |
("neuromonitoring_change", r"(loss|significant (decrease|change)).*(ssep|mep|meps|tcem|neuromonitor)"),
|
|
@@ -329,7 +365,7 @@ def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str,
|
|
| 329 |
("positioning_injury", r"\b(pressure (injury|ulcer)|brachial plexus|ulnar neuropathy|peroneal neuropathy)\b"),
|
| 330 |
("transfusion", r"\btransfus(ed|ion)|prbc\b|\bcell saver\b"),
|
| 331 |
("massive_blood_loss", r"\bebl\b.*\b(> ?800\s?ml|>\s?1(\.|,)?0?00\s?ml|> ?1\s?l|> ?1000\s?cc)\b"),
|
| 332 |
-
("unplanned_return_to_or", r"\
|
| 333 |
("wound_dehiscence", r"\bwound dehiscence\b|\bdehisced\b"),
|
| 334 |
]
|
| 335 |
for key, pat in comp_rules:
|
|
@@ -338,7 +374,7 @@ def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str,
|
|
| 338 |
if hit:
|
| 339 |
complications_list.append(key.replace("_", " ").title())
|
| 340 |
|
| 341 |
-
# Negation in IONM
|
| 342 |
if _has(t, r"no significant changes in (motor|sensory) evoked potentials|neuromonitoring.*no (significant )?changes"):
|
| 343 |
complications_map["neuromonitoring_change"] = False
|
| 344 |
complications_list = [c for c in complications_list if c != "Neuromonitoring Change"]
|
|
@@ -346,7 +382,7 @@ def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str,
|
|
| 346 |
return modifiers, complications_map, complications_list
|
| 347 |
|
| 348 |
# spine_coder_core_pro.py
|
| 349 |
-
# Vertebro FINAL-v2.2-PRO
|
| 350 |
# Output builder & main entrypoint with top_k + backward-compat alias
|
| 351 |
|
| 352 |
def vertebro_infer(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[str, Any]:
|
|
@@ -354,7 +390,7 @@ def vertebro_infer(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[
|
|
| 354 |
t = _norm(note)
|
| 355 |
levels = _extract_levels(t)
|
| 356 |
region = _classify_region(levels)
|
| 357 |
-
inters
|
| 358 |
laterality, mods_stub = _laterality_and_modifiers(t)
|
| 359 |
|
| 360 |
# Inference
|
|
@@ -362,7 +398,7 @@ def vertebro_infer(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[
|
|
| 362 |
extra_rows = _apply_specialty_packs(t, region, inters, levels)
|
| 363 |
rows = base_rows + extra_rows
|
| 364 |
|
| 365 |
-
#
|
| 366 |
merged: Dict[tuple, Dict[str, Any]] = {}
|
| 367 |
for r in rows:
|
| 368 |
key = (r["cpt"], r["category"])
|
|
@@ -383,7 +419,7 @@ def vertebro_infer(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[
|
|
| 383 |
}
|
| 384 |
tech_flags = [k for k, v in flags_map.items() if v]
|
| 385 |
|
| 386 |
-
# Attach tech info to primary
|
| 387 |
if tech_flags:
|
| 388 |
tech_txt = f" (Tech: {', '.join(tech_flags)})"
|
| 389 |
for r in rows:
|
|
@@ -406,7 +442,7 @@ def vertebro_infer(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[
|
|
| 406 |
if r.get("modifiers"):
|
| 407 |
r["modifiers"] = [m for m in r["modifiers"] if m != "53"]
|
| 408 |
|
| 409 |
-
# Top-K cap
|
| 410 |
try:
|
| 411 |
k = int(top_k)
|
| 412 |
if k > 0:
|
|
@@ -436,12 +472,12 @@ suggest_with_cpt_billing = vertebro_infer
|
|
| 436 |
# Quick CLI test
|
| 437 |
if __name__ == "__main__":
|
| 438 |
sample = """
|
| 439 |
-
Assistant(s): Dr. Amber Parker
|
| 440 |
Preop Dx: C3βC6 stenosis with myelopathy.
|
| 441 |
Procedure:
|
| 442 |
1. C3βC6 laminectomy and medial facetectomies.
|
| 443 |
2. C2βT1 posterolateral arthrodesis; posterior instrumentation C2βT1.
|
| 444 |
3. Navigation and fluoroscopy utilized; neuromonitoring with no significant changes.
|
| 445 |
-
EBL 1200 mL. Dural tear identified and repaired primarily.
|
| 446 |
"""
|
| 447 |
print(json.dumps(vertebro_infer(sample, top_k=12), indent=2))
|
|
|
|
| 1 |
# spine_coder_core_pro.py
|
| 2 |
+
# Vertebro FINAL-v2.2-PRO (Block 1/5)
|
| 3 |
# Core utilities, text normalization, level parsing, region classification
|
| 4 |
|
| 5 |
import re, json
|
|
|
|
| 18 |
def _has(text: str, pat: str) -> bool:
|
| 19 |
return re.search(pat, text, flags=re.I) is not None
|
| 20 |
|
|
|
|
| 21 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
# LEVEL AND REGION LOGIC
|
| 23 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
_ORDER = ["C", "T", "L", "S"]
|
| 25 |
_MAXNUM = {"C": 7, "T": 12, "L": 5, "S": 5}
|
| 26 |
|
| 27 |
def _level_sort_key(lv: str) -> Tuple[int, int]:
|
| 28 |
band_rank = {"C": 100, "T": 200, "L": 300, "S": 400}
|
| 29 |
band = band_rank.get(lv[0].upper(), 999)
|
| 30 |
+
num = int(re.sub(r"\D", "", lv) or 0)
|
| 31 |
return (band, num)
|
| 32 |
|
| 33 |
_SPAN = re.compile(r"\b([CTLS])\s?(\d{1,2})\s*[-β]\s*([CTLS])?\s?(\d{1,2})\b", re.I)
|
|
|
|
| 35 |
|
| 36 |
def _expand_across_regions(p1: str, n1: int, p2: str, n2: int) -> List[str]:
|
| 37 |
p1, p2 = p1.upper(), p2.upper()
|
| 38 |
+
out, r, num = [], _ORDER.index(p1), n1
|
| 39 |
while True:
|
| 40 |
out.append(f"{_ORDER[r]}{num}")
|
| 41 |
if _ORDER[r] == p2 and num == n2:
|
|
|
|
| 67 |
return sorted(levels, key=_level_sort_key)
|
| 68 |
|
| 69 |
def _count_interspaces(levels: List[str]) -> int:
|
| 70 |
+
if not levels: return 0
|
|
|
|
| 71 |
lv_sorted = sorted(set(levels), key=_level_sort_key)
|
| 72 |
return max(0, len(lv_sorted) - 1)
|
| 73 |
|
|
|
|
| 85 |
if s and not (c or t or l): return "sacral"
|
| 86 |
return "mixed"
|
| 87 |
|
| 88 |
+
# laterality/modifiers stub (real modifiers are detected case-level)
|
| 89 |
def _laterality_and_modifiers(note: str) -> Tuple[str, List[str]]:
|
| 90 |
+
return "midline", []
|
| 91 |
|
| 92 |
# Base keyword groups
|
| 93 |
FUSION_KW = r"\b(arthrodesis|fusion|t?lif|alif|plif|xlif|interbody\s+cage|peek\s+cage|structural\s+cage)\b"
|
|
|
|
| 98 |
AUTO_SEP_KW = r"\b(iliac\s+crest|separate\s+incision|rib\s+graft|iliac crest bone|icbg)\b"
|
| 99 |
|
| 100 |
# spine_coder_core_pro.py
|
| 101 |
+
# Vertebro FINAL-v2.2-PRO (Block 2/5)
|
| 102 |
+
# CPT reasoning engine (decompression, TLIF/PLIF, ALIF, fusion, instrumentation, grafts, nav, removal)
|
| 103 |
|
| 104 |
def _inst_code_by_span(span: int, anterior: bool) -> str:
|
| 105 |
if span <= 1: return ""
|
|
|
|
| 107 |
return "22845" if span <= 3 else ("22846" if span <= 7 else "22847")
|
| 108 |
return "22842" if span <= 3 else ("22843" if span <= 7 else "22844")
|
| 109 |
|
| 110 |
+
def _infer_id_depth_code(t: str) -> str:
|
| 111 |
+
"""
|
| 112 |
+
Heuristic for I&D depth CPT:
|
| 113 |
+
- 11044: bone
|
| 114 |
+
- 11043: fascia/muscle
|
| 115 |
+
- 11042: skin/subcut or unspecified
|
| 116 |
+
"""
|
| 117 |
+
if _has(t, r"\b(bone|osteomyelitis|to bone|down to bone)\b"): return "11044"
|
| 118 |
+
if _has(t, r"\b(fascia|fascial|muscle|muscular|deep fascial)\b"): return "11043"
|
| 119 |
+
return "11042"
|
| 120 |
+
|
| 121 |
def _infer_cpts(note: str, region: str, levels: List[str]) -> List[Dict[str, Any]]:
|
| 122 |
t = _norm(note)
|
| 123 |
out: List[Dict[str, Any]] = []
|
|
|
|
| 125 |
span = max(inters + 1, 2)
|
| 126 |
|
| 127 |
def add(cpt: str, desc: str, rationale: str, cat: str, conf: float = 0.85, primary: bool = False):
|
| 128 |
+
out.append({
|
| 129 |
+
"cpt": cpt, "desc": desc, "rationale": rationale, "category": cat,
|
| 130 |
+
"confidence": round(conf, 2), "primary": primary
|
| 131 |
+
})
|
| 132 |
|
| 133 |
+
# Washout/I&D guard β prevents false fusion/instrumentation on washout only
|
| 134 |
washout_only = _has(t, r"(washout|irrigation and debridement|i\&d)") and not _has(t, FUSION_KW)
|
| 135 |
|
| 136 |
+
# (A) I&D / Washout CPTs (always add if present)
|
| 137 |
+
if _has(t, r"\b(washout|irrigation and debridement|i\&d|incision and debridement|debridement)\b"):
|
| 138 |
+
id_code = _infer_id_depth_code(t)
|
| 139 |
+
add(id_code, "Irrigation & Debridement (depth-based)",
|
| 140 |
+
"Infection/washout documented.", "washout", 0.86, True)
|
| 141 |
+
|
| 142 |
+
# (B) Decompression
|
| 143 |
if _has(t, r"laminectomy|decompression|facetectomy|foraminotomy"):
|
| 144 |
base_map = {"cervical": "63045", "thoracic": "63046", "lumbar": "63047"}
|
| 145 |
base = base_map.get(region, "63047")
|
|
|
|
| 149 |
add("63048", f"Each additional level Γ{inters}",
|
| 150 |
"Multi-level decompression inferred.", "decompression add-on", 0.82)
|
| 151 |
|
| 152 |
+
# (C) TLIF/PLIF (explicit or implicit)
|
| 153 |
tlif_like = (
|
| 154 |
+
_has(t, r"\btlif\b|\bplif\b|posterior interbody fusion") or
|
| 155 |
+
(_has(t, r"\bfacetectom(y|ies)\b|complete facetectomy|transforaminal") and
|
| 156 |
+
_has(t, r"\b(interbody (cage|device|spacer)|peek (cage|spacer)|titanium (cage|spacer)|allograft spacer)\b") and
|
| 157 |
+
_has(t, r"\bpedicle\s+screws?\b"))
|
| 158 |
)
|
| 159 |
if tlif_like and not washout_only and region in {"lumbar", "thoracic"}:
|
| 160 |
add("22633", "Posterior/posterolateral + posterior interbody, single level",
|
|
|
|
| 169 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 170 |
add(code, desc, "Posterior instrumentation detected.", "instrumentation", 0.83)
|
| 171 |
|
| 172 |
+
# (D) ALIF
|
| 173 |
if _has(t, r"\balif\b|anterior lumbar interbody fusion") and not washout_only:
|
| 174 |
add("22558", "Anterior lumbar interbody fusion, single interspace",
|
| 175 |
"ALIF detected.", "ALIF", 0.9, True)
|
|
|
|
| 184 |
"22847":"Anterior instrumentation (8+ segments)"}[code]
|
| 185 |
add(code, desc, "Anterior plate present; span estimated from levels.", "instrumentation", 0.8)
|
| 186 |
|
| 187 |
+
# (E) Posterolateral/posterior fusion (no interbody)
|
| 188 |
if _has(t, r"posterolateral\b.*\bfusion|posterior\b.*\bfusion|in situ\b.*\bfusion") \
|
| 189 |
and not _has(t, r"\btlif\b|\bplif\b|posterior interbody") and not washout_only:
|
| 190 |
base_map = {"cervical":"22600", "thoracic":"22610", "lumbar":"22612", "lumbosacral":"22612", "cervicothoracic":"22600"}
|
|
|
|
| 202 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 203 |
add(code, desc, "Posterior instrumentation detected.", "instrumentation", 0.8)
|
| 204 |
|
| 205 |
+
# (F) Instrumentation without explicit fusion (posterior)
|
| 206 |
if _has(t, INSTR_KW) and not washout_only:
|
| 207 |
code = _inst_code_by_span(span, anterior=False)
|
| 208 |
if code and not any(r["category"] == "instrumentation" for r in out):
|
|
|
|
| 211 |
"22844":"Posterior segmental instrumentation (8+ segments)"}[code]
|
| 212 |
add(code, desc, "Posterior instrumentation documented.", "instrumentation", 0.82)
|
| 213 |
|
| 214 |
+
# (G) Navigation
|
| 215 |
if _has(t, NAV_KW):
|
| 216 |
add("61783", "Intraoperative navigation (image-guided)",
|
| 217 |
"Navigation terms detected (O-arm/3D spin/Stealth/7D).", "navigation", 0.82)
|
| 218 |
|
| 219 |
+
# (H) Bone grafts
|
| 220 |
if _has(t, AUTO_SEP_KW):
|
| 221 |
add("20937", "Autograft (separate incision)", "Iliac crest or separate-site autograft.", "graft", 0.8)
|
| 222 |
elif _has(t, AUTO_LOCAL_KW):
|
|
|
|
| 224 |
if _has(t, ALLO_KW):
|
| 225 |
add("20930", "Allograft, morselized / DBM", "Allograft/DBM used.", "graft", 0.8)
|
| 226 |
|
| 227 |
+
# (I) Hardware removal
|
| 228 |
if _has(t, r"(remov(ed|al)|explant).*(instrument|hardware|plate|rod|screw)"):
|
| 229 |
if _has(t, r"\banterior\b|acdf|plate"):
|
| 230 |
add("22855", "Removal of anterior instrumentation", "Anterior hardware removal.", "hardware_removal", 0.84, True)
|
|
|
|
| 233 |
|
| 234 |
return out
|
| 235 |
|
| 236 |
+
# spine_coder_core_pro.py
|
| 237 |
+
# Vertebro FINAL-v2.2-PRO (Block 3/5)
|
| 238 |
+
# Specialty packs layered onto core CPT logic
|
| 239 |
+
|
| 240 |
def _apply_specialty_packs(note: str, region: str, inters: int, levels: List[str]) -> List[Dict[str, Any]]:
|
| 241 |
t = _norm(note)
|
| 242 |
extra: List[Dict[str, Any]] = []
|
| 243 |
|
| 244 |
def add(cpt, desc, rationale, cat, conf=0.84, primary=False):
|
| 245 |
+
extra.append({
|
| 246 |
+
"cpt": cpt, "desc": desc, "rationale": rationale, "category": cat,
|
| 247 |
+
"confidence": round(conf, 2), "primary": primary
|
| 248 |
+
})
|
| 249 |
|
| 250 |
# Tumor / corpectomy
|
| 251 |
if _has(t, r"corpectomy|tumou?r|metastatic|metastasis|en bloc"):
|
|
|
|
| 287 |
return extra
|
| 288 |
|
| 289 |
# spine_coder_core_pro.py
|
| 290 |
+
# Vertebro FINAL-v2.2-PRO (Block 4/5)
|
| 291 |
# Case-level modifiers & complications detector
|
| 292 |
|
| 293 |
def _detect_case_modifiers_and_complications(note: str) -> Tuple[List[Dict[str, str]], Dict[str, bool], List[str]]:
|
|
|
|
| 296 |
complications_map: Dict[str, bool] = {}
|
| 297 |
complications_list: List[str] = []
|
| 298 |
|
| 299 |
+
# Modifiers (-50 / -59 intentionally suppressed for spine)
|
| 300 |
mod_patterns = [
|
| 301 |
("22", r"(complex|technically (difficult|demanding)|difficult dissection|extensive adhesiolysis|"
|
| 302 |
r"severe deformity|morbid obesity|revision (case|exposure)|re-?operative field|"
|
| 303 |
r"dense scar tissue|prolonged (exposure|procedure)|ossified p(?:l|ll)\b)",
|
| 304 |
"Increased procedural service (complexity)."),
|
| 305 |
+
|
| 306 |
+
# 52 β reduced service (guarded)
|
| 307 |
+
("52", r"\b(partial|limited|reduced (service|extent))\b",
|
| 308 |
+
"Reduced service performed."),
|
| 309 |
+
|
| 310 |
("53", r"\b(aborted|terminated|discontinued|stopped prior to completion)\b",
|
| 311 |
"Procedure discontinued for patient safety."),
|
| 312 |
+
|
| 313 |
("62", r"\b(co[- ]?surgeon|two surgeons|co-surgeons)\b",
|
| 314 |
"Two surgeons (co-surgeons) documented."),
|
| 315 |
+
|
| 316 |
("76", r"\brepeat(ed)? procedure\b.*\b(same (surgeon|physician))\b",
|
| 317 |
"Repeat procedure/service by the same physician."),
|
| 318 |
("77", r"\brepeat(ed)? procedure\b.*\b(another|different) (surgeon|physician)\b",
|
| 319 |
"Repeat procedure by another physician."),
|
| 320 |
+
|
| 321 |
+
# 78 β broadened phrasing for Jason-style notes
|
| 322 |
+
("78", r"(\btake[- ]?back\b|\bunplanned return\b).*\b(operating|op)\s*room\b"
|
| 323 |
+
r"|(\breturn to (the )?(operating|op)\s*room\b.*\b(postop|post[- ]?operative|global period)\b)",
|
| 324 |
"Unplanned return to OR during postoperative period."),
|
| 325 |
+
|
| 326 |
+
("79", r"\bunrelated procedure\b.*\b(postop|post[- ]?operative|global period)\b",
|
| 327 |
"Unrelated procedure during postoperative period."),
|
|
|
|
| 328 |
]
|
| 329 |
for code, pat, reason in mod_patterns:
|
| 330 |
if _has(t, pat):
|
| 331 |
modifiers.append({"modifier": code, "reason": reason})
|
| 332 |
|
| 333 |
+
# Assistant logic: AS vs -80 (mutually exclusive); -82 supersedes -80; PA/NP beats -80
|
| 334 |
has_pa_np = _has(t, r"\b(pa[- ]?c|physician assistant|pa-c|nurse practitioner|np|advanced practice provider|app)\b")
|
| 335 |
has_md_do = _has(t, r"\b(dr\.?|m\.?d\.?|d\.?o\.?)\b") or _has(t, r"\bassistant surgeon\b")
|
| 336 |
+
if _has(t, r"assistant\(s\):") or _has(t, r"\bassistant(s)?[:\-]"): # generic assistant section β likely MD
|
| 337 |
has_md_do = True
|
| 338 |
if has_pa_np:
|
| 339 |
modifiers.append({"modifier": "AS", "reason": "Non-physician assistant at surgery."})
|
|
|
|
| 346 |
if any(m["modifier"] == "53" for m in modifiers):
|
| 347 |
modifiers = [m for m in modifiers if m["modifier"] != "52"]
|
| 348 |
|
| 349 |
+
# Corpectomy-specific rule: suppress -52 if βpartial decompressionβ inside a corpectomy case
|
| 350 |
+
if any(m["modifier"] == "52" for m in modifiers):
|
| 351 |
+
if _has(t, r"\bcorpectomy\b") and _has(t, r"\bpartial\b.*\b(decompression|laminectomy|facetectomy)\b"):
|
| 352 |
+
modifiers = [m for m in modifiers if m["modifier"] != "52"]
|
| 353 |
+
|
| 354 |
+
# Complications
|
| 355 |
comp_rules = [
|
| 356 |
("dural_tear_or_durotomy", r"\b(dural tear|durotom(y|ies)|csf leak|cerebrospinal fluid leak)\b"),
|
| 357 |
("neuromonitoring_change", r"(loss|significant (decrease|change)).*(ssep|mep|meps|tcem|neuromonitor)"),
|
|
|
|
| 365 |
("positioning_injury", r"\b(pressure (injury|ulcer)|brachial plexus|ulnar neuropathy|peroneal neuropathy)\b"),
|
| 366 |
("transfusion", r"\btransfus(ed|ion)|prbc\b|\bcell saver\b"),
|
| 367 |
("massive_blood_loss", r"\bebl\b.*\b(> ?800\s?ml|>\s?1(\.|,)?0?00\s?ml|> ?1\s?l|> ?1000\s?cc)\b"),
|
| 368 |
+
("unplanned_return_to_or", r"\b(take[- ]?back|unplanned return)\b.*\b(operating|op)\s*room\b"),
|
| 369 |
("wound_dehiscence", r"\bwound dehiscence\b|\bdehisced\b"),
|
| 370 |
]
|
| 371 |
for key, pat in comp_rules:
|
|
|
|
| 374 |
if hit:
|
| 375 |
complications_list.append(key.replace("_", " ").title())
|
| 376 |
|
| 377 |
+
# Negation in IONM (βno significant changesβ) β clear flag
|
| 378 |
if _has(t, r"no significant changes in (motor|sensory) evoked potentials|neuromonitoring.*no (significant )?changes"):
|
| 379 |
complications_map["neuromonitoring_change"] = False
|
| 380 |
complications_list = [c for c in complications_list if c != "Neuromonitoring Change"]
|
|
|
|
| 382 |
return modifiers, complications_map, complications_list
|
| 383 |
|
| 384 |
# spine_coder_core_pro.py
|
| 385 |
+
# Vertebro FINAL-v2.2-PRO (Block 5/5)
|
| 386 |
# Output builder & main entrypoint with top_k + backward-compat alias
|
| 387 |
|
| 388 |
def vertebro_infer(note: str, payer: str = "Medicare", top_k: int = 10) -> Dict[str, Any]:
|
|
|
|
| 390 |
t = _norm(note)
|
| 391 |
levels = _extract_levels(t)
|
| 392 |
region = _classify_region(levels)
|
| 393 |
+
inters = _count_interspaces(levels)
|
| 394 |
laterality, mods_stub = _laterality_and_modifiers(t)
|
| 395 |
|
| 396 |
# Inference
|
|
|
|
| 398 |
extra_rows = _apply_specialty_packs(t, region, inters, levels)
|
| 399 |
rows = base_rows + extra_rows
|
| 400 |
|
| 401 |
+
# De-dup: keep max confidence, concat rationale
|
| 402 |
merged: Dict[tuple, Dict[str, Any]] = {}
|
| 403 |
for r in rows:
|
| 404 |
key = (r["cpt"], r["category"])
|
|
|
|
| 419 |
}
|
| 420 |
tech_flags = [k for k, v in flags_map.items() if v]
|
| 421 |
|
| 422 |
+
# Attach tech info to primary row(s)
|
| 423 |
if tech_flags:
|
| 424 |
tech_txt = f" (Tech: {', '.join(tech_flags)})"
|
| 425 |
for r in rows:
|
|
|
|
| 442 |
if r.get("modifiers"):
|
| 443 |
r["modifiers"] = [m for m in r["modifiers"] if m != "53"]
|
| 444 |
|
| 445 |
+
# Top-K cap (HF UI)
|
| 446 |
try:
|
| 447 |
k = int(top_k)
|
| 448 |
if k > 0:
|
|
|
|
| 472 |
# Quick CLI test
|
| 473 |
if __name__ == "__main__":
|
| 474 |
sample = """
|
| 475 |
+
Assistant(s): Dr. Amber Parker, PA-C
|
| 476 |
Preop Dx: C3βC6 stenosis with myelopathy.
|
| 477 |
Procedure:
|
| 478 |
1. C3βC6 laminectomy and medial facetectomies.
|
| 479 |
2. C2βT1 posterolateral arthrodesis; posterior instrumentation C2βT1.
|
| 480 |
3. Navigation and fluoroscopy utilized; neuromonitoring with no significant changes.
|
| 481 |
+
EBL 1200 mL. Dural tear identified and repaired primarily. Unplanned return to operating room noted.
|
| 482 |
"""
|
| 483 |
print(json.dumps(vertebro_infer(sample, top_k=12), indent=2))
|