#!/usr/bin/env python3 """ Real-World RF Device Identification Benchmark Tests the identification system against actual Flipper Zero community captures. Compares performance on real data vs synthetic data. """ import sys import json from pathlib import Path from typing import List, Dict, Tuple from dataclasses import dataclass, field from collections import defaultdict # Add project root to path sys.path.insert(0, str(Path(__file__).parent.parent)) from src.parser.sub_parser import parse_sub_file from src.matcher.pattern_decoder import get_pattern_decoder, DeviceMatch @dataclass class RealCaptureResult: """Result for a single real capture test""" filename: str expected_protocol: str category: str frequency: int # Results top_match: str = "" top_confidence: float = 0.0 all_matches: List[DeviceMatch] = field(default_factory=list) # Metrics correct: bool = False rank: int = -1 # Rank of expected protocol (-1 if not found) # Performance parse_time_ms: float = 0.0 match_time_ms: float = 0.0 @dataclass class RealBenchmarkSummary: """Overall benchmark statistics for real captures""" total_tests: int = 0 # Top-K accuracy top1_accuracy: float = 0.0 top3_accuracy: float = 0.0 top5_accuracy: float = 0.0 # Confidence distribution high_confidence_count: int = 0 # >80% medium_confidence_count: int = 0 # 50-80% low_confidence_count: int = 0 # <50% none_found_count: int = 0 # No matches # Category breakdown category_stats: Dict[str, Dict] = field(default_factory=dict) # Performance avg_parse_time_ms: float = 0.0 avg_match_time_ms: float = 0.0 # All results results: List[RealCaptureResult] = field(default_factory=list) class RealWorldBenchmark: """Benchmark runner for real-world captures""" def __init__(self): self.decoder = get_pattern_decoder() def run_benchmark( self, test_cases_dir: Path, manifest_path: Path ) -> RealBenchmarkSummary: """ Run benchmark on real-world captures Args: test_cases_dir: Directory containing .sub files manifest_path: Path to manifest.json Returns: RealBenchmarkSummary with all metrics """ # Load manifest with open(manifest_path) as f: manifest = json.load(f) test_cases = manifest['test_cases'] summary = RealBenchmarkSummary() summary.total_tests = len(test_cases) print(f"Running real-world benchmark on {len(test_cases)} captures...") print() for i, test_case in enumerate(test_cases, 1): filepath = test_cases_dir / test_case['filename'] print(f"[{i}/{len(test_cases)}] Testing {filepath.name}...", end=" ") result = self._test_single_file( filepath, test_case['expected_protocol'], test_case['category'], test_case['frequency'] ) summary.results.append(result) # Update category stats if result.category not in summary.category_stats: summary.category_stats[result.category] = { 'total': 0, 'correct': 0, 'top3': 0, 'top5': 0 } stats = summary.category_stats[result.category] stats['total'] += 1 if result.correct and result.rank == 1: stats['correct'] += 1 print(f"✓ CORRECT (confidence: {result.top_confidence:.1%})") elif result.rank > 0 and result.rank <= 3: stats['top3'] += 1 print(f"⚠ Rank {result.rank} (expected in top 3)") elif result.rank > 0 and result.rank <= 5: stats['top5'] += 1 print(f"⚠ Rank {result.rank} (expected in top 5)") else: if result.top_match: print(f"✗ WRONG: {result.top_match} (confidence: {result.top_confidence:.1%})") else: print(f"✗ NO MATCHES FOUND") print() # Calculate aggregate metrics self._calculate_summary_metrics(summary) return summary def _test_single_file( self, filepath: Path, expected_protocol: str, category: str, frequency: int ) -> RealCaptureResult: """Test a single .sub file""" import time result = RealCaptureResult( filename=filepath.name, expected_protocol=expected_protocol, category=category, frequency=frequency ) # Parse file t0 = time.time() try: signal_metadata = parse_sub_file(str(filepath)) except Exception as e: print(f"Parse error: {e}") return result result.parse_time_ms = (time.time() - t0) * 1000 # Run matching t0 = time.time() try: matches = self.decoder.decode(signal_metadata) result.all_matches = matches except Exception as e: print(f"Match error: {e}") return result result.match_time_ms = (time.time() - t0) * 1000 if not matches: return result # Extract top match result.top_match = matches[0].name result.top_confidence = matches[0].confidence # Check if expected protocol is in results (fuzzy matching) for rank, match in enumerate(matches, 1): if self._protocol_matches(match.name, expected_protocol): result.correct = (rank == 1) result.rank = rank break return result def _protocol_matches(self, actual: str, expected: str) -> bool: """Check if protocol names match (fuzzy matching)""" actual_lower = actual.lower() expected_lower = expected.lower() # Exact match if actual_lower == expected_lower: return True # Partial match actual_parts = set(actual_lower.replace('-', ' ').split()) expected_parts = set(expected_lower.replace('-', ' ').split()) # If 2+ common tokens, consider match common = actual_parts & expected_parts if len(common) >= 2: return True # Check if expected is substring of actual if expected_lower in actual_lower: return True return False def _calculate_summary_metrics(self, summary: RealBenchmarkSummary): """Calculate aggregate metrics from individual results""" if summary.total_tests == 0: return # Top-K accuracy top1_correct = sum(1 for r in summary.results if r.correct and r.rank == 1) top3_correct = sum(1 for r in summary.results if r.rank > 0 and r.rank <= 3) top5_correct = sum(1 for r in summary.results if r.rank > 0 and r.rank <= 5) summary.top1_accuracy = top1_correct / summary.total_tests summary.top3_accuracy = top3_correct / summary.total_tests summary.top5_accuracy = top5_correct / summary.total_tests # Confidence distribution for result in summary.results: if result.top_confidence == 0.0: summary.none_found_count += 1 elif result.top_confidence >= 0.8: summary.high_confidence_count += 1 elif result.top_confidence >= 0.5: summary.medium_confidence_count += 1 else: summary.low_confidence_count += 1 # Performance if summary.results: summary.avg_parse_time_ms = sum(r.parse_time_ms for r in summary.results) / len(summary.results) summary.avg_match_time_ms = sum(r.match_time_ms for r in summary.results) / len(summary.results) def print_summary(self, summary: RealBenchmarkSummary): """Print human-readable summary""" print("=" * 80) print("REAL-WORLD BENCHMARK SUMMARY") print("=" * 80) print() print(f"Total Tests: {summary.total_tests}") print() print("TOP-K ACCURACY:") print(f" Top-1: {summary.top1_accuracy:.1%} ({int(summary.top1_accuracy * summary.total_tests)}/{summary.total_tests})") print(f" Top-3: {summary.top3_accuracy:.1%} ({int(summary.top3_accuracy * summary.total_tests)}/{summary.total_tests})") print(f" Top-5: {summary.top5_accuracy:.1%} ({int(summary.top5_accuracy * summary.total_tests)}/{summary.total_tests})") print() print("CONFIDENCE DISTRIBUTION:") print(f" High (>80%): {summary.high_confidence_count} ({summary.high_confidence_count/summary.total_tests:.1%})") print(f" Medium (50-80%): {summary.medium_confidence_count} ({summary.medium_confidence_count/summary.total_tests:.1%})") print(f" Low (<50%): {summary.low_confidence_count} ({summary.low_confidence_count/summary.total_tests:.1%})") print(f" None found: {summary.none_found_count} ({summary.none_found_count/summary.total_tests:.1%})") print() print("PERFORMANCE:") print(f" Avg Parse Time: {summary.avg_parse_time_ms:.2f} ms") print(f" Avg Match Time: {summary.avg_match_time_ms:.2f} ms") print(f" Total Time: {summary.avg_parse_time_ms + summary.avg_match_time_ms:.2f} ms") print() print("BY CATEGORY:") print(f"{'Category':<20} {'Tests':>6} {'Accuracy':>10} {'Top-3':>10}") print("-" * 80) for category, stats in sorted(summary.category_stats.items()): accuracy = stats['correct'] / stats['total'] if stats['total'] > 0 else 0 top3 = stats['top3'] / stats['total'] if stats['total'] > 0 else 0 print(f"{category:<20} {stats['total']:>6} {accuracy:>9.1%} {top3:>9.1%}") print() def save_report(self, summary: RealBenchmarkSummary, output_path: Path): """Save detailed report to markdown""" with open(output_path, 'w') as f: f.write("# RF Device Identification - Real-World Benchmark Results\n\n") f.write(f"**Date**: {Path(__file__).stat().st_mtime}\n") f.write(f"**Total Tests**: {summary.total_tests}\n") f.write(f"**Source**: Real Flipper Zero community captures\n\n") f.write("## Overall Metrics\n\n") f.write("### Top-K Accuracy\n\n") f.write(f"- **Top-1**: {summary.top1_accuracy:.1%}\n") f.write(f"- **Top-3**: {summary.top3_accuracy:.1%}\n") f.write(f"- **Top-5**: {summary.top5_accuracy:.1%}\n\n") f.write("### Confidence Distribution\n\n") f.write(f"- **High (>80%)**: {summary.high_confidence_count} ({summary.high_confidence_count/summary.total_tests:.1%})\n") f.write(f"- **Medium (50-80%)**: {summary.medium_confidence_count} ({summary.medium_confidence_count/summary.total_tests:.1%})\n") f.write(f"- **Low (<50%)**: {summary.low_confidence_count} ({summary.low_confidence_count/summary.total_tests:.1%})\n") f.write(f"- **None found**: {summary.none_found_count} ({summary.none_found_count/summary.total_tests:.1%})\n\n") f.write("### Performance\n\n") f.write(f"- **Avg Parse Time**: {summary.avg_parse_time_ms:.2f} ms\n") f.write(f"- **Avg Match Time**: {summary.avg_match_time_ms:.2f} ms\n") f.write(f"- **Total**: {summary.avg_parse_time_ms + summary.avg_match_time_ms:.2f} ms\n\n") f.write("## By Category\n\n") f.write("| Category | Tests | Top-1 Acc | Top-3 Acc |\n") f.write("|----------|-------|-----------|------------|\n") for category, stats in sorted(summary.category_stats.items()): accuracy = stats['correct'] / stats['total'] if stats['total'] > 0 else 0 top3 = stats['top3'] / stats['total'] if stats['total'] > 0 else 0 f.write(f"| {category} | {stats['total']} | {accuracy:.1%} | {top3:.1%} |\n") f.write("\n## Detailed Results\n\n") for result in summary.results: status = "✓" if result.correct else "✗" f.write(f"### {status} {result.filename}\n\n") f.write(f"- **Expected**: {result.expected_protocol}\n") f.write(f"- **Category**: {result.category}\n") f.write(f"- **Got**: {result.top_match if result.top_match else 'No matches'} ") f.write(f"(confidence: {result.top_confidence:.1%})\n") f.write(f"- **Rank**: {result.rank if result.rank > 0 else 'Not Found'}\n") if result.all_matches: f.write(f"\n**Top 5 Matches**:\n") for i, match in enumerate(result.all_matches[:5], 1): f.write(f"{i}. {match.name} ({match.confidence:.1%})\n") f.write("\n") def main(): """Main benchmark runner""" print("=" * 80) print("REAL-WORLD RF DEVICE IDENTIFICATION BENCHMARK") print("=" * 80) print() # Setup paths test_dir = Path(__file__).parent.parent / "tests" / "real_captures" manifest_path = test_dir / "manifest.json" if not manifest_path.exists(): print(f"❌ Manifest not found: {manifest_path}") return 1 # Run benchmark benchmark = RealWorldBenchmark() summary = benchmark.run_benchmark(test_dir, manifest_path) # Print results print() benchmark.print_summary(summary) # Save report report_path = Path(__file__).parent.parent / "REAL_TEST_RESULTS.md" benchmark.save_report(summary, report_path) print(f"Detailed report saved to: {report_path}") print() # Compare with synthetic print("=" * 80) print("COMPARISON: Real vs Synthetic") print("=" * 80) print() synthetic_results_path = Path(__file__).parent.parent / "TEST_RESULTS_SUMMARY.md" if synthetic_results_path.exists(): print("Real-world results compared with synthetic benchmark:") print(f" Real captures: {summary.total_tests} tests") print(f" Synthetic signals: 12 tests (from TEST_RESULTS_SUMMARY.md)") print() print(f" Real Top-1: {summary.top1_accuracy:.1%}") print(f" Synthetic Top-1: 33.3% (from previous run)") print() print(f" Real Top-3: {summary.top3_accuracy:.1%}") print(f" Synthetic Top-3: 50.0%") print() # Return exit code if summary.top1_accuracy >= 0.25: print("✓ Benchmark PASSED (Top-1 accuracy >= 25%)") return 0 else: print(f"✗ Benchmark FAILED (Top-1 accuracy {summary.top1_accuracy:.1%} < 25%)") return 1 if __name__ == '__main__': sys.exit(main())