| import matplotlib.pyplot as plt |
| import matplotlib.patches as mpatches |
| from matplotlib.patches import FancyBboxPatch, PathPatch |
| from matplotlib.path import Path |
| import numpy as np |
| from pathlib import Path |
|
|
| |
| |
| |
| OUTPUT_DIR = Path("images/publication_final") |
| OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| |
| C_PRIMARY = "#003366" |
| C_FILL_SVC = "#E6F2FF" |
| C_FILL_DB = "#E6FFEA" |
| C_FILL_EXT = "#F0F0F0" |
| C_ACCENT = "#FF6600" |
| C_TEXT = "#000000" |
| C_ARROW = "#333333" |
|
|
| plt.rcParams.update({ |
| 'font.family': 'sans-serif', |
| 'font.sans-serif': ['Arial', 'DejaVu Sans'], |
| 'font.size': 10, |
| 'axes.linewidth': 1, |
| }) |
|
|
| class DrawEngine: |
| def __init__(self, ax): |
| self.ax = ax |
| self.ax.set_aspect('equal') |
| self.ax.axis('off') |
|
|
| def rect(self, x, y, w, h, label, fill=C_FILL_SVC, border=C_PRIMARY, subtitle=None): |
| """Standard Service Rectangle""" |
| box = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0,rounding_size=0.1", |
| ec=border, fc=fill, lw=1.5, zorder=10) |
| self.ax.add_patch(box) |
| |
| cx, cy = x + w/2, y + h/2 |
| self.ax.text(cx, cy + (0.15 if subtitle else 0), label, ha='center', va='center', |
| fontweight='bold', color=C_TEXT, zorder=11) |
| if subtitle: |
| self.ax.text(cx, cy - 0.15, subtitle, ha='center', va='center', |
| fontsize=8, color='#555555', zorder=11) |
| return (x, y, w, h) |
|
|
| def database(self, x, y, w, h, label): |
| """Cylinder Shape for DB""" |
| |
| top = mpatches.Ellipse((x + w/2, y + h), w, h*0.3, ec=C_PRIMARY, fc=C_FILL_DB, lw=1.5, zorder=12) |
| |
| body = mpatches.Rectangle((x, y + h*0.15), w, h*0.85, ec='none', fc=C_FILL_DB, zorder=11) |
| |
| bottom = mpatches.Arc((x + w/2, y + h*0.15), w, h*0.3, theta1=180, theta2=360, ec=C_PRIMARY, lw=1.5, zorder=12) |
| |
| self.ax.plot([x, x], [y + h*0.15, y + h], color=C_PRIMARY, lw=1.5, zorder=12) |
| self.ax.plot([x+w, x+w], [y + h*0.15, y + h], color=C_PRIMARY, lw=1.5, zorder=12) |
| |
| self.ax.add_patch(top) |
| self.ax.add_patch(body) |
| self.ax.add_patch(bottom) |
| |
| self.ax.text(x + w/2, y + h*0.5, label, ha='center', va='center', |
| fontweight='bold', fontsize=8, zorder=13) |
|
|
| def actor(self, x, y, label): |
| """Stick figure user""" |
| |
| head = mpatches.Circle((x, y + 0.8), 0.2, ec=C_PRIMARY, fc='white', lw=1.5) |
| self.ax.add_patch(head) |
| |
| self.ax.plot([x, x], [y + 0.6, y + 0.3], color=C_PRIMARY, lw=1.5) |
| |
| self.ax.plot([x - 0.25, x + 0.25], [y + 0.5, y + 0.5], color=C_PRIMARY, lw=1.5) |
| |
| self.ax.plot([x, x - 0.2], [y + 0.3, y], color=C_PRIMARY, lw=1.5) |
| self.ax.plot([x, x + 0.2], [y + 0.3, y], color=C_PRIMARY, lw=1.5) |
| |
| self.ax.text(x, y - 0.2, label, ha='center', va='top', fontweight='bold') |
|
|
| def connector(self, p1, p2, label=None, style='->'): |
| """Orthogonal or straight arrow""" |
| |
| |
| self.ax.annotate("", xy=p2, xytext=p1, |
| arrowprops=dict(arrowstyle=style, color=C_ARROW, lw=1.5)) |
| if label: |
| mid = ((p1[0]+p2[0])/2, (p1[1]+p2[1])/2) |
| self.ax.text(mid[0], mid[1] + 0.1, label, ha='center', fontsize=8, |
| bbox=dict(facecolor='white', edgecolor='none', alpha=0.8)) |
|
|
| def title(self, label): |
| self.ax.text(0.5, 0.95, label, transform=self.ax.transAxes, |
| ha='center', fontsize=16, fontweight='bold', color=C_PRIMARY) |
|
|
|
|
| |
| |
| |
|
|
| def slide6_system_overview(): |
| fig, ax = plt.subplots(figsize=(12, 7)) |
| d = DrawEngine(ax) |
| ax.set_xlim(0, 12) |
| ax.set_ylim(0, 8) |
| |
| d.title("High-Level System Architecture") |
| |
| |
| d.actor(1, 4, "Patient") |
| |
| d.rect(2.5, 3.5, 2, 1.5, "Frontend UI", subtitle="Streamlit") |
| d.rect(5.5, 3.5, 2, 1.5, "Orchestrator", subtitle="FastAPI") |
| |
| |
| rag_box = FancyBboxPatch((8, 1), 3.5, 6, boxstyle="round,pad=0.2", |
| ec=C_PRIMARY, fc="#F5F5F5", linestyle="--") |
| ax.add_patch(rag_box) |
| ax.text(9.75, 6.7, "RAG Engine", ha='center', fontweight='bold', color='#555555') |
| |
| d.rect(8.5, 5, 2.5, 1, "Retrieval", subtitle="Hybrid (BM25+Dense)") |
| d.rect(8.5, 3.5, 2.5, 1, "Reranker", subtitle="Cross-Encoder") |
| d.rect(8.5, 2, 2.5, 1, "Generative Model", subtitle="BioMistral-7B") |
| |
| |
| d.database(8.75, 0, 2, 1.2, "Vector DB\n(Chroma)") |
| |
| |
| d.connector((1.3, 4.5), (2.5, 4.5)) |
| d.connector((4.5, 4.25), (5.5, 4.25), "JSON") |
| d.connector((7.5, 4.25), (8.5, 5.5), "Query") |
| |
| |
| d.connector((9.75, 5), (9.75, 4.5)) |
| d.connector((9.75, 3.5), (9.75, 3)) |
| |
| |
| d.connector((9.75, 2), (7.5, 3.8), "Response") |
| |
| plt.tight_layout() |
| plt.savefig(OUTPUT_DIR / "slide6_system_overview.png", dpi=300, facecolor='white') |
| plt.close() |
|
|
| def slide7_detailed_system(): |
| fig, ax = plt.subplots(figsize=(14, 9)) |
| d = DrawEngine(ax) |
| ax.set_xlim(0, 14) |
| ax.set_ylim(0, 9) |
| |
| d.title("Detailed Healthcare RAG Pipeline Component View") |
| |
| |
| y_main = 5 |
| |
| |
| d.rect(0.5, y_main, 2, 1, "Query Processing", subtitle="Clean/NER") |
| |
| |
| d.rect(3, y_main, 2, 1, "Embedding Model", subtitle="MedCPT") |
| |
| |
| d.rect(5.5, y_main+1.5, 2, 1, "Dense Retrieval") |
| d.rect(5.5, y_main-1.5, 2, 1, "Sparse Retrieval", subtitle="BM25") |
| |
| |
| d.database(5.5, y_main-0.25, 2, 1.5, "ChromaDB") |
| |
| |
| d.rect(8, y_main, 2, 1, "Hybrid Fusion\n& Reranking") |
| |
| |
| d.rect(10.5, y_main, 2.5, 1, "LLM Generation", subtitle="BioMistral (QLoRA)") |
| |
| |
| d.rect(10.5, 2, 2.5, 1.5, "XAI Module", subtitle="SHAP/Citations") |
| |
| |
| d.rect(10.5, 0.5, 2.5, 1, "Output Formatter") |
| |
| |
| d.connector((2.5, 5.5), (3, 5.5)) |
| d.connector((5, 5.5), (5.5, 5.5)) |
| |
| |
| ax.annotate("", xy=(5.5, 6), xytext=(4, 6), arrowprops=dict(arrowstyle="->", connectionstyle="angle,angleA=0,angleB=90,rad=10")) |
| |
| d.connector((5, 5.5), (5.5, 6.5)) |
| d.connector((5, 5.5), (5.5, 4)) |
| |
| d.connector((7.5, 6.5), (8, 6)) |
| d.connector((7.5, 4), (8, 5)) |
| |
| d.connector((10, 5.5), (10.5, 5.5)) |
| |
| d.connector((11.75, 5), (11.75, 3.5)) |
| d.connector((11.75, 2), (11.75, 1.5)) |
| |
| plt.tight_layout() |
| plt.savefig(OUTPUT_DIR / "slide7_detailed_system.png", dpi=300, facecolor='white') |
| plt.close() |
|
|
| def slide9_hybrid_retrieval(): |
| fig, ax = plt.subplots(figsize=(10, 6)) |
| d = DrawEngine(ax) |
| ax.set_xlim(0, 10) |
| ax.set_ylim(0, 6) |
| |
| d.title("Hybrid Retrieval Architecture") |
| |
| |
| d.rect(0.5, 2.5, 1.5, 1, "Query") |
| |
| |
| d.connector((2, 3), (3, 4.5)) |
| d.connector((2, 3), (3, 1.5)) |
| |
| |
| d.rect(3, 4, 2, 1, "Dense Enc", subtitle="MedCPT") |
| d.database(5.5, 3.8, 1.5, 1.2, "Vector DB") |
| |
| |
| d.rect(3, 1, 2, 1, "Keyword Ext") |
| d.database(5.5, 0.8, 1.5, 1.2, "Inv. Index") |
| |
| |
| d.rect(8, 2, 1.5, 2, "Hybrid\nFusion", subtitle="Reciprocal Rank") |
| |
| |
| d.connector((5, 4.5), (5.5, 4.5)) |
| d.connector((5, 1.5), (5.5, 1.5)) |
| |
| d.connector((7, 4.5), (8, 3.5)) |
| d.connector((7, 1.5), (8, 2.5)) |
| |
| plt.tight_layout() |
| plt.savefig(OUTPUT_DIR / "slide9_hybrid_retrieval.png", dpi=300, facecolor='white') |
| plt.close() |
|
|
| def slide10_corrective_rag(): |
| fig, ax = plt.subplots(figsize=(10, 7)) |
| d = DrawEngine(ax) |
| ax.set_xlim(0, 10) |
| ax.set_ylim(0, 8) |
| d.title("Corrective RAG (CRAG) Logic") |
| |
| |
| d.rect(1, 6, 2, 1, "Retrieved Docs") |
| |
| |
| |
| d.rect(4, 5.5, 2, 2, "Grounding\nEvaluator", subtitle="Relevance Check") |
| |
| |
| d.rect(7, 6.5, 2, 1, "Relevant", fill="#D4EDDA", border="#28A745") |
| d.rect(7, 4.5, 2, 1, "Irrelevant", fill="#F8D7DA", border="#DC3545") |
| |
| d.rect(7, 2, 2, 1.5, "Knowledge\nRefinement", subtitle="Web Search / Filter") |
| |
| d.rect(4, 0.5, 2, 1, "Final Context") |
| |
| |
| d.connector((3, 6.5), (4, 6.5)) |
| d.connector((6, 7), (7, 7)) |
| d.connector((6, 5), (7, 5)) |
| |
| d.connector((9, 7), (9, 3.5), style="-") |
| d.connector((9, 5), (9, 3.5)) |
| d.connector((9, 3.5), (9, 2)) |
| |
| d.connector((8, 2), (6, 1)) |
| |
| plt.tight_layout() |
| plt.savefig(OUTPUT_DIR / "slide10_corrective_rag.png", dpi=300, facecolor='white') |
| plt.close() |
|
|
| def slide11_xai_module_linear(): |
| fig, ax = plt.subplots(figsize=(12, 6)) |
| d = DrawEngine(ax) |
| ax.set_xlim(0, 12) |
| ax.set_ylim(0, 6) |
| |
| d.title("XAI Module Processing Pipeline") |
| |
| |
| d.rect(0.5, 2.5, 2, 1, "Raw LLM Output") |
| |
| |
| d.rect(3.5, 4.5, 2.5, 1, "Feature Importance", subtitle="SHAP / LIME") |
| d.rect(3.5, 2.5, 2.5, 1, "Confidence Scorer", subtitle="Logits Analysis") |
| d.rect(3.5, 0.5, 2.5, 1, "Source Attribution", subtitle="Citation Matching") |
| |
| |
| d.rect(7, 2, 2, 2, "Explanation\nAggregator") |
| |
| |
| d.rect(10, 2.25, 1.5, 1.5, "Explainable\nResponse", fill="#FFF3CD", border=C_ACCENT) |
| |
| |
| |
| d.connector((2.5, 3), (3.5, 5)) |
| d.connector((2.5, 3), (3.5, 3)) |
| d.connector((2.5, 3), (3.5, 1)) |
| |
| |
| d.connector((6, 5), (7, 3.5)) |
| d.connector((6, 3), (7, 3)) |
| d.connector((6, 1), (7, 2.5)) |
| |
| d.connector((9, 3), (10, 3)) |
| |
| plt.tight_layout() |
| plt.savefig(OUTPUT_DIR / "slide11_xai_module.png", dpi=300, facecolor='white') |
| plt.close() |
| |
| |
| def generate_charts(): |
| |
| plt.figure(figsize=(8, 5)) |
| models = ['Baseline', 'Naive RAG', 'Hybrid', 'Our System'] |
| acc = [0.62, 0.74, 0.81, 0.89] |
| plt.bar(models, acc, color=[C_PRIMARY]*3 + [C_ACCENT]) |
| plt.title("Accuracy Comparison") |
| plt.ylim(0, 1) |
| plt.savefig(OUTPUT_DIR / "slide12_accuracy_results.png", dpi=300) |
| plt.close() |
| |
| |
| plt.figure(figsize=(8, 5)) |
| lat = [200, 600, 900, 1200] |
| acc = [0.62, 0.74, 0.81, 0.89] |
| plt.scatter(lat, acc, s=100, c='gray') |
| plt.scatter([1200], [0.89], s=150, c=C_ACCENT, label='Our System') |
| plt.xlabel('Latency (ms)') |
| plt.ylabel('Accuracy') |
| plt.title('Performance Trade-off') |
| plt.grid(True, linestyle='--', alpha=0.5) |
| plt.savefig(OUTPUT_DIR / "slide13_latency_results.png", dpi=300) |
| plt.close() |
|
|
| if __name__ == "__main__": |
| slide6_system_overview() |
| slide7_detailed_system() |
| slide9_hybrid_retrieval() |
| slide10_corrective_rag() |
| slide11_xai_module_linear() |
| generate_charts() |
|
|