#!/usr/bin/env python3 """ PQC Integration Budget & Person-Month Checker Purpose: Validates consortium budget and person-month allocations from consortium-tracker.csv against PQC Integration proposal constraints: - Total budget: €2,800,000 (€2.8M) - Total person-months: 104 PM baseline (112 PM with 10% buffer) - Budget distribution: VaultMesh 70.4%, Brno 10%, Cyber Trust 12.5%, France 7.1% Usage: python3 budget_checker.py Expected CSV structure (from consortium-tracker.csv): Partner Name, Country, Type, Budget (EUR), Person-Months, LOI Status, ... Author: VaultMesh Technologies B.V. Version: 1.0 Date: 2025-11-06 """ import csv import sys from pathlib import Path from typing import Dict, List, Tuple from dataclasses import dataclass from enum import Enum class CheckStatus(Enum): """Status codes for validation checks.""" PASS = "✓ PASS" WARN = "⚠ WARN" FAIL = "✗ FAIL" @dataclass class PartnerAllocation: """Partner budget and person-month allocation.""" name: str country: str partner_type: str budget_eur: int person_months: float loi_status: str budget_pct: float = 0.0 pm_fte_avg: float = 0.0 @dataclass class ValidationResult: """Result of a validation check.""" check_name: str status: CheckStatus expected: str actual: str details: str = "" class BudgetChecker: """Validates PQC Integration budget and person-month allocations.""" # Proposal constraints TOTAL_BUDGET_EUR = 2_800_000 # €2.8M total BASELINE_PM = 104 # Baseline person-months BUFFERED_PM = 112 # With 10% buffer PROJECT_MONTHS = 24 # 24-month duration # Expected budget distribution (from PQC_Submission_Checklist.md) EXPECTED_BUDGET_PCT = { "VaultMesh Technologies B.V.": 70.4, "Masaryk University": 10.0, "Cyber Trust S.A.": 12.5, "Public Digital Services Agency": 7.1, } # Tolerances BUDGET_TOLERANCE_PCT = 2.0 # ±2% tolerance for budget distribution PM_TOLERANCE_PCT = 5.0 # ±5% tolerance for person-months def __init__(self, csv_path: Path): """Initialize checker with path to consortium tracker CSV.""" self.csv_path = csv_path self.partners: List[PartnerAllocation] = [] self.results: List[ValidationResult] = [] def load_csv(self) -> bool: """Load partner data from CSV file.""" if not self.csv_path.exists(): print(f"✗ ERROR: CSV file not found: {self.csv_path}") return False try: with open(self.csv_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: # Only process rows for PQC Integration proposal # CSV uses "Proposal Track" column if 'PQC' not in row.get('Proposal Track', ''): continue # Parse budget (remove € symbol and commas) budget_str = row.get('Budget (€)', '0').replace('€', '').replace(',', '').strip() try: budget = int(budget_str) if budget_str else 0 except ValueError: print(f"⚠ WARNING: Invalid budget for {row.get('Partner Name')}: {budget_str}") budget = 0 # Parse person-months pm_str = row.get('Person-Months', '0').strip() try: pm = float(pm_str) if pm_str else 0.0 except ValueError: print(f"⚠ WARNING: Invalid person-months for {row.get('Partner Name')}: {pm_str}") pm = 0.0 partner = PartnerAllocation( name=row.get('Partner Name', 'Unknown').strip(), country=row.get('Country', 'Unknown').strip(), partner_type=row.get('Type', 'Unknown').strip(), budget_eur=budget, person_months=pm, loi_status=row.get('LOI Status', 'Unknown').strip(), ) self.partners.append(partner) if not self.partners: print("✗ ERROR: No PQC Integration partners found in CSV") return False print(f"✓ Loaded {len(self.partners)} partners from {self.csv_path.name}\n") return True except Exception as e: print(f"✗ ERROR loading CSV: {e}") return False def calculate_totals(self) -> Tuple[int, float]: """Calculate total budget and person-months.""" total_budget = sum(p.budget_eur for p in self.partners) total_pm = sum(p.person_months for p in self.partners) # Calculate percentages and FTE averages for partner in self.partners: partner.budget_pct = (partner.budget_eur / total_budget * 100) if total_budget > 0 else 0.0 partner.pm_fte_avg = partner.person_months / self.PROJECT_MONTHS return total_budget, total_pm def check_total_budget(self, actual_budget: int) -> ValidationResult: """Validate total budget against proposal constraint.""" expected = f"€{self.TOTAL_BUDGET_EUR:,}" actual = f"€{actual_budget:,}" if actual_budget == self.TOTAL_BUDGET_EUR: status = CheckStatus.PASS details = "Budget matches proposal exactly" elif abs(actual_budget - self.TOTAL_BUDGET_EUR) / self.TOTAL_BUDGET_EUR * 100 < self.BUDGET_TOLERANCE_PCT: status = CheckStatus.WARN variance_pct = (actual_budget - self.TOTAL_BUDGET_EUR) / self.TOTAL_BUDGET_EUR * 100 details = f"Budget variance: {variance_pct:+.1f}% (within tolerance)" else: status = CheckStatus.FAIL variance = actual_budget - self.TOTAL_BUDGET_EUR details = f"Budget off by €{variance:,} ({variance/self.TOTAL_BUDGET_EUR*100:+.1f}%)" return ValidationResult( check_name="Total Budget", status=status, expected=expected, actual=actual, details=details ) def check_total_person_months(self, actual_pm: float) -> ValidationResult: """Validate total person-months against baseline/buffered targets.""" expected = f"{self.BASELINE_PM} PM (baseline) / {self.BUFFERED_PM} PM (buffered)" actual = f"{actual_pm:.1f} PM" if self.BASELINE_PM <= actual_pm <= self.BUFFERED_PM: status = CheckStatus.PASS details = f"Within baseline-buffered range ({actual_pm/self.PROJECT_MONTHS:.1f} FTE avg)" elif actual_pm < self.BASELINE_PM: status = CheckStatus.WARN shortage = self.BASELINE_PM - actual_pm details = f"Below baseline by {shortage:.1f} PM (may underdeliver)" else: status = CheckStatus.FAIL excess = actual_pm - self.BUFFERED_PM details = f"Exceeds buffer by {excess:.1f} PM (over budget risk)" return ValidationResult( check_name="Total Person-Months", status=status, expected=expected, actual=actual, details=details ) def check_budget_distribution(self) -> List[ValidationResult]: """Validate per-partner budget percentages against expected distribution.""" results = [] for partner in self.partners: # Find expected percentage (match by partner name prefix) expected_pct = None for expected_name, pct in self.EXPECTED_BUDGET_PCT.items(): if expected_name in partner.name or partner.name in expected_name: expected_pct = pct break if expected_pct is None: results.append(ValidationResult( check_name=f"Budget % ({partner.name})", status=CheckStatus.WARN, expected="N/A", actual=f"{partner.budget_pct:.1f}%", details="Partner not in expected distribution list" )) continue # Check if actual matches expected within tolerance variance = abs(partner.budget_pct - expected_pct) if variance < self.BUDGET_TOLERANCE_PCT: status = CheckStatus.PASS details = f"Matches expected ({variance:.1f}% variance)" elif variance < self.BUDGET_TOLERANCE_PCT * 2: status = CheckStatus.WARN details = f"Slightly off ({variance:.1f}% variance, {partner.budget_pct - expected_pct:+.1f}%)" else: status = CheckStatus.FAIL details = f"Significant deviation ({variance:.1f}% variance, {partner.budget_pct - expected_pct:+.1f}%)" results.append(ValidationResult( check_name=f"Budget % ({partner.name})", status=status, expected=f"{expected_pct:.1f}%", actual=f"{partner.budget_pct:.1f}%", details=details )) return results def check_loi_status(self) -> List[ValidationResult]: """Validate LOI status for all partners.""" results = [] for partner in self.partners: expected = "Confirmed/Signed/Sent/Coordinator" actual = partner.loi_status if actual.lower() in ['confirmed', 'signed', 'sent', 'coordinator']: status = CheckStatus.PASS details = "LOI confirmed" if actual.lower() != 'coordinator' else "Coordinator (no LOI needed)" elif actual.lower() in ['draft', 'pending']: status = CheckStatus.WARN details = "LOI not yet confirmed (follow up needed)" else: status = CheckStatus.FAIL details = f"LOI status unclear: {actual}" results.append(ValidationResult( check_name=f"LOI Status ({partner.name})", status=status, expected=expected, actual=actual, details=details )) return results def run_all_checks(self) -> bool: """Run all validation checks and store results.""" print("=" * 80) print("PQC INTEGRATION BUDGET & PERSON-MONTH VALIDATION") print("=" * 80) print() # Calculate totals total_budget, total_pm = self.calculate_totals() # Run checks self.results.append(self.check_total_budget(total_budget)) self.results.append(self.check_total_person_months(total_pm)) self.results.extend(self.check_budget_distribution()) self.results.extend(self.check_loi_status()) # Check if all passed all_passed = all(r.status == CheckStatus.PASS for r in self.results) has_warnings = any(r.status == CheckStatus.WARN for r in self.results) has_failures = any(r.status == CheckStatus.FAIL for r in self.results) return all_passed, has_warnings, has_failures def print_partner_breakdown(self): """Print detailed partner breakdown table.""" print("\n" + "=" * 80) print("PARTNER BREAKDOWN") print("=" * 80) print() print(f"{'Partner':<35} {'Country':<8} {'Budget':<15} {'%':<8} {'PM':<8} {'FTE':<6}") print("-" * 80) for partner in self.partners: budget_str = f"€{partner.budget_eur:,}" pct_str = f"{partner.budget_pct:.1f}%" pm_str = f"{partner.person_months:.1f}" fte_str = f"{partner.pm_fte_avg:.2f}" print(f"{partner.name:<35} {partner.country:<8} {budget_str:<15} {pct_str:<8} {pm_str:<8} {fte_str:<6}") # Print totals total_budget, total_pm = self.calculate_totals() total_fte = total_pm / self.PROJECT_MONTHS print("-" * 80) print(f"{'TOTAL':<35} {'':<8} {'€{:,}'.format(total_budget):<15} {'100.0%':<8} {f'{total_pm:.1f}':<8} {f'{total_fte:.2f}':<6}") print() def print_validation_results(self): """Print validation results in formatted table.""" print("\n" + "=" * 80) print("VALIDATION RESULTS") print("=" * 80) print() print(f"{'Check':<40} {'Status':<10} {'Expected':<20} {'Actual':<20}") print("-" * 80) for result in self.results: status_symbol = result.status.value print(f"{result.check_name:<40} {status_symbol:<10} {result.expected:<20} {result.actual:<20}") if result.details: print(f" → {result.details}") print() def print_summary(self, all_passed: bool, has_warnings: bool, has_failures: bool): """Print final summary with recommendations.""" print("=" * 80) print("SUMMARY") print("=" * 80) print() total_checks = len(self.results) passed = sum(1 for r in self.results if r.status == CheckStatus.PASS) warned = sum(1 for r in self.results if r.status == CheckStatus.WARN) failed = sum(1 for r in self.results if r.status == CheckStatus.FAIL) print(f"Total Checks: {total_checks}") print(f"✓ Passed: {passed}") print(f"⚠ Warnings: {warned}") print(f"✗ Failed: {failed}") print() if all_passed: print("🎉 ALL CHECKS PASSED — Budget ready for submission!") print() print("Next steps:") print(" 1. Verify all partner PICs are registered on EU Funding & Tenders Portal") print(" 2. Ensure consortium agreement includes these budget allocations") print(" 3. Cross-check with Part B Section 3.1 (Work Plan & Resources)") print(" 4. Run this checker again if any changes are made to consortium-tracker.csv") return True elif has_failures: print("❌ CRITICAL ISSUES DETECTED — Budget requires fixes before submission!") print() print("Action required:") print(" 1. Review failed checks above") print(" 2. Update consortium-tracker.csv with corrected values") print(" 3. Re-run budget_checker.py to verify fixes") print(" 4. Notify steering committee if budget reallocation needed (requires 75% vote)") return False elif has_warnings: print("⚠️ WARNINGS DETECTED — Budget mostly ready, minor issues to address") print() print("Recommended actions:") print(" 1. Review warnings above (may be acceptable variances)") print(" 2. Confirm with steering committee if warnings are acceptable") print(" 3. Document any intentional deviations in consortium agreement") print(" 4. Re-run checker after any corrections") return True return False def main(): """Main entry point.""" # Determine path to consortium-tracker.csv (relative to this script) script_dir = Path(__file__).parent csv_path = script_dir.parent / "consortium" / "consortium-tracker.csv" print(f"PQC Integration Budget Checker v1.0") print(f"Checking: {csv_path}") print() checker = BudgetChecker(csv_path) # Load CSV if not checker.load_csv(): sys.exit(1) # Print partner breakdown checker.print_partner_breakdown() # Run validation checks all_passed, has_warnings, has_failures = checker.run_all_checks() # Print results checker.print_validation_results() checker.print_summary(all_passed, has_warnings, has_failures) # Exit with appropriate code if has_failures: sys.exit(2) # Critical failures elif has_warnings: sys.exit(1) # Warnings only else: sys.exit(0) # All passed if __name__ == "__main__": main()