Files
gh-k-dense-ai-claude-scient…/skills/clinical-decision-support/scripts/build_decision_tree.py
2025-11-30 08:30:14 +08:00

448 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Build Clinical Decision Tree Flowcharts in TikZ Format
Generates LaTeX/TikZ code for clinical decision algorithms from
simple text or YAML descriptions.
Dependencies: pyyaml (optional, for YAML input)
"""
import argparse
from pathlib import Path
import json
class DecisionNode:
"""Represents a decision point in the clinical algorithm."""
def __init__(self, question, yes_path=None, no_path=None, node_id=None):
self.question = question
self.yes_path = yes_path
self.no_path = no_path
self.node_id = node_id or self._generate_id(question)
def _generate_id(self, text):
"""Generate clean node ID from text."""
return ''.join(c for c in text if c.isalnum())[:15].lower()
class ActionNode:
"""Represents an action/outcome in the clinical algorithm."""
def __init__(self, action, urgency='routine', node_id=None):
self.action = action
self.urgency = urgency # 'urgent', 'semiurgent', 'routine'
self.node_id = node_id or self._generate_id(action)
def _generate_id(self, text):
return ''.join(c for c in text if c.isalnum())[:15].lower()
def generate_tikz_header():
"""Generate TikZ preamble with style definitions."""
tikz = """\\documentclass[10pt]{article}
\\usepackage[margin=0.5in, landscape]{geometry}
\\usepackage{tikz}
\\usetikzlibrary{shapes,arrows,positioning}
\\usepackage{xcolor}
% Color definitions
\\definecolor{urgentred}{RGB}{220,20,60}
\\definecolor{actiongreen}{RGB}{0,153,76}
\\definecolor{decisionyellow}{RGB}{255,193,7}
\\definecolor{routineblue}{RGB}{100,181,246}
\\definecolor{headerblue}{RGB}{0,102,204}
% TikZ styles
\\tikzstyle{startstop} = [rectangle, rounded corners=8pt, minimum width=3cm, minimum height=1cm,
text centered, draw=black, fill=headerblue!20, font=\\small\\bfseries]
\\tikzstyle{decision} = [diamond, minimum width=3cm, minimum height=1.2cm, text centered,
draw=black, fill=decisionyellow!40, font=\\small, aspect=2, inner sep=0pt,
text width=3.5cm]
\\tikzstyle{process} = [rectangle, rounded corners=4pt, minimum width=3.5cm, minimum height=0.9cm,
text centered, draw=black, fill=actiongreen!20, font=\\small]
\\tikzstyle{urgent} = [rectangle, rounded corners=4pt, minimum width=3.5cm, minimum height=0.9cm,
text centered, draw=urgentred, line width=1.5pt, fill=urgentred!15,
font=\\small\\bfseries]
\\tikzstyle{routine} = [rectangle, rounded corners=4pt, minimum width=3.5cm, minimum height=0.9cm,
text centered, draw=black, fill=routineblue!20, font=\\small]
\\tikzstyle{arrow} = [thick,->,>=stealth]
\\tikzstyle{urgentarrow} = [ultra thick,->,>=stealth,color=urgentred]
\\begin{document}
\\begin{center}
{\\Large\\bfseries Clinical Decision Algorithm}\\\\[10pt]
{\\large [TITLE TO BE SPECIFIED]}
\\end{center}
\\vspace{10pt}
\\begin{tikzpicture}[node distance=2.2cm and 3.5cm, auto]
"""
return tikz
def generate_tikz_footer():
"""Generate TikZ closing code."""
tikz = """
\\end{tikzpicture}
\\end{document}
"""
return tikz
def simple_algorithm_to_tikz(algorithm_text, output_file='algorithm.tex'):
"""
Convert simple text-based algorithm to TikZ flowchart.
Input format (simple question-action pairs):
START: Chief complaint
Q1: High-risk criteria present? -> YES: Immediate action (URGENT) | NO: Continue
Q2: Risk score >= 3? -> YES: Admit ICU | NO: Outpatient management (ROUTINE)
END: Final outcome
Parameters:
algorithm_text: Multi-line string with algorithm
output_file: Path to save .tex file
"""
tikz_code = generate_tikz_header()
# Parse algorithm text
lines = [line.strip() for line in algorithm_text.strip().split('\n') if line.strip()]
node_defs = []
arrow_defs = []
previous_node = None
node_counter = 0
for line in lines:
if line.startswith('START:'):
# Start node
text = line.replace('START:', '').strip()
node_id = 'start'
node_defs.append(f"\\node [startstop] ({node_id}) {{{text}}};")
previous_node = node_id
node_counter += 1
elif line.startswith('END:'):
# End node
text = line.replace('END:', '').strip()
node_id = 'end'
# Position relative to previous
if previous_node:
node_defs.append(f"\\node [startstop, below=of {previous_node}] ({node_id}) {{{text}}};")
arrow_defs.append(f"\\draw [arrow] ({previous_node}) -- ({node_id});")
elif line.startswith('Q'):
# Decision node
parts = line.split(':', 1)
if len(parts) < 2:
continue
question_part = parts[1].split('->')[0].strip()
node_id = f'q{node_counter}'
# Add decision node
if previous_node:
node_defs.append(f"\\node [decision, below=of {previous_node}] ({node_id}) {{{question_part}}};")
arrow_defs.append(f"\\draw [arrow] ({previous_node}) -- ({node_id});")
else:
node_defs.append(f"\\node [decision] ({node_id}) {{{question_part}}};")
# Parse YES and NO branches
if '->' in line:
branches = line.split('->')[1].split('|')
for branch in branches:
branch = branch.strip()
if branch.startswith('YES:'):
yes_action = branch.replace('YES:', '').strip()
yes_id = f'yes{node_counter}'
# Check urgency
if '(URGENT)' in yes_action:
style = 'urgent'
yes_action = yes_action.replace('(URGENT)', '').strip()
arrow_style = 'urgentarrow'
elif '(ROUTINE)' in yes_action:
style = 'routine'
yes_action = yes_action.replace('(ROUTINE)', '').strip()
arrow_style = 'arrow'
else:
style = 'process'
arrow_style = 'arrow'
node_defs.append(f"\\node [{style}, left=of {node_id}] ({yes_id}) {{{yes_action}}};")
arrow_defs.append(f"\\draw [{arrow_style}] ({node_id}) -- node[above] {{Yes}} ({yes_id});")
elif branch.startswith('NO:'):
no_action = branch.replace('NO:', '').strip()
no_id = f'no{node_counter}'
# Check urgency
if '(URGENT)' in no_action:
style = 'urgent'
no_action = no_action.replace('(URGENT)', '').strip()
arrow_style = 'urgentarrow'
elif '(ROUTINE)' in no_action:
style = 'routine'
no_action = no_action.replace('(ROUTINE)', '').strip()
arrow_style = 'arrow'
else:
style = 'process'
arrow_style = 'arrow'
node_defs.append(f"\\node [{style}, right=of {node_id}] ({no_id}) {{{no_action}}};")
arrow_defs.append(f"\\draw [{arrow_style}] ({node_id}) -- node[above] {{No}} ({no_id});")
previous_node = node_id
node_counter += 1
# Add all nodes and arrows to TikZ
tikz_code += '\n'.join(node_defs) + '\n\n'
tikz_code += '% Arrows\n'
tikz_code += '\n'.join(arrow_defs) + '\n'
tikz_code += generate_tikz_footer()
# Save to file
with open(output_file, 'w') as f:
f.write(tikz_code)
print(f"TikZ flowchart saved to: {output_file}")
print(f"Compile with: pdflatex {output_file}")
return tikz_code
def json_to_tikz(json_file, output_file='algorithm.tex'):
"""
Convert JSON decision tree specification to TikZ flowchart.
JSON format:
{
"title": "Algorithm Title",
"nodes": {
"start": {"type": "start", "text": "Patient presentation"},
"q1": {"type": "decision", "text": "Criteria met?", "yes": "action1", "no": "q2"},
"action1": {"type": "action", "text": "Immediate intervention", "urgency": "urgent"},
"q2": {"type": "decision", "text": "Score >= 3?", "yes": "action2", "no": "action3"},
"action2": {"type": "action", "text": "Admit ICU"},
"action3": {"type": "action", "text": "Outpatient", "urgency": "routine"}
},
"start_node": "start"
}
"""
with open(json_file, 'r') as f:
spec = json.load(f)
tikz_code = generate_tikz_header()
# Replace title
title = spec.get('title', 'Clinical Decision Algorithm')
tikz_code = tikz_code.replace('[TITLE TO BE SPECIFIED]', title)
nodes = spec['nodes']
start_node = spec.get('start_node', 'start')
# Generate nodes (simplified layout - vertical)
node_defs = []
arrow_defs = []
# Track positioning
previous_node = None
level = 0
def add_node(node_id, position_rel=None):
"""Recursively add nodes."""
if node_id not in nodes:
return
node = nodes[node_id]
node_type = node['type']
text = node['text']
# Determine TikZ style
if node_type == 'start' or node_type == 'end':
style = 'startstop'
elif node_type == 'decision':
style = 'decision'
elif node_type == 'action':
urgency = node.get('urgency', 'normal')
if urgency == 'urgent':
style = 'urgent'
elif urgency == 'routine':
style = 'routine'
else:
style = 'process'
else:
style = 'process'
# Position node
if position_rel:
node_def = f"\\node [{style}, {position_rel}] ({node_id}) {{{text}}};"
else:
node_def = f"\\node [{style}] ({node_id}) {{{text}}};"
node_defs.append(node_def)
# Add arrows for decision nodes
if node_type == 'decision':
yes_target = node.get('yes')
no_target = node.get('no')
if yes_target:
# Determine arrow style based on target urgency
target_node = nodes.get(yes_target, {})
arrow_style = 'urgentarrow' if target_node.get('urgency') == 'urgent' else 'arrow'
arrow_defs.append(f"\\draw [{arrow_style}] ({node_id}) -| node[near start, above] {{Yes}} ({yes_target});")
if no_target:
target_node = nodes.get(no_target, {})
arrow_style = 'urgentarrow' if target_node.get('urgency') == 'urgent' else 'arrow'
arrow_defs.append(f"\\draw [{arrow_style}] ({node_id}) -| node[near start, above] {{No}} ({no_target});")
# Simple layout - just list nodes (manual positioning in JSON works better for complex trees)
for node_id in nodes.keys():
add_node(node_id)
tikz_code += '\n'.join(node_defs) + '\n\n'
tikz_code += '% Arrows\n'
tikz_code += '\n'.join(arrow_defs) + '\n'
tikz_code += generate_tikz_footer()
# Save
with open(output_file, 'w') as f:
f.write(tikz_code)
print(f"TikZ flowchart saved to: {output_file}")
return tikz_code
def create_example_json():
"""Create example JSON specification for testing."""
example = {
"title": "Acute Chest Pain Management Algorithm",
"nodes": {
"start": {
"type": "start",
"text": "Patient with\\nchest pain"
},
"q1": {
"type": "decision",
"text": "STEMI\\ncriteria?",
"yes": "stemi_action",
"no": "q2"
},
"stemi_action": {
"type": "action",
"text": "Activate cath lab\\nAspirin, heparin\\nPrimary PCI",
"urgency": "urgent"
},
"q2": {
"type": "decision",
"text": "High-risk\\nfeatures?",
"yes": "admit",
"no": "q3"
},
"admit": {
"type": "action",
"text": "Admit CCU\\nSerial troponins\\nEarly angiography"
},
"q3": {
"type": "decision",
"text": "TIMI\\nscore 0-1?",
"yes": "lowrisk",
"no": "moderate"
},
"lowrisk": {
"type": "action",
"text": "Observe 6-12h\\nStress test\\nOutpatient f/u",
"urgency": "routine"
},
"moderate": {
"type": "action",
"text": "Admit telemetry\\nMedical management\\nRisk stratification"
}
},
"start_node": "start"
}
return example
def main():
parser = argparse.ArgumentParser(description='Build clinical decision tree flowcharts')
parser.add_argument('-i', '--input', type=str, default=None,
help='Input file (JSON format)')
parser.add_argument('-o', '--output', type=str, default='clinical_algorithm.tex',
help='Output .tex file')
parser.add_argument('--example', action='store_true',
help='Generate example algorithm')
parser.add_argument('--text', type=str, default=None,
help='Simple text algorithm (see format in docstring)')
args = parser.parse_args()
if args.example:
print("Generating example algorithm...")
example_spec = create_example_json()
# Save example JSON
with open('example_algorithm.json', 'w') as f:
json.dump(example_spec, f, indent=2)
print("Example JSON saved to: example_algorithm.json")
# Generate TikZ from example
json_to_tikz('example_algorithm.json', args.output)
elif args.text:
print("Generating algorithm from text...")
simple_algorithm_to_tikz(args.text, args.output)
elif args.input:
print(f"Generating algorithm from {args.input}...")
if args.input.endswith('.json'):
json_to_tikz(args.input, args.output)
else:
with open(args.input, 'r') as f:
text = f.read()
simple_algorithm_to_tikz(text, args.output)
else:
print("No input provided. Use --example to generate example, --text for simple text, or -i for JSON input.")
print("\nSimple text format:")
print("START: Patient presentation")
print("Q1: Criteria met? -> YES: Action (URGENT) | NO: Continue")
print("Q2: Score >= 3? -> YES: Admit | NO: Outpatient (ROUTINE)")
print("END: Follow-up")
if __name__ == '__main__':
main()
# Example usage:
# python build_decision_tree.py --example
# python build_decision_tree.py -i algorithm_spec.json -o my_algorithm.tex
#
# Then compile:
# pdflatex clinical_algorithm.tex