#!/usr/bin/env python3 """ MDR Gap Analyzer - EU MDR 2017/745 Compliance Gap Assessment Tool Analyzes device classification, identifies documentation gaps, and generates compliance roadmap for EU MDR transition. Usage: python mdr_gap_analyzer.py --device "Device Name" --class IIa python mdr_gap_analyzer.py --device "Device Name" --class III --output json python mdr_gap_analyzer.py --interactive """ import argparse import json import sys from dataclasses import dataclass, field, asdict from datetime import datetime from typing import List, Dict, Optional from enum import Enum class DeviceClass(Enum): I = "I" I_STERILE = "Is" I_MEASURING = "Im" IIA = "IIa" IIB = "IIb" III = "III" class GapStatus(Enum): NOT_STARTED = "Not Started" IN_PROGRESS = "In Progress" COMPLETE = "Complete" NOT_APPLICABLE = "N/A" @dataclass class GapItem: requirement: str category: str description: str status: GapStatus = GapStatus.NOT_STARTED priority: str = "Medium" evidence_needed: List[str] = field(default_factory=list) notes: str = "" @dataclass class GapAnalysisResult: device_name: str device_class: str analysis_date: str total_requirements: int gaps_identified: int completion_percentage: float gaps: List[Dict] recommendations: List[str] critical_gaps: List[str] class MDRGapAnalyzer: """Analyzer for EU MDR 2017/745 compliance gaps.""" # MDR Requirements by category REQUIREMENTS = { "technical_documentation": [ GapItem( requirement="Annex II - Device Description", category="Technical Documentation", description="Complete device description including variants, accessories, intended purpose", priority="High", evidence_needed=["Device specification", "Intended purpose statement", "Variant listing"] ), GapItem( requirement="Annex II - Information Supplied", category="Technical Documentation", description="Label and IFU meeting Article 13 requirements", priority="High", evidence_needed=["Label artwork", "Instructions for use", "Symbol glossary"] ), GapItem( requirement="Annex II - Design and Manufacturing", category="Technical Documentation", description="Design history file and manufacturing documentation", priority="High", evidence_needed=["Design history file", "Process flow diagram", "Validation reports"] ), GapItem( requirement="Annex II - GSPR Compliance", category="Technical Documentation", description="General Safety and Performance Requirements checklist", priority="Critical", evidence_needed=["GSPR matrix", "Standard compliance evidence", "Risk management file"] ), ], "clinical_evaluation": [ GapItem( requirement="Annex XIV Part A - Clinical Evaluation", category="Clinical Evaluation", description="Clinical evaluation report with systematic literature review", priority="Critical", evidence_needed=["Clinical evaluation report", "Literature search protocol", "Data appraisal"] ), GapItem( requirement="Annex XIV Part B - PMCF", category="Clinical Evaluation", description="Post-market clinical follow-up plan and evaluation report", priority="High", evidence_needed=["PMCF plan", "PMCF evaluation report", "Residual risk assessment"] ), GapItem( requirement="Qualified Person for CER", category="Clinical Evaluation", description="Clinical evaluation by qualified evaluator per Annex XIV", priority="High", evidence_needed=["Evaluator CV", "Qualification evidence", "Signed CER"] ), ], "risk_management": [ GapItem( requirement="ISO 14971 Risk Management", category="Risk Management", description="Complete risk management file per ISO 14971:2019", priority="Critical", evidence_needed=["Risk management plan", "Risk analysis", "Risk evaluation", "Risk control"] ), GapItem( requirement="Benefit-Risk Analysis", category="Risk Management", description="Documented benefit-risk determination", priority="High", evidence_needed=["Benefit-risk analysis document", "Residual risk acceptability"] ), ], "quality_management": [ GapItem( requirement="ISO 13485 QMS", category="Quality Management", description="Quality management system conforming to ISO 13485:2016", priority="Critical", evidence_needed=["QMS manual", "Process documentation", "Internal audit records"] ), GapItem( requirement="Post-Market Surveillance", category="Quality Management", description="PMS system per Article 83-86", priority="High", evidence_needed=["PMS plan", "PSUR (if required)", "Vigilance procedures"] ), ], "udi_eudamed": [ GapItem( requirement="UDI System", category="UDI/EUDAMED", description="Unique Device Identification per Article 27", priority="High", evidence_needed=["UDI-DI assignment", "Label with UDI carrier", "GUDID/EUDAMED registration"] ), GapItem( requirement="EUDAMED Registration", category="UDI/EUDAMED", description="Actor, device, and certificate registration in EUDAMED", priority="Medium", evidence_needed=["Actor registration", "Device registration", "Certificate upload"] ), ], "notified_body": [ GapItem( requirement="Notified Body Selection", category="Notified Body", description="Selection and engagement of MDR-designated Notified Body", priority="Critical", evidence_needed=["NB selection criteria", "NB engagement letter", "Audit schedule"] ), GapItem( requirement="Conformity Assessment", category="Notified Body", description="Completion of appropriate conformity assessment procedure", priority="Critical", evidence_needed=["Application dossier", "Technical documentation submission", "Certificate"] ), ], } # Class-specific requirements CLASS_REQUIREMENTS = { DeviceClass.III: [ GapItem( requirement="Annex III - Class III Additions", category="Technical Documentation", description="Additional documentation for Class III devices", priority="Critical", evidence_needed=["Implant card", "Patient information", "Device tracking"] ), GapItem( requirement="Clinical Investigation", category="Clinical Evaluation", description="Clinical investigation per Article 61 (unless equivalent device)", priority="Critical", evidence_needed=["Clinical investigation plan", "Ethics approval", "Clinical study report"] ), ], DeviceClass.IIB: [ GapItem( requirement="Implantable Device Documentation", category="Technical Documentation", description="Additional requirements for implantable Class IIb devices", priority="High", evidence_needed=["Implant card (if implantable)", "Long-term safety data"] ), ], } def __init__(self, device_name: str, device_class: DeviceClass): self.device_name = device_name self.device_class = device_class self.gaps: List[GapItem] = [] self._build_requirements_list() def _build_requirements_list(self): """Build complete requirements list based on device class.""" # Add all base requirements for category_gaps in self.REQUIREMENTS.values(): for gap in category_gaps: self.gaps.append(GapItem( requirement=gap.requirement, category=gap.category, description=gap.description, priority=gap.priority, evidence_needed=gap.evidence_needed.copy() )) # Add class-specific requirements if self.device_class in self.CLASS_REQUIREMENTS: for gap in self.CLASS_REQUIREMENTS[self.device_class]: self.gaps.append(GapItem( requirement=gap.requirement, category=gap.category, description=gap.description, priority=gap.priority, evidence_needed=gap.evidence_needed.copy() )) # Class I self-certification: NB not required if self.device_class == DeviceClass.I: for gap in self.gaps: if gap.category == "Notified Body": gap.status = GapStatus.NOT_APPLICABLE def update_gap_status(self, requirement: str, status: GapStatus, notes: str = ""): """Update status of a specific gap.""" for gap in self.gaps: if gap.requirement == requirement: gap.status = status gap.notes = notes break def analyze(self) -> GapAnalysisResult: """Perform gap analysis and generate results.""" applicable_gaps = [g for g in self.gaps if g.status != GapStatus.NOT_APPLICABLE] complete_gaps = [g for g in applicable_gaps if g.status == GapStatus.COMPLETE] completion = (len(complete_gaps) / len(applicable_gaps) * 100) if applicable_gaps else 0 # Identify critical gaps critical_gaps = [ g.requirement for g in applicable_gaps if g.priority == "Critical" and g.status != GapStatus.COMPLETE ] # Generate recommendations recommendations = self._generate_recommendations() return GapAnalysisResult( device_name=self.device_name, device_class=self.device_class.value, analysis_date=datetime.now().isoformat(), total_requirements=len(applicable_gaps), gaps_identified=len(applicable_gaps) - len(complete_gaps), completion_percentage=round(completion, 1), gaps=[{ "requirement": g.requirement, "category": g.category, "status": g.status.value, "priority": g.priority, "evidence_needed": g.evidence_needed } for g in applicable_gaps], recommendations=recommendations, critical_gaps=critical_gaps ) def _generate_recommendations(self) -> List[str]: """Generate prioritized recommendations.""" recommendations = [] # Check for critical gaps critical_incomplete = [ g for g in self.gaps if g.priority == "Critical" and g.status not in [GapStatus.COMPLETE, GapStatus.NOT_APPLICABLE] ] if critical_incomplete: recommendations.append( f"CRITICAL: {len(critical_incomplete)} critical requirements not complete. " "Address immediately to proceed with conformity assessment." ) # Check clinical evaluation cer_gap = next((g for g in self.gaps if "Clinical Evaluation" in g.requirement), None) if cer_gap and cer_gap.status != GapStatus.COMPLETE: recommendations.append( "Clinical Evaluation Report (CER) is incomplete. " "This is required before Notified Body submission." ) # Check for Class III specific if self.device_class == DeviceClass.III: ci_gap = next((g for g in self.gaps if "Clinical Investigation" in g.requirement), None) if ci_gap and ci_gap.status != GapStatus.COMPLETE: recommendations.append( "Class III device requires clinical investigation per Article 61 " "unless equivalence can be demonstrated." ) # Check EUDAMED udi_gap = next((g for g in self.gaps if "UDI System" in g.requirement), None) if udi_gap and udi_gap.status != GapStatus.COMPLETE: recommendations.append( "Implement UDI system and plan for EUDAMED registration. " "Required for placing device on EU market." ) return recommendations def format_text_output(result: GapAnalysisResult) -> str: """Format analysis result as text.""" lines = [ "=" * 60, "MDR 2017/745 GAP ANALYSIS REPORT", "=" * 60, f"Device: {result.device_name}", f"Class: {result.device_class}", f"Date: {result.analysis_date[:10]}", "", "-" * 60, "SUMMARY", "-" * 60, f"Total Requirements: {result.total_requirements}", f"Gaps Identified: {result.gaps_identified}", f"Completion: {result.completion_percentage}%", "", ] if result.critical_gaps: lines.extend([ "-" * 60, "CRITICAL GAPS (Address Immediately)", "-" * 60, ]) for gap in result.critical_gaps: lines.append(f" * {gap}") lines.append("") lines.extend([ "-" * 60, "GAP DETAILS BY CATEGORY", "-" * 60, ]) # Group by category categories = {} for gap in result.gaps: cat = gap["category"] if cat not in categories: categories[cat] = [] categories[cat].append(gap) for category, gaps in categories.items(): lines.append(f"\n{category}:") for gap in gaps: status_mark = "✓" if gap["status"] == "Complete" else "○" lines.append(f" [{status_mark}] {gap['requirement']} ({gap['priority']})") lines.extend([ "", "-" * 60, "RECOMMENDATIONS", "-" * 60, ]) for i, rec in enumerate(result.recommendations, 1): lines.append(f"{i}. {rec}") lines.append("=" * 60) return "\n".join(lines) def interactive_mode(): """Run interactive gap analysis session.""" print("=" * 60) print("MDR 2017/745 Gap Analysis - Interactive Mode") print("=" * 60) device_name = input("\nDevice name: ").strip() if not device_name: device_name = "Unnamed Device" print("\nDevice classes:") print(" 1. Class I") print(" 2. Class I (sterile)") print(" 3. Class I (measuring)") print(" 4. Class IIa") print(" 5. Class IIb") print(" 6. Class III") class_map = { "1": DeviceClass.I, "2": DeviceClass.I_STERILE, "3": DeviceClass.I_MEASURING, "4": DeviceClass.IIA, "5": DeviceClass.IIB, "6": DeviceClass.III, } class_choice = input("\nSelect class (1-6): ").strip() device_class = class_map.get(class_choice, DeviceClass.IIA) analyzer = MDRGapAnalyzer(device_name, device_class) print("\nFor each requirement, enter status:") print(" c = Complete") print(" i = In Progress") print(" n = Not Started (default)") print(" x = Not Applicable") print(" Enter = Skip (Not Started)") print("") status_map = { "c": GapStatus.COMPLETE, "i": GapStatus.IN_PROGRESS, "n": GapStatus.NOT_STARTED, "x": GapStatus.NOT_APPLICABLE, } for gap in analyzer.gaps: if gap.status == GapStatus.NOT_APPLICABLE: continue status_input = input(f"{gap.requirement} [c/i/n/x]: ").strip().lower() if status_input in status_map: gap.status = status_map[status_input] result = analyzer.analyze() print("\n" + format_text_output(result)) def main(): parser = argparse.ArgumentParser( description="EU MDR 2017/745 Gap Analysis Tool" ) parser.add_argument("--device", type=str, help="Device name") parser.add_argument( "--class", dest="device_class", choices=["I", "Is", "Im", "IIa", "IIb", "III"], help="Device classification" ) parser.add_argument( "--output", choices=["text", "json"], default="text", help="Output format" ) parser.add_argument( "--interactive", action="store_true", help="Run in interactive mode" ) args = parser.parse_args() if args.interactive: interactive_mode() return if not args.device or not args.device_class: parser.print_help() print("\nError: --device and --class required (or use --interactive)") sys.exit(1) class_map = { "I": DeviceClass.I, "Is": DeviceClass.I_STERILE, "Im": DeviceClass.I_MEASURING, "IIa": DeviceClass.IIA, "IIb": DeviceClass.IIB, "III": DeviceClass.III, } analyzer = MDRGapAnalyzer(args.device, class_map[args.device_class]) result = analyzer.analyze() if args.output == "json": print(json.dumps(asdict(result), indent=2)) else: print(format_text_output(result)) if __name__ == "__main__": main()