diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 46a683c..2b35ed5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: Report a reproducible defect in public SchroSIM components (CLI/core/docs). +description: Report a reproducible defect in public SchroSIM components (CLI/core/docs/Studio UI). title: "[Bug]: " labels: - bug @@ -24,6 +24,7 @@ body: - CLI (schrosim-cli) - Swift core runtime - Rust core runtime + - SchroSIM Studio UI - Configuration/examples - Documentation - Other public component @@ -34,7 +35,7 @@ body: attributes: label: Command / Entry point description: Include command and backend (if applicable). - placeholder: swift run schrosim-cli run examples/runtime_default_foundry.json --backend hybrid + placeholder: swift run schrosim-cli run docs/examples/runtime_default_foundry.json --backend hybrid - type: textarea id: reproduction attributes: @@ -72,5 +73,5 @@ body: options: - label: This is not a security vulnerability disclosure. required: true - - label: This report only references public repo scope (CLI/core/docs), not private enterprise GUI code. + - label: This report only references public repo scope (CLI/core/docs/Studio UI). required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 46cda1f..39d5788 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - name: Security Vulnerability Report - url: https://github.com/DennisWayo/SchroSIM/security + url: https://github.com/Gottesman-Software/SchroSIM/security about: Report security issues privately per SECURITY.md. Do not post exploit details publicly. - name: Community Guidelines - url: https://github.com/DennisWayo/SchroSIM/blob/main/CODE_OF_CONDUCT.md + url: https://github.com/Gottesman-Software/SchroSIM/blob/main/CODE_OF_CONDUCT.md about: Read expected behavior and reporting guidelines. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 88e9584..0d65e66 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature Request -description: Propose an enhancement for public SchroSIM components (CLI/core/docs). +description: Propose an enhancement for public SchroSIM components (CLI/core/docs/Studio UI). title: "[Feature]: " labels: - enhancement @@ -38,5 +38,5 @@ body: attributes: label: Scope Confirmation options: - - label: This request targets public repo scope (CLI/core/docs) and not private enterprise GUI components. + - label: This request targets public repo scope (CLI/core/docs/Studio UI). required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9abc4f4..97d898d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,10 +5,10 @@ Describe the change in 2-5 bullet points. What problem does this PR solve? ## Scope -Public scope for this repository is CLI/core/docs. +Public scope for this repository is CLI/core/docs/Studio UI. -- [ ] I confirm this PR does not add or modify private enterprise GUI code/assets/workflows. -- [ ] I confirm this PR does not include enterprise-only configuration or internal enterprise paths. +- [ ] I confirm this PR keeps public examples, config, benchmarks, and assets under `docs/`. +- [ ] I confirm this PR does not include credentials, generated exports, build outputs, or private lab data. ## Validation List commands run and key outputs. diff --git a/.gitignore b/.gitignore index e13b68a..6c718ab 100644 --- a/.gitignore +++ b/.gitignore @@ -27,15 +27,7 @@ exports/*.json docs/SchroSIM_ConceptNote.pdf last_result.json -/enterprise/* -/enterprise/README.md - -/Sources/schrosim-gui/ -/core-rust/schrosim-gui/ -/core-swift/Sources/schrosim-enterprise-ui/ -/core-swift/Tests/EnterpriseUITests/ -/core-swift/Sources/schrosim-cli/trace_enterprise.swift -/config/profiles/enterprise-default-v1.json -/docs/ui-mockups/*enterprise* -!/docs/ui-mockups/schrosim-enterprise-preview.gif -/.github/workflows/*enterprise*.yml +# SchroSIM Studio generated/local artifacts +/studio/docs/ +/studio/.github/ +/studio/exports/ diff --git a/Package.swift b/Package.swift index d148752..3248442 100644 --- a/Package.swift +++ b/Package.swift @@ -2,9 +2,9 @@ import Foundation import PackageDescription -let enterpriseUISourcePath = "enterprise/core-swift/Sources/schrosim-enterprise-ui" -let enterpriseUITestsPath = "enterprise/core-swift/Tests/EnterpriseUITests" -let hasEnterpriseUI = FileManager.default.fileExists(atPath: enterpriseUISourcePath) +let studioUISourcePath = "core-swift/Sources/schrosim-studio" +let studioUITestsPath = "core-swift/Tests/SchroSIMStudioTests" +let hasStudioUI = FileManager.default.fileExists(atPath: studioUISourcePath) var products: [Product] = [ .library( @@ -17,11 +17,11 @@ var products: [Product] = [ ), ] -if hasEnterpriseUI { +if hasStudioUI { products.append( .executable( - name: "schrosim-enterprise-ui", - targets: ["schrosim-enterprise-ui"] + name: "schrosim-studio", + targets: ["schrosim-studio"] ) ) } @@ -54,21 +54,21 @@ var targets: [Target] = [ ), ] -if hasEnterpriseUI { +if hasStudioUI { targets.append( .executableTarget( - name: "schrosim-enterprise-ui", + name: "schrosim-studio", dependencies: ["SchroSIM"], - path: enterpriseUISourcePath + path: studioUISourcePath ) ) - if FileManager.default.fileExists(atPath: enterpriseUITestsPath) { + if FileManager.default.fileExists(atPath: studioUITestsPath) { targets.append( .testTarget( - name: "EnterpriseUITests", - dependencies: ["schrosim-enterprise-ui"], - path: enterpriseUITestsPath + name: "SchroSIMStudioTests", + dependencies: ["schrosim-studio"], + path: studioUITestsPath ) ) } diff --git a/README.md b/README.md index c18ea01..81670ef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- SchroSIM - Simulating Quantum Light + SchroSIM - Simulating Quantum Light

@@ -11,8 +11,8 @@ - CI - Docs + CI + Docs Release

@@ -22,20 +22,19 @@ SchroSIM is a continuous-variable (CV) photonic quantum simulation stack for des Built for students, researchers, and quantum foundry engineers, SchroSIM provides an exact-solution simulation path for tractable circuits and scalable backend options for larger workflows. > Documentation: -Full project documentation is in [SchroSIM Docs](https://denniswayo.github.io/SchroSIM/). +Full project documentation is in [SchroSIM Docs](https://gottesman-software.github.io/SchroSIM/). -> "SchroSIM core (compiler, runtime, SDK/CLI, docs, and public examples) is open source under MIT for research, education, and downstream studies. -Enterprise-only capabilities (advanced UI, partner-specific integrations, and commercialization-focused IP) are developed in enterprise mode and are intended to live in a separate private distribution path. -This boundary keeps reproducible scientific workflows public while allowing confidential enterprise delivery for hardware and industrial partners." +> "SchroSIM core, Studio UI, SDK/CLI, docs, and public examples are open source under MIT for research, education, and downstream studies. +The public boundary is reproducible scientific software: design photonic circuit studies, compile them into backend-aware runtime configuration, inspect validation evidence, and replay results from source-controlled artifacts." -![SchroSIM Enterprise UI Preview](docs/ui-mockups/schrosim-enterprise-preview.gif) +![SchroSIM Studio UI Preview](docs/ui-mockups/schrosim-studio-preview.gif) -The enterprise UI is focused on: +SchroSIM Studio is focused on: 1. project-scoped circuit workflows, 2. backend-aware execution policies, 3. analysis views for simulation and QEC outcomes. -Status: enterprise-track active development. +Status: open-source studio active development. ### The Need Photonic and CV workflows need fast iteration across modeling, backend selection, and error-correction validation. In practice, teams often lose time moving between disconnected tools for design, compilation, execution, and debugging. @@ -61,7 +60,7 @@ Use this rule of thumb: 2. use scalable/hybrid/Fock policy-targeted paths as controlled approximations for larger experiments and pre-hardware iteration. ### Design-to-Result Workflow -1. define a circuit in project JSON (public workflow) or design it in the enterprise UI (private track), +1. define a circuit in project JSON or design it in SchroSIM Studio, 2. compile to backend-aware intermediate/runtime configuration, 3. run static checks for operation support and constraint compatibility, 4. execute simulation on selected path (model-exact Gaussian or controlled approximation backend path), @@ -116,15 +115,20 @@ swift run schrosim-cli --help Python 3 is required for helper scripts and packaging workflows. +Run the open-source Studio UI: +```bash +swift run schrosim-studio +``` + ### CLI: Run Demos Run Boson Sampling: ```bash -schrosim run examples/runtime_default_foundry.json --backend hybrid +schrosim run docs/examples/runtime_default_foundry.json --backend hybrid ``` Run Quantum Error Correction: ```bash -schrosim run examples/cv/qec_single_logical_gkp_memory_mvp.json --backend hybrid +schrosim run docs/examples/cv/qec_single_logical_gkp_memory_mvp.json --backend hybrid ``` ### Optional: Run from CLion @@ -133,7 +137,7 @@ CLion is recommended for contributor workflows (debugging, stepping through code ### Ecosystem Comparison (High-Level) | Framework | Primary focus | Photonic scope | Execution path | Typical fit | |---|---|---|---|---| -| [SchroSIM](https://denniswayo.github.io/SchroSIM/) | CV photonic circuit simulation with backend-aware compilation, runtime diagnostics, and QEC-oriented workflows | Gaussian + selected non-Gaussian workflows (JSON/CLI flow) | Local classical simulation (`gaussian`, `fock`, `hybrid`; CPU/Metal where available) | Reproducible research workflows, QEC scenario studies, validation pipelines, and hardware-like prechecks | +| [SchroSIM](https://gottesman-software.github.io/SchroSIM/) | CV photonic circuit simulation with backend-aware compilation, runtime diagnostics, and QEC-oriented workflows | Gaussian + selected non-Gaussian workflows (JSON/CLI flow) | Local classical simulation (`gaussian`, `fock`, `hybrid`; CPU/Metal where available) | Reproducible research workflows, QEC scenario studies, validation pipelines, and hardware-like prechecks | | [Strawberry Fields](https://strawberryfields.ai/photonics/) | Full-stack Python framework for photonic quantum computing | Continuous-variable photonics, Gaussian/Fock-style workflows | Local simulators + remote execution on Xanadu photonic hardware/cloud | CV algorithm research and cloud photonic experiments | | [Perceval](https://perceval.quandela.net/docs/v1.1/) | Linear optics quantum framework | Discrete-variable / linear-optics photonic circuits | Local simulation + remote execution on simulator/QPU platforms | Linear-optics experiment design, sampling, and cloud QPU workflows | | [PennyLane](https://docs.pennylane.ai/) | Differentiable quantum programming and quantum ML framework | Photonics via plugins (not photonic-only core) | Local simulators + hardware integrations via devices/plugins | Hybrid quantum-classical ML workflows, gradient-based circuit research, and QEC prototyping/education workflows | diff --git a/core-rust/README.md b/core-rust/README.md index 1c1a25e..af7e6e6 100644 --- a/core-rust/README.md +++ b/core-rust/README.md @@ -4,8 +4,7 @@ This folder contains Rust crates for SchroSIM. Current crate: -- `schrosim-gui` — egui desktop app that invokes the Swift CLI -- `schrosim-core` — Rust CLI runtime (`run`, `trace`) used by SwiftUI runtime-engine selection +- `schrosim-core` — Rust CLI runtime (`run`, `trace`, `bench`, `parity`) used for runtime parity checks and benchmark workflows - `schrosim-core` now includes: - Gaussian simulation kernels (phase/squeeze/beam-splitter/displace/loss/thermal/measurement) - Measurement-driven feedback displacement (`feedback_displace`) for syndrome-based correction loops @@ -13,20 +12,15 @@ Current crate: - Fock single-mode path (`phase`, `displace`, `inject_fock`, `inject_cat`) - Foundry compile step parity (validation + optional injected mode-loss) - `parity` command to compare Rust output against `schrosim-cli` -- GUI default backend is `hybrid` (recommended enterprise routing policy) - Compiler/contraction recommendation is sourced from CLI policy: - compiler: `foundry-aware-ir-v1` - contraction policy: `hybrid_auto` -- GUI includes trace-driven live playback: - - `Load trace` calls `schrosim-cli trace ...` - - `Play/Pause/Stop` animates per-gate propagation frames continuously Build/test from repo root with: ```bash -cargo test -p schrosim-gui -cargo run -p schrosim-gui -cargo run -p schrosim-core -- run exports/swiftui_runtime_input.json --backend hybrid -cargo run -p schrosim-core -- trace exports/swiftui_runtime_input.json --backend hybrid --trace-role editor -cargo run -p schrosim-core -- parity exports/swiftui_runtime_input.json --backend hybrid --trace-role editor +cargo test -p schrosim-core +cargo run -p schrosim-core -- run docs/examples/foundry_loss_map.json --backend hybrid +cargo run -p schrosim-core -- trace docs/examples/foundry_loss_map.json --backend hybrid --trace-role editor +cargo run -p schrosim-core -- parity docs/examples/foundry_loss_map.json --backend hybrid --trace-role editor ``` diff --git a/core-rust/schrosim-core/src/main.rs b/core-rust/schrosim-core/src/main.rs index 988fbb0..03de35b 100644 --- a/core-rust/schrosim-core/src/main.rs +++ b/core-rust/schrosim-core/src/main.rs @@ -97,25 +97,25 @@ struct BenchCaseDefinition { const BENCHMARK_CORE_CASES: [BenchCaseDefinition; 5] = [ BenchCaseDefinition { name: "basic_loss_gaussian", - input: BenchInputSource::File("examples/basic_loss.json"), + input: BenchInputSource::File("docs/examples/basic_loss.json"), requested_backend: "gaussian", expected_backend: "gaussian", }, BenchCaseDefinition { name: "seeded_homodyne_gaussian", - input: BenchInputSource::File("examples/seeded_homodyne.json"), + input: BenchInputSource::File("docs/examples/seeded_homodyne.json"), requested_backend: "gaussian", expected_backend: "gaussian", }, BenchCaseDefinition { name: "fock_injection_fock", - input: BenchInputSource::File("examples/fock_injection_smoke.json"), + input: BenchInputSource::File("docs/examples/fock_injection_smoke.json"), requested_backend: "fock", expected_backend: "fock", }, BenchCaseDefinition { name: "fock_injection_hybrid", - input: BenchInputSource::File("examples/fock_injection_smoke.json"), + input: BenchInputSource::File("docs/examples/fock_injection_smoke.json"), requested_backend: "hybrid", expected_backend: "fock", }, @@ -2945,7 +2945,7 @@ fn resolve_foundry_spec( let registry_path = options .foundry_registry_path .as_deref() - .unwrap_or("config/foundry_registry.json"); + .unwrap_or("docs/config/foundry_registry.json"); let signing_key = resolve_foundry_signing_key(options).ok_or_else(|| { "Missing foundry signing key. Provide --foundry-key or SCHROSIM_FOUNDRY_HMAC_KEY" .to_string() diff --git a/core-swift/Sources/schrosim-cli/main.swift b/core-swift/Sources/schrosim-cli/main.swift index 58dc346..c4ee9ca 100644 --- a/core-swift/Sources/schrosim-cli/main.swift +++ b/core-swift/Sources/schrosim-cli/main.swift @@ -496,7 +496,7 @@ enum SchemaPolicy { static let maxSupportedVersion = 1 } -let defaultFoundryRegistryPath = "config/foundry_registry.json" +let defaultFoundryRegistryPath = "docs/config/foundry_registry.json" enum RuntimeRecommendation { static let compiler = "foundry-aware-ir-v1" diff --git a/core-swift/Sources/schrosim-cli/trace_support.swift b/core-swift/Sources/schrosim-cli/trace_support.swift index 43d9aab..f7faae3 100644 --- a/core-swift/Sources/schrosim-cli/trace_support.swift +++ b/core-swift/Sources/schrosim-cli/trace_support.swift @@ -2,7 +2,7 @@ import Foundation let traceRBACCurrentSchemaVersion = 1 let traceArtifactCurrentSchemaVersion = 1 -let defaultTraceRBACPolicyPath = "config/trace_rbac_policy.json" +let defaultTraceRBACPolicyPath = "docs/config/trace_rbac_policy.json" enum TraceRBACAction: String { case view diff --git a/core-swift/Sources/schrosim-studio/AppModels.swift b/core-swift/Sources/schrosim-studio/AppModels.swift new file mode 100644 index 0000000..952bd7e --- /dev/null +++ b/core-swift/Sources/schrosim-studio/AppModels.swift @@ -0,0 +1,1101 @@ +import Foundation + +enum EnvironmentBadge: String, CaseIterable, Identifiable { + case dev + case stage + case prod + + var id: String { rawValue } +} + +enum UserRoleBadge: String, CaseIterable, Identifiable { + case viewer + case editor + case approver + case admin + + var id: String { rawValue } +} + +enum AppTheme: String, CaseIterable, Identifiable { + case white = "White" + case black = "Black" + + var id: String { rawValue } +} + +enum BackendSelection: String, CaseIterable, Identifiable { + case gaussian + case fock + case hybrid + + var id: String { rawValue } + + var title: String { + switch self { + case .gaussian: + return "Gaussian" + case .fock: + return "Fock" + case .hybrid: + return "Hybrid" + } + } +} + +enum RuntimeEngineSelection: String, CaseIterable, Identifiable { + case swift + case rust + + var id: String { rawValue } + + var title: String { + switch self { + case .swift: + return "Swift" + case .rust: + return "Rust" + } + } +} + +enum RightInspectorTab: String, CaseIterable, Identifiable { + case selectionInspector = "SelectionInspector" + case circuitConfigInspector = "CircuitConfigInspector" + case statusInspector = "StatusInspector" + case quantumStateInspector = "QuantumStateInspector" + + var id: String { rawValue } + + var title: String { + switch self { + case .selectionInspector: + return "Selection Panel" + case .circuitConfigInspector: + return "Circuit Config" + case .statusInspector: + return "Status" + case .quantumStateInspector: + return "State" + } + } +} + +enum BottomTab: String, CaseIterable, Identifiable { + case validationConsole = "ValidationConsole" + case runOutputPanel = "RunOutputPanel" + case auditTrailPanel = "AuditTrailPanel" + + var id: String { rawValue } + + var title: String { + switch self { + case .validationConsole: + return "Validation Console" + case .runOutputPanel: + return "Run Output" + case .auditTrailPanel: + return "Audit Trail" + } + } +} + +enum RuntimePhase: String, CaseIterable, Identifiable { + case standby + case compiling + case running + case finished + + var id: String { rawValue } + + var title: String { + switch self { + case .standby: + return "Standby" + case .compiling: + return "Compiling" + case .running: + return "Running" + case .finished: + return "Finished" + } + } +} + +enum RuntimeParityStatus: String { + case match + case drift + case unmatched + + var title: String { + switch self { + case .match: + return "MATCH" + case .drift: + return "DRIFT" + case .unmatched: + return "UNMATCHED" + } + } +} + +struct RuntimeParitySummary: Hashable { + let status: RuntimeParityStatus + let selectedRuntime: RuntimeEngineSelection + let referenceRuntime: RuntimeEngineSelection + let selectedBackendVersion: String + let referenceBackendVersion: String + let meanPhotonDelta: Double? + let stateDeltaMax: Double? + let measurementDelta: Int? + let detail: String +} + +enum StateHealthStatus: String { + case healthy + case review + case invalid + case unavailable + + var title: String { + switch self { + case .healthy: + return "HEALTHY" + case .review: + return "REVIEW" + case .invalid: + return "INVALID" + case .unavailable: + return "UNAVAILABLE" + } + } +} + +enum StateValidityStatus: String { + case valid + case partial + case invalid + case unavailable + + var title: String { + switch self { + case .valid: + return "VALID" + case .partial: + return "PARTIAL" + case .invalid: + return "INVALID" + case .unavailable: + return "UNAVAILABLE" + } + } +} + +enum StateConfidenceStatus: String { + case match + case drift + case unmatched + case unavailable + + var title: String { + switch self { + case .match: + return "MATCH" + case .drift: + return "DRIFT" + case .unmatched: + return "UNMATCHED" + case .unavailable: + return "UNAVAILABLE" + } + } +} + +enum StateRobustnessStatus: String { + case robust + case sensitive + case fragile + case unavailable + + var title: String { + switch self { + case .robust: + return "ROBUST" + case .sensitive: + return "SENSITIVE" + case .fragile: + return "FRAGILE" + case .unavailable: + return "UNAVAILABLE" + } + } +} + +struct ThresholdCheckResult: Hashable, Identifiable { + let key: String + let title: String + let passed: Bool + let value: Double? + let threshold: Double? + let comparator: String + let note: String? + + var id: String { key } +} + +struct KPIPolicyThresholds: Hashable, Decodable { + let minSuccessRate: Double + let maxMeanPhotonP95Delta: Double + let maxStateP95Delta: Double + let maxMeasurementMismatchRate: Double + let minMeanPhotonNumber: Double? + let maxMeanPhotonNumber: Double? + let minSqueezingDB: Double? + let minGaussianPurity: Double? + let maxFockNormalizationError: Double? + let maxLogicalErrorRate: Double? + let minSuppressionFactor: Double? + let minBreakEvenGain: Double? + + enum CodingKeys: String, CodingKey { + case minSuccessRate = "min_success_rate" + case maxMeanPhotonP95Delta = "max_mean_photon_p95_delta" + case maxStateP95Delta = "max_state_p95_delta" + case maxMeasurementMismatchRate = "max_measurement_mismatch_rate" + case minMeanPhotonNumber = "min_mean_photon_number" + case maxMeanPhotonNumber = "max_mean_photon_number" + case minSqueezingDB = "min_squeezing_db" + case minGaussianPurity = "min_gaussian_purity" + case maxFockNormalizationError = "max_fock_normalization_error" + case maxLogicalErrorRate = "max_logical_error_rate" + case minSuppressionFactor = "min_suppression_factor" + case minBreakEvenGain = "min_break_even_gain" + } +} + +struct KPIPolicyDefinition: Hashable, Decodable, Identifiable { + let id: String + let name: String + let version: Int + let targetRepresentation: String + let summary: String + let references: [String] + let thresholds: KPIPolicyThresholds + + enum CodingKeys: String, CodingKey { + case id + case name + case version + case targetRepresentation = "target_representation" + case summary + case references + case thresholds + } +} + +enum KPIPolicyStatus: String { + case pass + case fail + case unavailable + + var title: String { + switch self { + case .pass: + return "PASS" + case .fail: + return "FAIL" + case .unavailable: + return "UNAVAILABLE" + } + } +} + +struct KPIPolicyEvaluationSummary: Hashable { + let policyID: String + let policyName: String + let policyVersion: Int + let status: KPIPolicyStatus + let checks: [ThresholdCheckResult] + let recommendation: String + let detail: String +} + +struct StateRobustnessSummary: Hashable { + let status: StateRobustnessStatus + let score: Double? + let sampleCount: Int + let successRate: Double? + let meanPhotonP95Delta: Double? + let stateP95Delta: Double? + let measurementMismatchRate: Double? + let checks: [ThresholdCheckResult] + let detail: String +} + +struct StateHealthSummary: Hashable { + let status: StateHealthStatus + let validity: StateValidityStatus + let confidence: StateConfidenceStatus + let robustness: StateRobustnessSummary + let kpi: KPIPolicyEvaluationSummary + let checksCompleted: Int + let issueCount: Int + let errors: [String] + let warnings: [String] + let recommendation: String + let successRationale: String? + let detail: String +} + +enum BosonicSignoffStatus: String { + case pass + case incomplete + case unavailable + + var title: String { + switch self { + case .pass: + return "PASS" + case .incomplete: + return "INCOMPLETE" + case .unavailable: + return "UNAVAILABLE" + } + } +} + +struct BosonicSignoffCheck: Hashable, Identifiable { + let key: String + let title: String + let passed: Bool + let observed: String + let expected: String + let detail: String + + var id: String { key } +} + +struct BosonicSignoffSummary: Hashable { + let status: BosonicSignoffStatus + let requiredChecks: Int + let passedChecks: Int + let checks: [BosonicSignoffCheck] + let recommendation: String + let detail: String +} + +struct MonteCarloNoiseModel: Hashable, Codable { + var sampleCount: Int + var lossEtaSigma: Double + var thermalNoiseSigma: Double + var phaseSigmaRad: Double + var displacementSigma: Double + + static let standard = MonteCarloNoiseModel( + sampleCount: 24, + lossEtaSigma: 0.015, + thermalNoiseSigma: 0.06, + phaseSigmaRad: 0.025, + displacementSigma: 0.03 + ) +} + +struct DetectorCalibrationModel: Hashable, Codable { + var homodyneSigma: Double + var heterodyneSigma: Double + var darkCountRate: Double + + static let standard = DetectorCalibrationModel( + homodyneSigma: 0.010, + heterodyneSigma: 0.015, + darkCountRate: 0.0005 + ) + + enum CodingKeys: String, CodingKey { + case homodyneSigma = "homodyne_sigma" + case heterodyneSigma = "heterodyne_sigma" + case darkCountRate = "dark_count_rate" + } +} + +struct DriftCalibrationModel: Hashable, Codable { + var lossEtaPerSample: Double + var thermalPerSample: Double + var phaseRadPerSample: Double + var displacementPerSample: Double + + static let none = DriftCalibrationModel( + lossEtaPerSample: 0.0, + thermalPerSample: 0.0, + phaseRadPerSample: 0.0, + displacementPerSample: 0.0 + ) + + enum CodingKeys: String, CodingKey { + case lossEtaPerSample = "loss_eta_per_sample" + case thermalPerSample = "thermal_per_sample" + case phaseRadPerSample = "phase_rad_per_sample" + case displacementPerSample = "displacement_per_sample" + } +} + +struct CalibratedNoiseCorrelationTensors: Hashable, Codable { + var lossEta: [[Double]]? + var thermal: [[Double]]? + var phaseRad: [[Double]]? + var displacement: [[Double]]? + + enum CodingKeys: String, CodingKey { + case lossEta = "loss_eta" + case thermal + case phaseRad = "phase_rad" + case displacement + } +} + +struct CalibratedNoiseTensorProfile: Hashable, Codable { + var schemaVersion: Int + var deviceID: String + var deviceName: String + var calibratedAt: String? + var modeCount: Int? + var correlations: CalibratedNoiseCorrelationTensors + var detectorModel: DetectorCalibrationModel + var driftModel: DriftCalibrationModel + var notes: String? + + enum CodingKeys: String, CodingKey { + case schemaVersion = "schema_version" + case deviceID = "device_id" + case deviceName = "device_name" + case calibratedAt = "calibrated_at" + case modeCount = "mode_count" + case correlations + case detectorModel = "detector_model" + case driftModel = "drift_model" + case notes + } + + init( + schemaVersion: Int, + deviceID: String, + deviceName: String, + calibratedAt: String?, + modeCount: Int?, + correlations: CalibratedNoiseCorrelationTensors, + detectorModel: DetectorCalibrationModel, + driftModel: DriftCalibrationModel, + notes: String? + ) { + self.schemaVersion = schemaVersion + self.deviceID = deviceID + self.deviceName = deviceName + self.calibratedAt = calibratedAt + self.modeCount = modeCount + self.correlations = correlations + self.detectorModel = detectorModel + self.driftModel = driftModel + self.notes = notes + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + schemaVersion = try container.decodeIfPresent(Int.self, forKey: .schemaVersion) ?? 1 + deviceID = try container.decodeIfPresent(String.self, forKey: .deviceID) ?? "unknown-device" + deviceName = try container.decodeIfPresent(String.self, forKey: .deviceName) ?? deviceID + calibratedAt = try container.decodeIfPresent(String.self, forKey: .calibratedAt) + modeCount = try container.decodeIfPresent(Int.self, forKey: .modeCount) + correlations = try container.decodeIfPresent(CalibratedNoiseCorrelationTensors.self, forKey: .correlations) + ?? CalibratedNoiseCorrelationTensors(lossEta: nil, thermal: nil, phaseRad: nil, displacement: nil) + detectorModel = try container.decodeIfPresent(DetectorCalibrationModel.self, forKey: .detectorModel) + ?? .standard + driftModel = try container.decodeIfPresent(DriftCalibrationModel.self, forKey: .driftModel) + ?? .none + notes = try container.decodeIfPresent(String.self, forKey: .notes) + } +} + +enum PaletteFilter: String, CaseIterable, Identifiable { + case all = "All" + case gaussian = "Gaussian" + case nonGaussian = "Non-Gaussian" + case channels = "Channels" + case measurement = "Measurement" + case control = "Control" + + var id: String { rawValue } +} + +enum PaletteSection: String, CaseIterable, Identifiable { + case sourceAndStatePrep = "SourceAndStatePrep" + case gaussianOps = "GaussianOps" + case nonGaussianOps = "NonGaussianOps" + case channels = "Channels" + case measurements = "Measurements" + case classicalControl = "ClassicalControl" + + var id: String { rawValue } +} + +enum GateKind: String, CaseIterable, Identifiable, Codable { + case sourceVacuum = "source_vacuum" + case sourceCoherent = "source_coherent" + case sourceSqueezed = "source_squeezed" + + case phase = "phase" + case squeeze = "squeeze" + case beamSplitter = "beam_splitter" + case displace = "displace" + + case injectFock = "inject_fock" + case injectCat = "inject_cat" + case injectGkp = "inject_gkp" + + case loss = "loss" + case thermalLoss = "thermal_loss" + + case measureHomodyne = "measure_homodyne" + case measureHeterodyne = "measure_heterodyne" + + case classicalControl = "if_then" + + var id: String { rawValue } + + var displayName: String { + switch self { + case .sourceVacuum: + return "Source Vacuum" + case .sourceCoherent: + return "Source Coherent" + case .sourceSqueezed: + return "Source Squeezed" + case .phase: + return "Rz" + case .squeeze: + return "Squeeze" + case .beamSplitter: + return "Beam Splitter" + case .displace: + return "Displace" + case .injectFock: + return "Inject Fock" + case .injectCat: + return "Inject Cat" + case .injectGkp: + return "Inject GKP" + case .loss: + return "Loss" + case .thermalLoss: + return "Thermal Loss" + case .measureHomodyne: + return "Measure Homodyne" + case .measureHeterodyne: + return "Measure Heterodyne" + case .classicalControl: + return "Classical Control" + } + } + + var hoverDisplayName: String { + switch self { + case .phase: + return "Rz (Phase Rotation)" + default: + return displayName + } + } + + var systemIcon: String { + switch self { + case .sourceVacuum: + return "circle" + case .sourceCoherent: + return "dot.scope" + case .sourceSqueezed: + return "line.3.horizontal.decrease.circle" + case .phase: + return "angle" + case .squeeze: + return "arrow.left.and.right.righttriangle.left.righttriangle.right" + case .beamSplitter: + return "arrow.triangle.branch" + case .displace: + return "move.3d" + case .injectFock: + return "square.stack.3d.up" + case .injectCat: + return "sparkles" + case .injectGkp: + return "hexagon" + case .loss: + return "drop" + case .thermalLoss: + return "flame" + case .measureHomodyne: + return "waveform.path.ecg" + case .measureHeterodyne: + return "scope" + case .classicalControl: + return "switch.2" + } + } + + var runtimeUnsupportedReason: String? { + nil + } + + var symbol: String { + switch self { + case .sourceVacuum: + return "|0>" + case .sourceCoherent: + return "a" + case .sourceSqueezed: + return "r" + case .phase: + return "Rz" + case .squeeze: + return "S" + case .beamSplitter: + return "BS" + case .displace: + return "D" + case .injectFock: + return "|n>" + case .injectCat: + return "cat" + case .injectGkp: + return "gkp" + case .loss: + return "eta" + case .thermalLoss: + return "n_th" + case .measureHomodyne: + return "x" + case .measureHeterodyne: + return "x,p" + case .classicalControl: + return "if" + } + } + + var section: PaletteSection { + switch self { + case .sourceVacuum, .sourceCoherent, .sourceSqueezed: + return .sourceAndStatePrep + case .phase, .squeeze, .beamSplitter, .displace: + return .gaussianOps + case .injectFock, .injectCat, .injectGkp: + return .nonGaussianOps + case .loss, .thermalLoss: + return .channels + case .measureHomodyne, .measureHeterodyne: + return .measurements + case .classicalControl: + return .classicalControl + } + } + + var primaryFilter: PaletteFilter { + switch self { + case .sourceVacuum, .sourceCoherent, .sourceSqueezed, .phase, .squeeze, .beamSplitter, .displace: + return .gaussian + case .injectFock, .injectCat, .injectGkp: + return .nonGaussian + case .loss, .thermalLoss: + return .channels + case .measureHomodyne, .measureHeterodyne: + return .measurement + case .classicalControl: + return .control + } + } + + var defaultParamA: Double { + switch self { + case .sourceCoherent: + return 0.30 + case .sourceSqueezed: + return 0.60 + case .phase: + return 0.0 + case .squeeze: + return 0.8 + case .beamSplitter: + return 0.25 + case .displace: + return 0.35 + case .loss: + return 0.9 + case .thermalLoss: + return 0.85 + case .classicalControl: + return 0.15 + default: + return 0.0 + } + } + + var defaultParamB: Double { + switch self { + case .sourceCoherent: + return 0.00 + case .displace: + return 0.2 + case .thermalLoss: + return 0.1 + default: + return 0.0 + } + } + + var primaryParamLabel: String? { + switch self { + case .sourceCoherent: + return "q (coherent)" + case .sourceSqueezed: + return "r (source squeeze)" + case .phase: + return "theta (Rz angle)" + case .squeeze: + return "r (squeeze factor)" + case .beamSplitter: + return "theta (mixing)" + case .displace: + return "alpha" + case .loss: + return "eta" + case .thermalLoss: + return "eta" + case .classicalControl: + return "theta (if-then phase)" + default: + return nil + } + } + + var secondaryParamLabel: String? { + switch self { + case .sourceCoherent: + return "p (coherent)" + case .displace: + return "beta" + case .thermalLoss: + return "n_th" + default: + return nil + } + } + + var primaryParamRange: ClosedRange? { + switch self { + case .sourceCoherent: + return -2.0...2.0 + case .sourceSqueezed, .squeeze: + return 0.0...1.5 + case .phase: + return -Double.pi...Double.pi + case .beamSplitter: + return 0.0...(Double.pi / 2.0) + case .displace: + return -2.0...2.0 + case .loss, .thermalLoss: + return 0.0...1.0 + case .classicalControl: + return -Double.pi...Double.pi + default: + return nil + } + } + + var secondaryParamRange: ClosedRange? { + switch self { + case .sourceCoherent, .displace: + return -2.0...2.0 + case .thermalLoss: + return 0.0...5.0 + default: + return nil + } + } +} + +struct CircuitNode: Identifiable, Codable, Hashable { + let id: UUID + var kind: GateKind + var mode: Int + var slot: Int + var paramA: Double + var paramB: Double + + init(id: UUID = UUID(), kind: GateKind, mode: Int, slot: Int, paramA: Double? = nil, paramB: Double? = nil) { + self.id = id + self.kind = kind + self.mode = mode + self.slot = slot + self.paramA = paramA ?? kind.defaultParamA + self.paramB = paramB ?? kind.defaultParamB + } + + var subtitle: String { + switch kind { + case .sourceVacuum: + return "vacuum" + case .sourceCoherent: + return "q=\(format(paramA)) p=\(format(paramB))" + case .sourceSqueezed: + return "r=\(format(paramA))" + case .phase: + return "theta=\(format(paramA))" + case .squeeze: + return "r=\(format(paramA))" + case .beamSplitter: + return "theta=\(format(paramA))" + case .displace: + return "alpha=\(format(paramA)) beta=\(format(paramB))" + case .loss: + return "eta=\(format(paramA))" + case .thermalLoss: + return "eta=\(format(paramA)) n_th=\(format(paramB))" + case .classicalControl: + return "if m then phase=\(format(paramA))" + default: + return "default" + } + } + + private func format(_ value: Double) -> String { + String(format: "%.2f", value) + } +} + +struct ScientificCircuitPreset: Identifiable, Hashable { + let id: String + let title: String + let citation: String + let doiURL: String + let sourceURL: String + let mappingNote: String + let recommendedBackend: BackendSelection + let suggestedSeed: String + let modeCount: Int + let slotCount: Int + let nodes: [CircuitNode] +} + +enum ValidationSeverity: String, Codable { + case info = "Info" + case warning = "Warning" + case error = "Error" +} + +struct ValidationIssue: Identifiable, Codable, Hashable { + let id: UUID + let severity: ValidationSeverity + let code: String + let message: String + let location: String + + init( + id: UUID = UUID(), + severity: ValidationSeverity, + code: String, + message: String, + location: String + ) { + self.id = id + self.severity = severity + self.code = code + self.message = message + self.location = location + } +} + +struct RunProvenance: Decodable, Hashable { + let foundryProfileHash: String? + let gitSHA: String? + let backendVersion: String? + + enum CodingKeys: String, CodingKey { + case foundryProfileHash = "foundry_profile_hash" + case gitSHA = "git_sha" + case backendVersion = "backend_version" + } +} + +struct QuantumStateSnapshot: Decodable, Hashable { + struct FockComponent: Decodable, Hashable, Identifiable { + let n: Int + let probability: Double + let re: Double? + let im: Double? + + var id: Int { n } + } + + let backend: String? + let representation: String? + let modes: Int? + let cutoff: Int? + let mean: [Double]? + let covariance: [[Double]]? + let probabilities: [Double]? + let topProbabilities: [FockComponent]? + + enum CodingKeys: String, CodingKey { + case backend + case representation + case modes + case cutoff + case mean + case covariance + case probabilities + case topProbabilities = "top_probabilities" + } +} + +struct QECReportRound: Decodable, Hashable, Identifiable { + let round: Int? + let gateIndex: Int? + let measurementIndex: Int? + let sourceValueIndex: Int? + let mode: Int? + let syndromeValue: Double? + let decoder: String? + let latticeSpacing: Double? + let targetLatticeIndex: Int? + let nearestLatticeIndex: Int? + let nearestLatticeValue: Double? + let residual: Double? + let correctionValue: Double? + let appliedQ: Double? + let appliedP: Double? + let logicalPass: Bool? + + var id: Int { + round ?? gateIndex ?? measurementIndex ?? mode ?? 0 + } + + enum CodingKeys: String, CodingKey { + case round + case gateIndex = "gate_index" + case measurementIndex = "measurement_index" + case sourceValueIndex = "source_value_index" + case mode + case syndromeValue = "syndrome_value" + case decoder + case latticeSpacing = "lattice_spacing" + case targetLatticeIndex = "target_lattice_index" + case nearestLatticeIndex = "nearest_lattice_index" + case nearestLatticeValue = "nearest_lattice_value" + case residual + case correctionValue = "correction_value" + case appliedQ = "applied_q" + case appliedP = "applied_p" + case logicalPass = "logical_pass" + } +} + +struct QECReport: Decodable, Hashable { + let decoder: String? + let roundsExecuted: Int? + let logicalPassCount: Int? + let logicalFailCount: Int? + let logicalPass: Bool? + let logicalErrorRate: Double? + let physicalErrorRateProxy: Double? + let suppressionFactor: Double? + let breakEvenGain: Double? + let breakEvenPass: Bool? + let rounds: [QECReportRound]? + + enum CodingKeys: String, CodingKey { + case decoder + case roundsExecuted = "rounds_executed" + case logicalPassCount = "logical_pass_count" + case logicalFailCount = "logical_fail_count" + case logicalPass = "logical_pass" + case logicalErrorRate = "logical_error_rate" + case physicalErrorRateProxy = "physical_error_rate_proxy" + case suppressionFactor = "suppression_factor" + case breakEvenGain = "break_even_gain" + case breakEvenPass = "break_even_pass" + case rounds + } +} + +struct RunResponse: Decodable, Hashable { + let status: String + let backend: String? + let meanPhotonNumber: Double? + let measurementCount: Int? + let finalState: QuantumStateSnapshot? + let qec: QECReport? + let provenance: RunProvenance? + let error: String? + + enum CodingKeys: String, CodingKey { + case status + case backend + case meanPhotonNumber = "mean_photon_number" + case measurementCount = "measurement_count" + case finalState = "final_state" + case qec + case provenance + case error + } +} + +struct TraceResponse: Decodable, Hashable { + struct Frame: Decodable, Hashable, Identifiable { + let frameIndex: Int + let gateIndex: Int? + let gateType: String + let meanPhotonNumber: Double + let measurementCount: Int + let frameLatencyMs: Double + + var id: Int { frameIndex } + + enum CodingKeys: String, CodingKey { + case frameIndex = "frame_index" + case gateIndex = "gate_index" + case gateType = "gate_type" + case meanPhotonNumber = "mean_photon_number" + case measurementCount = "measurement_count" + case frameLatencyMs = "frame_latency_ms" + } + } + + let status: String + let backend: String? + let traceFrameCount: Int? + let traceOriginalFrameCount: Int? + let traceDroppedFrameCount: Int? + let traceDownsamplingApplied: Bool? + let traceRingBufferApplied: Bool? + let traceTotalMs: Double? + let traceMaxFrameLatencyMs: Double? + let playbackSuggestedFrameMS: Int? + let finalState: QuantumStateSnapshot? + let qec: QECReport? + let frames: [Frame]? + let error: String? + + enum CodingKeys: String, CodingKey { + case status + case backend + case traceFrameCount = "trace_frame_count" + case traceOriginalFrameCount = "trace_original_frame_count" + case traceDroppedFrameCount = "trace_dropped_frame_count" + case traceDownsamplingApplied = "trace_downsampling_applied" + case traceRingBufferApplied = "trace_ring_buffer_applied" + case traceTotalMs = "trace_total_ms" + case traceMaxFrameLatencyMs = "trace_max_frame_latency_ms" + case playbackSuggestedFrameMS = "playback_suggested_frame_ms" + case finalState = "final_state" + case qec + case frames + case error + } +} diff --git a/core-swift/Sources/schrosim-studio/CLIService.swift b/core-swift/Sources/schrosim-studio/CLIService.swift new file mode 100644 index 0000000..cb12e63 --- /dev/null +++ b/core-swift/Sources/schrosim-studio/CLIService.swift @@ -0,0 +1,962 @@ +import Foundation +import AppKit +import UniformTypeIdentifiers + +enum CLIRunnerError: LocalizedError { + case failedToDecodeJSON(String) + case commandFailed(String) + case malformedOutput(String) + case invalidRuntimeCircuit(String) + case invalidCircuitDocument(String) + case invalidNoiseProfile(String) + case unsupportedSaveFormat(String) + + var errorDescription: String? { + switch self { + case .failedToDecodeJSON(let message): + return "Failed to decode CLI JSON: \(message)" + case .commandFailed(let message): + return "CLI command failed: \(message)" + case .malformedOutput(let output): + return "Malformed CLI output: \(output)" + case .invalidRuntimeCircuit(let message): + return "Runtime circuit is invalid: \(message)" + case .invalidCircuitDocument(let message): + return "Circuit document is invalid: \(message)" + case .invalidNoiseProfile(let message): + return "Noise profile is invalid: \(message)" + case .unsupportedSaveFormat(let message): + return "Unsupported save format: \(message)" + } + } +} + +enum CLIService { + enum CircuitSaveFormat: String, CaseIterable { + case pdk + case json + case pdf + + var fileExtension: String { rawValue } + + var displayName: String { + switch self { + case .pdk: + return "SchroSIM Package (.pdk)" + case .json: + return "JSON Snapshot (.json)" + case .pdf: + return "Circuit Report (.pdf)" + } + } + + var contentType: UTType { + switch self { + case .pdk: + return UTType(filenameExtension: fileExtension, conformingTo: .data) ?? .data + case .json: + return .json + case .pdf: + return .pdf + } + } + + static func fromFileExtension(_ fileExtension: String) -> CircuitSaveFormat? { + let normalized = fileExtension.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return allCases.first { $0.fileExtension == normalized } + } + } + + struct CircuitSaveDestination { + let url: URL + let format: CircuitSaveFormat + } + + struct CircuitSnapshotDocument { + let nodes: [CircuitNode] + let modeCount: Int + let slotCount: Int + let schemaVersion: String + let runtimeEngine: RuntimeEngineSelection? + let backend: BackendSelection? + let seed: String? + let cutoff: Int? + let foundryProfile: String? + let workspace: String? + let project: String? + let environment: EnvironmentBadge? + let userRole: UserRoleBadge? + let kpiPolicyID: String? + let kpiPolicyVersion: Int? + } + + static func runCircuit( + jsonPath: String, + backend: BackendSelection, + environment: EnvironmentBadge, + runtimeEngine: RuntimeEngineSelection + ) throws -> RunResponse { + var arguments: [String] + switch runtimeEngine { + case .swift: + arguments = ["swift", "run", "schrosim-cli", "run", jsonPath, "--backend", backend.rawValue] + case .rust: + arguments = ["cargo", "run", "-q", "-p", "schrosim-core", "--", "run", jsonPath, "--backend", backend.rawValue] + } + if environment == .prod { + let registryPath = try validateProdRuntimePrerequisites() + arguments.append(contentsOf: ["--prod", "--foundry-registry", registryPath]) + } + + let (stdout, stderr, status) = try runCommand( + arguments: arguments + ) + + let response: RunResponse = try decodeFromMixedOutput(stdout) + + if status != 0 || response.status != "success" { + throw CLIRunnerError.commandFailed(response.error ?? stderr) + } + + return response + } + + static func loadTrace( + jsonPath: String, + backend: BackendSelection, + role: UserRoleBadge, + environment: EnvironmentBadge, + runtimeEngine: RuntimeEngineSelection + ) throws -> TraceResponse { + var arguments: [String] + switch runtimeEngine { + case .swift: + arguments = [ + "swift", + "run", + "schrosim-cli", + "trace", + jsonPath, + "--backend", + backend.rawValue, + "--trace-role", + role.rawValue, + ] + case .rust: + arguments = [ + "cargo", + "run", + "-q", + "-p", + "schrosim-core", + "--", + "trace", + jsonPath, + "--backend", + backend.rawValue, + "--trace-role", + role.rawValue, + ] + } + + if environment == .prod { + let registryPath = try validateProdRuntimePrerequisites() + arguments.append(contentsOf: ["--prod", "--foundry-registry", registryPath]) + } + + let (stdout, stderr, status) = try runCommand( + arguments: arguments + ) + + let response: TraceResponse = try decodeFromMixedOutput(stdout) + + if status != 0 || response.status != "success" { + throw CLIRunnerError.commandFailed(response.error ?? stderr) + } + + return response + } + + static func materializeRuntimeCircuitInput( + nodes: [CircuitNode], + modeCount: Int, + backend: BackendSelection, + seed: String, + cutoff: Int, + schemaVersion: String, + foundryProfile: String, + environment: EnvironmentBadge, + foundryProfileID: String?, + foundryProfileVersion: Int? + ) throws -> URL { + let resolvedModeCount = max(modeCount, 1) + let resolvedSchemaVersion = max(1, Int(schemaVersion.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 1) + let resolvedCutoff = max(1, cutoff) + let resolvedSeed = UInt64(seed.trimmingCharacters(in: .whitespacesAndNewlines)) + let gatePayload = try runtimeGatePayload(nodes: nodes, modeCount: resolvedModeCount) + + var payload: [String: Any] = [ + "schema_version": resolvedSchemaVersion, + "modes": resolvedModeCount, + "backend": backend.rawValue, + "cutoff": resolvedCutoff, + "gates": gatePayload, + ] + + if environment == .prod { + let trimmedID = foundryProfileID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !trimmedID.isEmpty, let foundryProfileVersion, foundryProfileVersion > 0 else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Prod environment requires a valid foundry profile reference (profile_id/version)." + ) + } + payload["foundry_profile"] = [ + "profile_id": trimmedID, + "version": foundryProfileVersion, + ] + } else { + payload["foundry"] = [ + "name": foundryProfile, + "max_modes": max(4, resolvedModeCount), + "max_squeezing_r": 1.5, + "allow_non_gaussian": true, + "allow_measurements": true, + "inject_mode_loss": false, + "mode_loss_eta": Array(repeating: 0.995, count: resolvedModeCount), + ] + } + + if let resolvedSeed { + payload["seed"] = resolvedSeed + } + + if nodes.contains(where: { $0.kind == .sourceVacuum }) { + payload["initial_state"] = "vacuum" + } + + let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys]) + let exportDir = repoRootURL().appendingPathComponent("exports", isDirectory: true) + try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true) + let runtimeURL = exportDir.appendingPathComponent("swiftui_runtime_input.json") + try data.write(to: runtimeURL, options: [.atomic]) + return runtimeURL + } + + static func validateRuntimeCircuit( + nodes: [CircuitNode], + modeCount: Int, + backend: BackendSelection + ) throws { + let resolvedModeCount = max(modeCount, 1) + _ = try runtimeGatePayload(nodes: nodes, modeCount: resolvedModeCount) + + let requiresFockPath = nodes.contains { node in + node.kind == .injectFock || node.kind == .injectCat + } + + let requiresFockCompatibility = backend == .fock || (backend == .hybrid && requiresFockPath) + guard requiresFockCompatibility else { + return + } + + var reasons: [String] = [] + if resolvedModeCount > 1 { + reasons.append("Fock path supports only single-mode circuits (got \(resolvedModeCount) modes).") + } + + let fockSupportedKinds: Set = [ + .sourceVacuum, + .sourceCoherent, + .phase, + .displace, + .injectFock, + .injectCat, + ] + + let unsupportedKinds = Set(nodes.map(\.kind)).subtracting(fockSupportedKinds) + if !unsupportedKinds.isEmpty { + let labels = unsupportedKinds + .map { $0.rawValue } + .sorted() + .joined(separator: ", ") + reasons.append( + "Unsupported gates for Fock path: \(labels). " + + "Supported: source_vacuum, source_coherent, phase, displace, inject_fock, inject_cat." + ) + } + + if !reasons.isEmpty { + throw CLIRunnerError.invalidRuntimeCircuit(reasons.joined(separator: " ")) + } + } + + @MainActor + static func promptForCircuitSaveDestination( + suggestedBaseName: String, + existingURL: URL? = nil + ) -> CircuitSaveDestination? { + let defaultFormat = existingURL + .flatMap { CircuitSaveFormat.fromFileExtension($0.pathExtension) } ?? .pdk + let panel = NSSavePanel() + panel.title = "Save Circuit As" + panel.message = "Choose file name and format." + panel.prompt = "Save" + panel.nameFieldLabel = "Name:" + panel.canCreateDirectories = true + panel.allowsOtherFileTypes = false + panel.isExtensionHidden = false + panel.showsTagField = false + panel.allowedContentTypes = CircuitSaveFormat.allCases.map(\.contentType) + + let accessoryContainer = NSStackView() + accessoryContainer.orientation = .vertical + accessoryContainer.alignment = .leading + accessoryContainer.spacing = 6 + accessoryContainer.edgeInsets = NSEdgeInsets(top: 2, left: 0, bottom: 0, right: 0) + + let formatLabel = NSTextField(labelWithString: "Format") + let formatPopup = NSPopUpButton() + for format in CircuitSaveFormat.allCases { + formatPopup.addItem(withTitle: format.displayName) + formatPopup.lastItem?.representedObject = format.rawValue + } + if let defaultIndex = CircuitSaveFormat.allCases.firstIndex(of: defaultFormat) { + formatPopup.selectItem(at: defaultIndex) + } + formatPopup.translatesAutoresizingMaskIntoConstraints = false + formatPopup.widthAnchor.constraint(greaterThanOrEqualToConstant: 280).isActive = true + + accessoryContainer.addArrangedSubview(formatLabel) + accessoryContainer.addArrangedSubview(formatPopup) + panel.accessoryView = accessoryContainer + + if let existingURL { + panel.directoryURL = existingURL.deletingLastPathComponent() + panel.nameFieldStringValue = existingURL.lastPathComponent + } else { + panel.directoryURL = repoRootURL().appendingPathComponent("exports", isDirectory: true) + panel.nameFieldStringValue = "\(suggestedBaseName).\(defaultFormat.fileExtension)" + } + + guard panel.runModal() == .OK else { + return nil + } + + let selectedFormatRaw = formatPopup.selectedItem?.representedObject as? String + let selectedFormat = selectedFormatRaw.flatMap(CircuitSaveFormat.init(rawValue:)) ?? defaultFormat + let selectedURL = resolvedSavePanelURL(from: panel) ?? panel.url + guard let selectedURL else { return nil } + return CircuitSaveDestination(url: selectedURL, format: selectedFormat) + } + + @MainActor + static func promptForCircuitOpenURL(existingURL: URL? = nil) -> URL? { + let panel = NSOpenPanel() + panel.title = "Open Circuit" + panel.message = "Open a .pdk or .json circuit snapshot." + panel.prompt = "Open" + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = false + panel.resolvesAliases = true + panel.allowedContentTypes = [ + CircuitSaveFormat.pdk.contentType, + CircuitSaveFormat.json.contentType, + ] + + if let existingURL { + panel.directoryURL = existingURL.deletingLastPathComponent() + } else { + panel.directoryURL = repoRootURL().appendingPathComponent("exports", isDirectory: true) + } + + return panel.runModal() == .OK ? panel.url : nil + } + + @MainActor + static func promptForNoiseProfileOpenURL(existingURL: URL? = nil) -> URL? { + let panel = NSOpenPanel() + panel.title = "Import Calibrated Noise Profile" + panel.message = "Open a calibrated device noise profile (.json)." + panel.prompt = "Import" + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = false + panel.resolvesAliases = true + panel.allowedContentTypes = [.json] + + if let existingURL { + panel.directoryURL = existingURL.deletingLastPathComponent() + } else { + panel.directoryURL = repoRootURL().appendingPathComponent("config", isDirectory: true) + } + + return panel.runModal() == .OK ? panel.url : nil + } + + static func loadCalibratedNoiseProfile(from url: URL) throws -> CalibratedNoiseTensorProfile { + let data = try Data(contentsOf: url) + do { + return try JSONDecoder().decode(CalibratedNoiseTensorProfile.self, from: data) + } catch { + if let wrapped = try? JSONDecoder().decode(CalibratedNoiseProfileEnvelope.self, from: data) { + return wrapped.profile + } + throw CLIRunnerError.invalidNoiseProfile(error.localizedDescription) + } + } + + @MainActor + static func saveCircuitDocument( + nodes: [CircuitNode], + modeCount: Int, + slotCount: Int, + metadata: [String: String], + to requestedURL: URL, + format selectedFormat: CircuitSaveFormat? = nil + ) throws -> URL { + let explicitFormat = CircuitSaveFormat.fromFileExtension(requestedURL.pathExtension) + let selectedContentType = UTType(filenameExtension: requestedURL.pathExtension, conformingTo: .data) + let resolvedFormat = selectedFormat + ?? explicitFormat + ?? CircuitSaveFormat.allCases.first(where: { $0.contentType == selectedContentType }) + ?? .pdk + + let normalizedExtension = requestedURL.pathExtension.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let targetURL: URL + if normalizedExtension == resolvedFormat.fileExtension { + targetURL = requestedURL + } else if normalizedExtension.isEmpty { + targetURL = requestedURL.appendingPathExtension(resolvedFormat.fileExtension) + } else { + targetURL = requestedURL.deletingPathExtension().appendingPathExtension(resolvedFormat.fileExtension) + } + + let exportDir = targetURL.deletingLastPathComponent() + try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true) + + switch resolvedFormat { + case .pdk, .json: + let payload = circuitSnapshotPayload( + nodes: nodes, + modeCount: modeCount, + slotCount: slotCount, + metadata: metadata + ) + let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys]) + try data.write(to: targetURL, options: [.atomic]) + case .pdf: + try writeCircuitReportPDF( + nodes: nodes, + modeCount: modeCount, + slotCount: slotCount, + metadata: metadata, + to: targetURL + ) + } + + return targetURL + } + + static func loadCircuitDocument(from url: URL) throws -> CircuitSnapshotDocument { + let data = try Data(contentsOf: url) + let object = try JSONSerialization.jsonObject(with: data, options: []) + guard let payload = object as? [String: Any] else { + throw CLIRunnerError.invalidCircuitDocument("Expected a JSON object at top-level.") + } + + guard let nodeEntries = payload["nodes"] as? [Any] else { + throw CLIRunnerError.invalidCircuitDocument("Missing required 'nodes' array.") + } + + var nodes: [CircuitNode] = [] + nodes.reserveCapacity(nodeEntries.count) + + for (index, entry) in nodeEntries.enumerated() { + guard let nodePayload = entry as? [String: Any] else { + throw CLIRunnerError.invalidCircuitDocument("Node at index \(index) is not an object.") + } + + guard + let kindRaw = stringValue(from: nodePayload["kind"]), + let kind = GateKind(rawValue: kindRaw) + else { + throw CLIRunnerError.invalidCircuitDocument("Node at index \(index) has unknown 'kind'.") + } + + guard let mode = intValue(from: nodePayload["mode"]), mode >= 0 else { + throw CLIRunnerError.invalidCircuitDocument("Node at index \(index) has invalid 'mode'.") + } + + guard let slot = intValue(from: nodePayload["slot"]), slot >= 0 else { + throw CLIRunnerError.invalidCircuitDocument("Node at index \(index) has invalid 'slot'.") + } + + let paramA = doubleValue(from: nodePayload["param_a"]) ?? kind.defaultParamA + let paramB = doubleValue(from: nodePayload["param_b"]) ?? kind.defaultParamB + let id = stringValue(from: nodePayload["id"]).flatMap(UUID.init(uuidString:)) ?? UUID() + + nodes.append( + CircuitNode( + id: id, + kind: kind, + mode: mode, + slot: slot, + paramA: paramA, + paramB: paramB + ) + ) + } + + let derivedModeCount = max(1, (nodes.map(\.mode).max() ?? -1) + 1) + let derivedSlotCount = max(1, (nodes.map(\.slot).max() ?? -1) + 1) + let modeCount = max(intValue(from: payload["mode_count"]) ?? derivedModeCount, 1) + let slotCount = max(intValue(from: payload["slot_count"]) ?? derivedSlotCount, 1) + + let context = payload["context"] as? [String: Any] ?? [:] + let environmentRaw = stringValue(from: context["environment"])?.lowercased() + ?? stringValue(from: payload["environment"])?.lowercased() + let userRoleRaw = stringValue(from: context["role"])?.lowercased() + ?? stringValue(from: payload["role"])?.lowercased() + + return CircuitSnapshotDocument( + nodes: nodes, + modeCount: modeCount, + slotCount: slotCount, + schemaVersion: stringValue(from: payload["schema_version"]) ?? "1", + runtimeEngine: stringValue(from: payload["runtime_engine"]).flatMap(RuntimeEngineSelection.init(rawValue:)), + backend: stringValue(from: payload["backend"]).flatMap(BackendSelection.init(rawValue:)), + seed: stringValue(from: payload["seed"]), + cutoff: intValue(from: payload["cutoff"]), + foundryProfile: stringValue(from: payload["foundry_profile"]), + workspace: stringValue(from: context["workspace"]) ?? stringValue(from: payload["workspace"]), + project: stringValue(from: context["project"]) ?? stringValue(from: payload["project"]), + environment: environmentRaw.flatMap(EnvironmentBadge.init(rawValue:)), + userRole: userRoleRaw.flatMap(UserRoleBadge.init(rawValue:)), + kpiPolicyID: stringValue(from: context["kpi_policy_id"]) ?? stringValue(from: payload["kpi_policy_id"]), + kpiPolicyVersion: intValue(from: context["kpi_policy_version"]) ?? intValue(from: payload["kpi_policy_version"]) + ) + } + + private struct CalibratedNoiseProfileEnvelope: Decodable { + let profile: CalibratedNoiseTensorProfile + + enum CodingKeys: String, CodingKey { + case profile + case calibratedNoiseProfile = "calibrated_noise_profile" + case noiseProfile = "noise_profile" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let direct = try container.decodeIfPresent(CalibratedNoiseTensorProfile.self, forKey: .profile) { + profile = direct + return + } + if let calibrated = try container.decodeIfPresent(CalibratedNoiseTensorProfile.self, forKey: .calibratedNoiseProfile) { + profile = calibrated + return + } + if let noise = try container.decodeIfPresent(CalibratedNoiseTensorProfile.self, forKey: .noiseProfile) { + profile = noise + return + } + throw CLIRunnerError.invalidNoiseProfile( + "Expected `profile`, `calibrated_noise_profile`, or `noise_profile` object." + ) + } + } + + static func exportCircuitSnapshot(nodes: [CircuitNode], metadata: [String: String]) throws -> URL { + var payload: [String: Any] = [ + "schema_version": metadata["schema_version"] ?? "1", + "runtime_engine": metadata["runtime_engine"] ?? "swift", + "backend": metadata["backend"] ?? "hybrid", + "seed": metadata["seed"] ?? "123456789", + "cutoff": Int(metadata["cutoff"] ?? "20") ?? 20, + "foundry_profile": metadata["foundry_profile"] ?? "studio-default-v1", + "nodes": nodes.map { node in + [ + "id": node.id.uuidString, + "kind": node.kind.rawValue, + "mode": node.mode, + "slot": node.slot, + "param_a": node.paramA, + "param_b": node.paramB, + ] + }, + ] + + var context: [String: Any] = [:] + if let workspace = metadata["workspace"], !workspace.isEmpty { + context["workspace"] = workspace + } + if let project = metadata["project"], !project.isEmpty { + context["project"] = project + } + if let environment = metadata["environment"], !environment.isEmpty { + context["environment"] = environment + } + if let role = metadata["role"], !role.isEmpty { + context["role"] = role + } + if let foundryProfileID = metadata["foundry_profile_id"], !foundryProfileID.isEmpty { + context["foundry_profile_id"] = foundryProfileID + } + if let foundryProfileVersion = metadata["foundry_profile_version"], !foundryProfileVersion.isEmpty { + context["foundry_profile_version"] = foundryProfileVersion + } + if let kpiPolicyID = metadata["kpi_policy_id"], !kpiPolicyID.isEmpty { + context["kpi_policy_id"] = kpiPolicyID + } + if let kpiPolicyVersion = metadata["kpi_policy_version"], !kpiPolicyVersion.isEmpty { + context["kpi_policy_version"] = kpiPolicyVersion + } + if !context.isEmpty { + payload["context"] = context + } + + let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys]) + let exportDir = repoRootURL().appendingPathComponent("exports", isDirectory: true) + try FileManager.default.createDirectory(at: exportDir, withIntermediateDirectories: true) + let exportURL = exportDir.appendingPathComponent("swiftui_circuit_snapshot.json") + try data.write(to: exportURL, options: [.atomic]) + return exportURL + } + + private static func runtimeGatePayload(nodes: [CircuitNode], modeCount: Int) throws -> [[String: Any]] { + let sortedNodes = nodes.sorted { + if $0.slot == $1.slot { + return $0.mode < $1.mode + } + return $0.slot < $1.slot + } + + var payload: [[String: Any]] = [] + var consumedBeamSplitters = Set() + var emittedMeasurementCount = 0 + + func validateMode(_ mode: Int, kind: GateKind) throws -> Int { + guard mode >= 0, mode < modeCount else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Mode index \(mode) for '\(kind.displayName)' is out of bounds 0...\(max(0, modeCount - 1))." + ) + } + return mode + } + + for node in sortedNodes { + if let reason = node.kind.runtimeUnsupportedReason { + throw CLIRunnerError.invalidRuntimeCircuit("\(node.kind.displayName): \(reason)") + } + + switch node.kind { + case .sourceVacuum: + continue + case .sourceCoherent: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "displace", "q": node.paramA, "p": node.paramB, "mode": mode]) + case .sourceSqueezed: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "squeeze", "r": node.paramA, "mode": mode]) + case .phase: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "phase", "theta": node.paramA, "mode": mode]) + case .squeeze: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "squeeze", "r": node.paramA, "mode": mode]) + case .beamSplitter: + if consumedBeamSplitters.contains(node.id) { + continue + } + + let modeA = try validateMode(node.mode, kind: node.kind) + let partner = sortedNodes.first { + $0.id != node.id && + $0.kind == .beamSplitter && + $0.slot == node.slot && + !consumedBeamSplitters.contains($0.id) + } + + let modeB: Int + if let partner { + modeB = try validateMode(partner.mode, kind: partner.kind) + consumedBeamSplitters.insert(partner.id) + } else { + guard modeCount > 1 else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Beam splitter requires at least 2 modes." + ) + } + modeB = modeA < (modeCount - 1) ? (modeA + 1) : (modeA - 1) + } + + consumedBeamSplitters.insert(node.id) + + guard modeA != modeB else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Beam splitter at mode \(modeA), slot \(node.slot) needs two different modes." + ) + } + + payload.append([ + "type": "beam_splitter", + "theta": node.paramA, + "mode_a": min(modeA, modeB), + "mode_b": max(modeA, modeB), + ]) + case .displace: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "displace", "q": node.paramA, "p": node.paramB, "mode": mode]) + case .loss: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "loss", "eta": node.paramA, "mode": mode]) + case .thermalLoss: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "thermal_loss", "eta": node.paramA, "n_th": node.paramB, "mode": mode]) + case .measureHomodyne: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "measure_homodyne", "theta": node.paramA, "mode": mode]) + emittedMeasurementCount += 1 + case .measureHeterodyne: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "measure_heterodyne", "mode": mode]) + emittedMeasurementCount += 1 + case .injectFock: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "inject_fock", "mode": mode, "n": max(1, Int(node.paramA.rounded()))]) + case .injectCat: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "inject_cat", "mode": mode, "alpha": node.paramA]) + case .injectGkp: + let mode = try validateMode(node.mode, kind: node.kind) + payload.append(["type": "inject_gkp", "mode": mode, "delta": max(0.001, abs(node.paramA))]) + case .classicalControl: + let mode = try validateMode(node.mode, kind: node.kind) + guard emittedMeasurementCount > 0 else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Classical control requires at least one earlier measurement." + ) + } + payload.append([ + "type": "if_then", + "on": emittedMeasurementCount - 1, + "apply_type": "phase", + "apply_mode": mode, + "apply_theta": node.paramA, + ]) + } + } + + return payload + } + + private static func circuitSnapshotPayload( + nodes: [CircuitNode], + modeCount: Int, + slotCount: Int, + metadata: [String: String] + ) -> [String: Any] { + var payload: [String: Any] = [ + "document_type": "schrosim_circuit_snapshot_v1", + "saved_at": ISO8601DateFormatter().string(from: Date()), + "schema_version": metadata["schema_version"] ?? "1", + "runtime_engine": metadata["runtime_engine"] ?? "swift", + "backend": metadata["backend"] ?? "hybrid", + "seed": metadata["seed"] ?? "123456789", + "cutoff": Int(metadata["cutoff"] ?? "20") ?? 20, + "foundry_profile": metadata["foundry_profile"] ?? "studio-default-v1", + "mode_count": max(modeCount, 1), + "slot_count": max(slotCount, 1), + "nodes": nodes.map { node in + [ + "id": node.id.uuidString, + "kind": node.kind.rawValue, + "mode": node.mode, + "slot": node.slot, + "param_a": node.paramA, + "param_b": node.paramB, + ] + }, + ] + + var context: [String: Any] = [:] + if let workspace = metadata["workspace"], !workspace.isEmpty { + context["workspace"] = workspace + } + if let project = metadata["project"], !project.isEmpty { + context["project"] = project + } + if let environment = metadata["environment"], !environment.isEmpty { + context["environment"] = environment + } + if let role = metadata["role"], !role.isEmpty { + context["role"] = role + } + if let foundryProfileID = metadata["foundry_profile_id"], !foundryProfileID.isEmpty { + context["foundry_profile_id"] = foundryProfileID + } + if let foundryProfileVersion = metadata["foundry_profile_version"], !foundryProfileVersion.isEmpty { + context["foundry_profile_version"] = foundryProfileVersion + } + if let kpiPolicyID = metadata["kpi_policy_id"], !kpiPolicyID.isEmpty { + context["kpi_policy_id"] = kpiPolicyID + } + if let kpiPolicyVersion = metadata["kpi_policy_version"], !kpiPolicyVersion.isEmpty { + context["kpi_policy_version"] = kpiPolicyVersion + } + if !context.isEmpty { + payload["context"] = context + } + + return payload + } + + @MainActor + private static func writeCircuitReportPDF( + nodes: [CircuitNode], + modeCount: Int, + slotCount: Int, + metadata: [String: String], + to targetURL: URL + ) throws { + let pdfData = CircuitReportPDFRenderer.render( + nodes: nodes, + modeCount: modeCount, + slotCount: slotCount, + metadata: metadata + ) + try pdfData.write(to: targetURL, options: [.atomic]) + } + + private static func runCommand(arguments: [String]) throws -> (stdout: String, stderr: String, status: Int32) { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = arguments + process.currentDirectoryURL = repoRootURL() + + var environment = ProcessInfo.processInfo.environment + environment["NO_COLOR"] = "1" + process.environment = environment + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + + try process.run() + process.waitUntilExit() + + let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile() + let stdout = String(decoding: stdoutData, as: UTF8.self) + let stderr = String(decoding: stderrData, as: UTF8.self) + + return (stdout, stderr, process.terminationStatus) + } + + private static func decodeFromMixedOutput(_ output: String) throws -> T { + guard + let openIndex = output.firstIndex(of: "{"), + let closeIndex = output.lastIndex(of: "}") + else { + throw CLIRunnerError.malformedOutput(output) + } + + let jsonCandidate = String(output[openIndex...closeIndex]) + let data = Data(jsonCandidate.utf8) + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw CLIRunnerError.failedToDecodeJSON("\(error)") + } + } + + private static func intValue(from rawValue: Any?) -> Int? { + switch rawValue { + case let value as Int: + return value + case let value as Int64: + return Int(value) + case let value as Double: + return Int(value) + case let value as NSNumber: + return value.intValue + case let value as String: + return Int(value.trimmingCharacters(in: .whitespacesAndNewlines)) + default: + return nil + } + } + + private static func doubleValue(from rawValue: Any?) -> Double? { + switch rawValue { + case let value as Double: + return value + case let value as Int: + return Double(value) + case let value as NSNumber: + return value.doubleValue + case let value as String: + return Double(value.trimmingCharacters(in: .whitespacesAndNewlines)) + default: + return nil + } + } + + private static func stringValue(from rawValue: Any?) -> String? { + switch rawValue { + case let value as String: + let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + case let value as NSNumber: + return value.stringValue + default: + return nil + } + } + + private static func resolvedSavePanelURL(from panel: NSSavePanel) -> URL? { + let typedName = panel.nameFieldStringValue.trimmingCharacters(in: .whitespacesAndNewlines) + guard !typedName.isEmpty else { return panel.url } + let sanitizedName = typedName.replacingOccurrences(of: "/", with: "_") + + if let directoryURL = panel.directoryURL { + return directoryURL.appendingPathComponent(sanitizedName) + } + + guard let fallbackURL = panel.url else { return nil } + return fallbackURL.deletingLastPathComponent().appendingPathComponent(sanitizedName) + } + + private static func repoRootURL() -> URL { + let fileURL = URL(fileURLWithPath: #filePath) + return fileURL + .deletingLastPathComponent() // schrosim-studio + .deletingLastPathComponent() // Sources + .deletingLastPathComponent() // core-swift + .deletingLastPathComponent() // repo root + } + + private static func defaultFoundryRegistryPath() -> String { + repoRootURL().appendingPathComponent("docs/config/foundry_registry.json").path + } + + private static func validateProdRuntimePrerequisites() throws -> String { + let foundrySigningKey = ProcessInfo.processInfo.environment["SCHROSIM_FOUNDRY_HMAC_KEY"]? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !foundrySigningKey.isEmpty else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Prod mode requires SCHROSIM_FOUNDRY_HMAC_KEY for foundry signature verification." + ) + } + + let registryPath = defaultFoundryRegistryPath() + guard FileManager.default.fileExists(atPath: registryPath) else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Prod mode requires foundry registry at \(registryPath)." + ) + } + return registryPath + } +} diff --git a/core-swift/Sources/schrosim-studio/CircuitReportPDFRenderer.swift b/core-swift/Sources/schrosim-studio/CircuitReportPDFRenderer.swift new file mode 100644 index 0000000..9f480b6 --- /dev/null +++ b/core-swift/Sources/schrosim-studio/CircuitReportPDFRenderer.swift @@ -0,0 +1,366 @@ +import Foundation +import AppKit +import SwiftUI + +@MainActor +enum CircuitReportPDFRenderer { + static func render( + nodes: [CircuitNode], + modeCount: Int, + slotCount: Int, + metadata: [String: String] + ) -> Data { + let safeModeCount = max(modeCount, 1) + let safeSlotCount = max(slotCount, 1) + let layout = CircuitReportLayout() + let canvasWidth = layout.canvasWidth(slotCount: safeSlotCount) + let canvasHeight = layout.canvasHeight(modeCount: safeModeCount) + let componentLines = componentLineItems(nodes: nodes) + let componentHeight = min(280, CGFloat(max(componentLines.count, 1)) * 18 + 54) + let reportWidth = max(980, canvasWidth + 56) + let reportHeight = 222 + canvasHeight + componentHeight + + let reportView = CircuitReportPDFView( + nodes: nodes, + modeCount: safeModeCount, + slotCount: safeSlotCount, + metadata: metadata, + layout: layout, + canvasWidth: canvasWidth, + canvasHeight: canvasHeight, + componentLines: componentLines + ) + .frame(width: reportWidth, height: reportHeight, alignment: .topLeading) + .background(Color.white) + + let hostingView = NSHostingView(rootView: reportView) + hostingView.frame = NSRect(x: 0, y: 0, width: reportWidth, height: reportHeight) + hostingView.layoutSubtreeIfNeeded() + return hostingView.dataWithPDF(inside: hostingView.bounds) + } + + private static func componentLineItems(nodes: [CircuitNode]) -> [String] { + let sortedNodes = nodes.sorted { + if $0.slot == $1.slot { return $0.mode < $1.mode } + return $0.slot < $1.slot + } + + guard !sortedNodes.isEmpty else { + return ["(empty)"] + } + + return sortedNodes.map { node in + let paramAText = String(format: "%.3f", node.paramA) + let paramBText = String(format: "%.3f", node.paramB) + return "t\(node.slot) m\(node.mode) \(node.kind.rawValue) a=\(paramAText) b=\(paramBText)" + } + } +} + +private struct CircuitReportLayout { + let laneLabelWidth: CGFloat = 110 + let slotWidth: CGFloat = 140 + let slotHeight: CGFloat = 68 + let rowSpacing: CGFloat = 10 + let columnSpacing: CGFloat = 8 + let canvasPadding: CGFloat = 10 + let timelineHeaderHeight: CGFloat = 24 + + func canvasWidth(slotCount: Int) -> CGFloat { + let safeSlotCount = max(slotCount, 1) + return canvasPadding * 2 + + laneLabelWidth + + columnSpacing + + CGFloat(safeSlotCount) * slotWidth + + CGFloat(max(safeSlotCount - 1, 0)) * columnSpacing + } + + func canvasHeight(modeCount: Int) -> CGFloat { + let safeModeCount = max(modeCount, 1) + return canvasPadding * 2 + + timelineHeaderHeight + + CGFloat(safeModeCount) * slotHeight + + CGFloat(max(safeModeCount - 1, 0)) * rowSpacing + } +} + +private struct CircuitReportPDFView: View { + let nodes: [CircuitNode] + let modeCount: Int + let slotCount: Int + let metadata: [String: String] + let layout: CircuitReportLayout + let canvasWidth: CGFloat + let canvasHeight: CGFloat + let componentLines: [String] + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + Text("SchroSIM Circuit Report") + .font(.system(size: 28, weight: .semibold)) + + HStack(alignment: .top, spacing: 36) { + VStack(alignment: .leading, spacing: 4) { + metadataLine("workspace", metadata["workspace"] ?? "n/a") + metadataLine("project", metadata["project"] ?? "n/a") + metadataLine("backend", metadata["backend"] ?? "hybrid") + metadataLine("foundry_profile", metadata["foundry_profile"] ?? "n/a") + } + VStack(alignment: .leading, spacing: 4) { + metadataLine("seed", metadata["seed"] ?? "n/a") + metadataLine("cutoff", metadata["cutoff"] ?? "n/a") + metadataLine("modes", "\(modeCount)") + metadataLine("slots", "\(slotCount)") + } + Spacer(minLength: 0) + } + + CircuitReportCanvasView( + nodes: nodes, + modeCount: modeCount, + slotCount: slotCount, + layout: layout + ) + .frame(width: canvasWidth, height: canvasHeight, alignment: .topLeading) + + VStack(alignment: .leading, spacing: 4) { + Text("Placed Components (slot, mode, kind, params):") + .font(.system(size: 13, weight: .semibold, design: .monospaced)) + + ForEach(Array(componentLines.enumerated()), id: \.offset) { item in + Text(item.element) + .font(.system(size: 12, weight: .regular, design: .monospaced)) + .foregroundStyle(.primary) + } + } + } + .padding(24) + } + + private func metadataLine(_ key: String, _ value: String) -> some View { + Text("\(key): \(value)") + .font(.system(size: 13, weight: .regular, design: .monospaced)) + .foregroundStyle(.primary) + } +} + +private struct CircuitReportCanvasView: View { + let nodes: [CircuitNode] + let modeCount: Int + let slotCount: Int + let layout: CircuitReportLayout + + var body: some View { + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 10) + .fill(Color(red: 0.965, green: 0.972, blue: 0.985)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.black.opacity(0.10), lineWidth: 1) + ) + + CircuitReportTimelineView( + slotCount: slotCount, + layout: layout + ) + .allowsHitTesting(false) + + CircuitReportConnectorOverlayView( + modeCount: modeCount, + slotCount: slotCount, + nodes: nodes, + layout: layout + ) + .allowsHitTesting(false) + + VStack(alignment: .leading, spacing: layout.rowSpacing) { + ForEach(0..") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 6) + .padding(.vertical, 6) + .frame(width: layout.laneLabelWidth, alignment: .leading) + + ForEach(0.. CircuitNode? { + nodes.first { $0.mode == mode && $0.slot == slot } + } +} + +private struct CircuitReportSlotCellView: View { + let node: CircuitNode? + let size: CGSize + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + if let node { + HStack(spacing: 8) { + Image(systemName: node.kind.systemIcon) + .font(.caption.weight(.semibold)) + .frame(width: 18) + Text(node.kind.displayName) + .font(.system(size: 12, weight: .semibold)) + .lineLimit(1) + } + + Text(node.subtitle) + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(.secondary) + .lineLimit(1) + } else { + VStack(spacing: 2) { + Text("Drop") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.blue) + Text("+") + .font(.system(size: 16, weight: .bold)) + .foregroundStyle(.blue) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .padding(8) + .frame(width: size.width, height: size.height, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(node == nil ? Color.white.opacity(0.65) : Color.white) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke( + node == nil ? Color.blue.opacity(0.45) : Color.black.opacity(0.18), + style: StrokeStyle(lineWidth: 1, dash: node == nil ? [5, 4] : []) + ) + ) + ) + } +} + +private struct CircuitReportTimelineView: View { + let slotCount: Int + let layout: CircuitReportLayout + + var body: some View { + Canvas { context, _ in + let safeSlotCount = max(slotCount, 1) + let axisY = layout.canvasPadding + 18 + let firstX = layout.canvasPadding + layout.laneLabelWidth + layout.columnSpacing + let stepX = layout.slotWidth + layout.columnSpacing + let lastX = firstX + + CGFloat(safeSlotCount) * layout.slotWidth + + CGFloat(max(safeSlotCount - 1, 0)) * layout.columnSpacing + + var axisPath = Path() + axisPath.move(to: CGPoint(x: firstX, y: axisY)) + axisPath.addLine(to: CGPoint(x: lastX, y: axisY)) + context.stroke(axisPath, with: .color(Color.secondary.opacity(0.72)), lineWidth: 1) + + for slot in 0.. 0, slotCount > 0 else { return } + let groupedBySlot = Dictionary(grouping: nodes) { $0.slot } + let baseWidth: CGFloat = 1.4 + let laneColor = Color.black.opacity(0.50) + let couplerColor = Color.black.opacity(0.65) + + for mode in 0.. 1 else { continue } + let x = slotCenterX(slot) + let sortedModes = slotNodes.map(\.mode).sorted() + let y0 = laneCenterY(sortedModes.first ?? 0) + let y1 = laneCenterY(sortedModes.last ?? 0) + + var couplerPath = Path() + couplerPath.move(to: CGPoint(x: x, y: y0)) + couplerPath.addLine(to: CGPoint(x: x, y: y1)) + context.stroke( + couplerPath, + with: .color(couplerColor), + style: StrokeStyle(lineWidth: baseWidth, lineCap: .round) + ) + + for mode in Set(slotNodes.map(\.mode)) { + let y = laneCenterY(mode) + let rect = CGRect(x: x - 2.3, y: y - 2.3, width: 4.6, height: 4.6) + context.fill(Path(ellipseIn: rect), with: .color(Color.black.opacity(0.88))) + } + } + } + } + + private var leftLaneX: CGFloat { + layout.canvasPadding + layout.laneLabelWidth + layout.columnSpacing + 6 + } + + private var rightLaneX: CGFloat { + leftLaneX + + CGFloat(slotCount) * layout.slotWidth + + CGFloat(max(slotCount - 1, 0)) * layout.columnSpacing + - 12 + } + + private func laneCenterY(_ mode: Int) -> CGFloat { + layout.canvasPadding + + layout.timelineHeaderHeight + + CGFloat(mode) * (layout.slotHeight + layout.rowSpacing) + + layout.slotHeight / 2 + } + + private func slotCenterX(_ slot: Int) -> CGFloat { + leftLaneX + CGFloat(slot) * (layout.slotWidth + layout.columnSpacing) + layout.slotWidth / 2 + } +} diff --git a/core-swift/Sources/schrosim-studio/SchroSIMStudioApp.swift b/core-swift/Sources/schrosim-studio/SchroSIMStudioApp.swift new file mode 100644 index 0000000..72cbd28 --- /dev/null +++ b/core-swift/Sources/schrosim-studio/SchroSIMStudioApp.swift @@ -0,0 +1,16 @@ +import SwiftUI + +@main +struct SchroSIMStudioApp: App { + @StateObject private var store = StudioStore() + + var body: some Scene { + WindowGroup("SchroSIM") { + StudioAppShellView() + .environmentObject(store) + .frame(minWidth: 1640, minHeight: 900) + .preferredColorScheme(store.theme == .black ? .dark : .light) + } + .defaultSize(width: 1720, height: 980) + } +} diff --git a/core-swift/Sources/schrosim-studio/StudioStore.swift b/core-swift/Sources/schrosim-studio/StudioStore.swift new file mode 100644 index 0000000..7f966d4 --- /dev/null +++ b/core-swift/Sources/schrosim-studio/StudioStore.swift @@ -0,0 +1,3498 @@ +import Foundation +import SwiftUI +import SchroSIM + +@MainActor +final class StudioStore: ObservableObject { + private struct WorkspaceProjectContext { + let workspace: String + let project: String + let defaultBackend: BackendSelection + let defaultSeed: String + let defaultCircuitPath: String + let devFoundryProfile: String + let stageFoundryProfile: String + let prodFoundryProfile: String + } + + private struct FoundryProfileRef { + let profileID: String + let version: Int + } + + private struct FoundryRegistryDocument: Decodable { + let profiles: [FoundryRegistryProfile] + } + + private struct FoundryRegistryProfile: Decodable { + let profileID: String + let version: Int + let status: String + let spec: FoundryRegistrySpec + + enum CodingKeys: String, CodingKey { + case profileID = "profile_id" + case version + case status + case spec + } + } + + private struct FoundryRegistrySpec: Decodable { + let name: String + + enum CodingKeys: String, CodingKey { + case name + } + } + + private struct FoundryRegistryCatalog { + let allProfileNames: [String] + let approvedProfileNames: Set + let profileRefsByName: [String: FoundryProfileRef] + } + + private struct RobustnessContext { + let nodes: [CircuitNode] + let modeCount: Int + let backend: BackendSelection + let seed: String + let cutoff: Int + let schemaVersion: String + let foundryProfile: String + let environment: EnvironmentBadge + let foundryProfileID: String? + let foundryProfileVersion: Int? + let runtimeEngine: RuntimeEngineSelection + let noiseModel: MonteCarloNoiseModel + let calibratedProfile: CalibratedNoiseTensorProfile? + } + + private struct KPIPolicyCatalogDocument: Decodable { + let policies: [KPIPolicyDefinition] + } + + private struct SeededGenerator: RandomNumberGenerator { + private var state: UInt64 + + init(seed: UInt64) { + state = seed == 0 ? 0x9E37_79B9_7F4A_7C15 : seed + } + + mutating func next() -> UInt64 { + state &+= 0x9E37_79B9_7F4A_7C15 + var z = state + z = (z ^ (z >> 30)) &* 0xBF58_476D_1CE4_E5B9 + z = (z ^ (z >> 27)) &* 0x94D0_49BB_1331_11EB + return z ^ (z >> 31) + } + } + + private static let fallbackFoundryProfiles = [ + "studio-default-v1", + "studio-default-v2", + "experimental-lab-v1", + ] + + private static let cvGaussianProductionPolicyID = "cv_gaussian_production_v1" + private static let cvQECGaussianPolicyID = "cv_qec_gaussian_v1" + private static let cvFockValidationPolicyID = "cv_fock_validation_v1" + + private static let fallbackKPIPolicies: [KPIPolicyDefinition] = [ + KPIPolicyDefinition( + id: StudioStore.cvGaussianProductionPolicyID, + name: "CV Gaussian Production", + version: 1, + targetRepresentation: "gaussian_phase_space", + summary: "Production readiness for Gaussian CV circuits using deterministic parity and calibrated-noise robustness bounds.", + references: [ + "Open-source studio qualification standard: CV-GAUSS-PROD-v1" + ], + thresholds: KPIPolicyThresholds( + minSuccessRate: 0.95, + maxMeanPhotonP95Delta: 0.050, + maxStateP95Delta: 0.020, + maxMeasurementMismatchRate: 0.01, + minMeanPhotonNumber: 0.0, + maxMeanPhotonNumber: 250.0, + minSqueezingDB: 1.0, + minGaussianPurity: 0.50, + maxFockNormalizationError: nil, + maxLogicalErrorRate: nil, + minSuppressionFactor: nil, + minBreakEvenGain: nil + ) + ), + KPIPolicyDefinition( + id: StudioStore.cvQECGaussianPolicyID, + name: "CV QEC Gaussian", + version: 1, + targetRepresentation: "gaussian_phase_space", + summary: "QEC policy for Gaussian/Hybrid backends focused on logical error rate, suppression against physical shift noise, and break-even gain.", + references: [ + "Open-source studio qualification standard: CV-QEC-GAUSS-v1" + ], + thresholds: KPIPolicyThresholds( + minSuccessRate: 0.90, + maxMeanPhotonP95Delta: 0.120, + maxStateP95Delta: 0.080, + maxMeasurementMismatchRate: 0.05, + minMeanPhotonNumber: 0.0, + maxMeanPhotonNumber: 400.0, + minSqueezingDB: nil, + minGaussianPurity: nil, + maxFockNormalizationError: nil, + maxLogicalErrorRate: 0.34, + minSuppressionFactor: 1.0, + minBreakEvenGain: 0.0 + ) + ), + KPIPolicyDefinition( + id: StudioStore.cvFockValidationPolicyID, + name: "CV Fock Validation", + version: 1, + targetRepresentation: "fock_number_basis", + summary: "Validation policy for Fock-basis runs emphasizing normalization fidelity and robust runtime consistency.", + references: [ + "Open-source studio qualification standard: CV-FOCK-VAL-v1" + ], + thresholds: KPIPolicyThresholds( + minSuccessRate: 0.95, + maxMeanPhotonP95Delta: 0.080, + maxStateP95Delta: 0.030, + maxMeasurementMismatchRate: 0.01, + minMeanPhotonNumber: 0.0, + maxMeanPhotonNumber: 200.0, + minSqueezingDB: nil, + minGaussianPurity: nil, + maxFockNormalizationError: 1e-6, + maxLogicalErrorRate: nil, + minSuppressionFactor: nil, + minBreakEvenGain: nil + ) + ), + ] + + private static let scientificCircuitPresetCatalog: [ScientificCircuitPreset] = [ + ScientificCircuitPreset( + id: "crespi-2013-five-mode-boson-sampling", + title: "Crespi et al. (2013) Five-Mode Boson Sampling Chip", + citation: "A. Crespi et al., Integrated multimode interferometers with arbitrary designs for photonic boson sampling, Nat. Photonics 7, 545-549 (2013).", + doiURL: "https://doi.org/10.1038/nphoton.2013.112", + sourceURL: "https://www.nature.com/articles/nphoton.2013.112", + mappingNote: "Mapped to SchroSIM primitives (squeeze, phase, beam_splitter, loss, measurement). This is a physically-motivated surrogate layout, not a mask-exact foundry replay.", + recommendedBackend: .gaussian, + suggestedSeed: "20130545", + modeCount: 5, + slotCount: 8, + nodes: [ + CircuitNode(kind: .sourceSqueezed, mode: 0, slot: 0, paramA: 0.82), + CircuitNode(kind: .sourceSqueezed, mode: 1, slot: 0, paramA: 0.76), + CircuitNode(kind: .sourceSqueezed, mode: 2, slot: 0, paramA: 0.72), + + CircuitNode(kind: .beamSplitter, mode: 0, slot: 1, paramA: 0.61), + CircuitNode(kind: .beamSplitter, mode: 1, slot: 1, paramA: 0.61), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 2, paramA: 0.55), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 2, paramA: 0.55), + + CircuitNode(kind: .phase, mode: 1, slot: 3, paramA: 0.21), + CircuitNode(kind: .phase, mode: 4, slot: 3, paramA: -0.16), + + CircuitNode(kind: .beamSplitter, mode: 1, slot: 4, paramA: 0.42), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 4, paramA: 0.42), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 5, paramA: 0.37), + CircuitNode(kind: .beamSplitter, mode: 4, slot: 5, paramA: 0.37), + + CircuitNode(kind: .loss, mode: 0, slot: 6, paramA: 0.93), + CircuitNode(kind: .loss, mode: 2, slot: 6, paramA: 0.95), + CircuitNode(kind: .thermalLoss, mode: 4, slot: 6, paramA: 0.91, paramB: 0.05), + + CircuitNode(kind: .measureHomodyne, mode: 0, slot: 7, paramA: 0.0), + CircuitNode(kind: .measureHomodyne, mode: 1, slot: 7, paramA: 0.0), + CircuitNode(kind: .measureHeterodyne, mode: 2, slot: 7), + ] + ), + ScientificCircuitPreset( + id: "spagnolo-2014-validation-chip", + title: "Spagnolo et al. (2014) Boson Sampling Validation Circuit", + citation: "N. Spagnolo et al., Experimental validation of photonic boson sampling, Nat. Photonics 8, 615-620 (2014).", + doiURL: "https://doi.org/10.1038/nphoton.2014.135", + sourceURL: "https://www.nature.com/articles/nphoton.2014.135", + mappingNote: "Validation-oriented surrogate mapped to available SchroSIM gates for compile/run checks and reproducibility workflows.", + recommendedBackend: .gaussian, + suggestedSeed: "20140615", + modeCount: 6, + slotCount: 9, + nodes: [ + CircuitNode(kind: .sourceSqueezed, mode: 0, slot: 0, paramA: 0.68), + CircuitNode(kind: .sourceSqueezed, mode: 1, slot: 0, paramA: 0.66), + CircuitNode(kind: .sourceSqueezed, mode: 2, slot: 0, paramA: 0.71), + CircuitNode(kind: .sourceSqueezed, mode: 3, slot: 0, paramA: 0.63), + + CircuitNode(kind: .beamSplitter, mode: 0, slot: 1, paramA: 0.48), + CircuitNode(kind: .beamSplitter, mode: 1, slot: 1, paramA: 0.48), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 2, paramA: 0.52), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 2, paramA: 0.52), + + CircuitNode(kind: .phase, mode: 1, slot: 3, paramA: 0.24), + CircuitNode(kind: .phase, mode: 2, slot: 3, paramA: -0.31), + + CircuitNode(kind: .beamSplitter, mode: 1, slot: 4, paramA: 0.47), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 4, paramA: 0.47), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 5, paramA: 0.41), + CircuitNode(kind: .beamSplitter, mode: 4, slot: 5, paramA: 0.41), + + CircuitNode(kind: .loss, mode: 0, slot: 6, paramA: 0.92), + CircuitNode(kind: .loss, mode: 4, slot: 6, paramA: 0.90), + CircuitNode(kind: .thermalLoss, mode: 5, slot: 6, paramA: 0.88, paramB: 0.08), + + CircuitNode(kind: .measureHomodyne, mode: 0, slot: 7, paramA: 0.0), + CircuitNode(kind: .measureHeterodyne, mode: 2, slot: 7), + CircuitNode(kind: .measureHomodyne, mode: 4, slot: 7, paramA: 0.0), + CircuitNode(kind: .classicalControl, mode: 3, slot: 8, paramA: 0.20), + ] + ), + ScientificCircuitPreset( + id: "hoch-2022-reconfigurable-3d-chip", + title: "Hoch et al. (2022) Reconfigurable 3D Photonic Circuit", + citation: "F. Hoch et al., Reconfigurable continuously-coupled 3D photonic circuit for boson sampling experiments, npj Quantum Inf. 8, 55 (2022).", + doiURL: "https://doi.org/10.1038/s41534-022-00568-6", + sourceURL: "https://www.nature.com/articles/s41534-022-00568-6", + mappingNote: "Topology-inspired 8-mode surrogate preserving staged coupling and phase blocks using SchroSIM's current runtime gate set.", + recommendedBackend: .hybrid, + suggestedSeed: "20220568", + modeCount: 8, + slotCount: 10, + nodes: [ + CircuitNode(kind: .sourceSqueezed, mode: 0, slot: 0, paramA: 0.74), + CircuitNode(kind: .sourceSqueezed, mode: 2, slot: 0, paramA: 0.70), + CircuitNode(kind: .sourceSqueezed, mode: 4, slot: 0, paramA: 0.67), + CircuitNode(kind: .sourceSqueezed, mode: 6, slot: 0, paramA: 0.73), + + CircuitNode(kind: .beamSplitter, mode: 0, slot: 1, paramA: 0.44), + CircuitNode(kind: .beamSplitter, mode: 1, slot: 1, paramA: 0.44), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 1, paramA: 0.51), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 1, paramA: 0.51), + CircuitNode(kind: .beamSplitter, mode: 4, slot: 1, paramA: 0.39), + CircuitNode(kind: .beamSplitter, mode: 5, slot: 1, paramA: 0.39), + CircuitNode(kind: .beamSplitter, mode: 6, slot: 1, paramA: 0.57), + CircuitNode(kind: .beamSplitter, mode: 7, slot: 1, paramA: 0.57), + + CircuitNode(kind: .phase, mode: 1, slot: 2, paramA: 0.18), + CircuitNode(kind: .phase, mode: 3, slot: 2, paramA: -0.22), + CircuitNode(kind: .phase, mode: 5, slot: 2, paramA: 0.11), + CircuitNode(kind: .phase, mode: 7, slot: 2, paramA: -0.15), + + CircuitNode(kind: .beamSplitter, mode: 1, slot: 3, paramA: 0.46), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 3, paramA: 0.46), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 3, paramA: 0.33), + CircuitNode(kind: .beamSplitter, mode: 4, slot: 3, paramA: 0.33), + CircuitNode(kind: .beamSplitter, mode: 5, slot: 3, paramA: 0.42), + CircuitNode(kind: .beamSplitter, mode: 6, slot: 3, paramA: 0.42), + + CircuitNode(kind: .displace, mode: 0, slot: 4, paramA: 0.12, paramB: 0.04), + CircuitNode(kind: .displace, mode: 7, slot: 4, paramA: -0.10, paramB: 0.02), + + CircuitNode(kind: .beamSplitter, mode: 0, slot: 5, paramA: 0.36), + CircuitNode(kind: .beamSplitter, mode: 3, slot: 5, paramA: 0.36), + CircuitNode(kind: .beamSplitter, mode: 4, slot: 6, paramA: 0.43), + CircuitNode(kind: .beamSplitter, mode: 7, slot: 6, paramA: 0.43), + + CircuitNode(kind: .loss, mode: 1, slot: 7, paramA: 0.94), + CircuitNode(kind: .loss, mode: 5, slot: 7, paramA: 0.93), + CircuitNode(kind: .thermalLoss, mode: 7, slot: 7, paramA: 0.90, paramB: 0.07), + + CircuitNode(kind: .measureHomodyne, mode: 0, slot: 8, paramA: 0.0), + CircuitNode(kind: .measureHeterodyne, mode: 3, slot: 8), + CircuitNode(kind: .measureHomodyne, mode: 6, slot: 8, paramA: 0.0), + CircuitNode(kind: .classicalControl, mode: 2, slot: 9, paramA: -0.19), + ] + ), + ] + + private static let workspaceProjectContexts: [WorkspaceProjectContext] = [ + WorkspaceProjectContext( + workspace: "Quantum Foundry", + project: "Boson Sampling", + defaultBackend: .hybrid, + defaultSeed: "123456789", + defaultCircuitPath: "docs/examples/foundry_loss_map.json", + devFoundryProfile: "experimental-lab-v1", + stageFoundryProfile: "studio-default-v2", + prodFoundryProfile: "studio-default-v1" + ), + WorkspaceProjectContext( + workspace: "Quantum Foundry", + project: "Quantum Error Correction", + defaultBackend: .hybrid, + defaultSeed: "42424242", + defaultCircuitPath: "docs/examples/cv/qec_single_logical_gkp_memory_mvp.json", + devFoundryProfile: "experimental-lab-v1", + stageFoundryProfile: "studio-default-v2", + prodFoundryProfile: "studio-default-v1" + ), + WorkspaceProjectContext( + workspace: "Device Lab", + project: "Boson Sampling", + defaultBackend: .gaussian, + defaultSeed: "27182818", + defaultCircuitPath: "docs/examples/foundry_loss_map.json", + devFoundryProfile: "experimental-lab-v1", + stageFoundryProfile: "studio-default-v2", + prodFoundryProfile: "studio-default-v1" + ), + WorkspaceProjectContext( + workspace: "Device Lab", + project: "Quantum Error Correction", + defaultBackend: .gaussian, + defaultSeed: "31415926", + defaultCircuitPath: "docs/examples/cv/qec_single_logical_gkp_memory_mvp.json", + devFoundryProfile: "experimental-lab-v1", + stageFoundryProfile: "studio-default-v2", + prodFoundryProfile: "studio-default-v1" + ), + ] + + @Published var workspace: String = "Quantum Foundry" { + didSet { + handleWorkspaceOrProjectChange(triggerReason: "Workspace changed to \(workspace)") + } + } + @Published var project: String = "Boson Sampling" { + didSet { + handleWorkspaceOrProjectChange(triggerReason: "Project changed to \(project)") + } + } + @Published var environment: EnvironmentBadge = .dev { + didSet { + handleEnvironmentChange() + } + } + @Published var userRole: UserRoleBadge = .editor { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: userRole, + reason: "Role changed to \(userRole.rawValue)", + revalidate: true + ) + } + } + @Published var theme: AppTheme = .black + + @Published var backend: BackendSelection = .hybrid { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: backend, + reason: "Backend changed to \(backend.rawValue)", + revalidate: true + ) + if oldValue != backend { + _ = enforceKPIPolicyForSelectedBackend() + } + } + } + @Published var runtimeEngine: RuntimeEngineSelection = .swift { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: runtimeEngine, + reason: "Runtime engine changed to \(runtimeEngine.rawValue)", + revalidate: true + ) + } + } + @Published var seed: String = "123456789" { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: seed, + reason: "Seed changed", + revalidate: false + ) + } + } + @Published var cutoff: Int = 20 { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: cutoff, + reason: "Cutoff changed to \(cutoff)", + revalidate: false + ) + } + } + @Published var schemaVersion: String = "1" { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: schemaVersion, + reason: "Schema version changed", + revalidate: false + ) + } + } + @Published var foundryProfile: String = "studio-default-v1" { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: foundryProfile, + reason: "Foundry profile changed to \(foundryProfile)", + revalidate: true + ) + } + } + @Published var foundryProfiles: [String] = StudioStore.fallbackFoundryProfiles + @Published var circuitJSONPath: String = "docs/examples/foundry_loss_map.json" { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: circuitJSONPath, + reason: "Circuit path changed", + revalidate: false + ) + } + } + @Published var selectedScientificCircuitPresetID: String = StudioStore.scientificCircuitPresetCatalog.first?.id ?? "" + @Published var kpiPolicies: [KPIPolicyDefinition] = StudioStore.fallbackKPIPolicies + @Published var selectedKPIPolicyID: String = StudioStore.fallbackKPIPolicies.first?.id ?? "" { + didSet { + handleConfigurationChange( + oldValue: oldValue, + newValue: selectedKPIPolicyID, + reason: "KPI policy changed", + revalidate: false + ) + if oldValue != selectedKPIPolicyID { + stateHealthSummary = nil + bosonicSignoffSummary = nil + let switchedBackend = enforceBackendForSelectedKPIPolicy() + if !switchedBackend { + validateAction() + } + } + } + } + + @Published var paletteSearch: String = "" + @Published var paletteFilter: PaletteFilter = .all + @Published var showGrid: Bool = true + @Published var showConnectors: Bool = true + @Published var showPhotonFlow: Bool = false + @Published var showSelection: Bool = true + @Published var showValidation: Bool = true + + @Published var modeCount: Int = 4 + @Published var slotCount: Int = 6 { + didSet { + handleSlotCountChange(oldValue: oldValue) + } + } + @Published var nodes: [CircuitNode] = [] + @Published var selectedNodeID: UUID? + @Published var selectedModeIndex: Int? + + @Published var rightInspectorTab: RightInspectorTab = .selectionInspector + @Published var bottomTab: BottomTab = .validationConsole + + @Published var validationIssues: [ValidationIssue] = [] + @Published var runOutput: [String] = ["Run output panel is ready."] + @Published var auditTrail: [String] = ["Session started"] + @Published var statusText: String = "Ready" + @Published var isDirty: Bool = false + @Published var isRunning: Bool = false + @Published var runtimePhase: RuntimePhase = .standby + @Published var isAnalysisPopupPresented: Bool = false + @Published var isRunBlockedAlertPresented: Bool = false + @Published var runBlockedAlertMessage: String = "Validation failed. Check your gates." + + @Published var lastRunResponse: RunResponse? + @Published var lastTraceResponse: TraceResponse? + @Published var runtimeParitySummary: RuntimeParitySummary? + @Published var stateHealthSummary: StateHealthSummary? + @Published var bosonicSignoffSummary: BosonicSignoffSummary? + @Published var lastExportPath: String? + @Published var lastSavedDocumentPath: String? + @Published var noiseModel: MonteCarloNoiseModel = .standard + @Published var useCalibratedNoiseProfile: Bool = true + @Published var calibratedNoiseProfile: CalibratedNoiseTensorProfile? + @Published var calibratedNoiseProfilePath: String? + @Published var showConfidenceBands: Bool = true + @Published var isMonteCarloRunning: Bool = false + @Published var monteCarloCompletedSamples: Int = 0 + @Published var monteCarloRunResponses: [RunResponse] = [] + @Published var monteCarloTraceResponses: [TraceResponse] = [] + @Published var monteCarloLastRunTimestamp: Date? + + private var isApplyingContext = false + private var lastSavedDocumentURL: URL? + private var approvedProdFoundryProfiles: Set = [] + private var foundryProfileRefsByName: [String: FoundryProfileRef] = [:] + + let compilerName = "foundry-aware-ir-v1" + let contractionPolicy = "hybrid_auto" + + var workspaces: [String] { + Self.uniqueOrdered(Self.workspaceProjectContexts.map(\.workspace)) + } + + var projects: [String] { + availableProjects(for: workspace) + } + + var scientificCircuitPresets: [ScientificCircuitPreset] { + Self.scientificCircuitPresetCatalog + } + + var selectedScientificCircuitPreset: ScientificCircuitPreset? { + Self.scientificCircuitPresetCatalog.first { $0.id == selectedScientificCircuitPresetID } + } + + var selectedKPIPolicy: KPIPolicyDefinition? { + kpiPolicies.first { $0.id == selectedKPIPolicyID } ?? kpiPolicies.first + } + + var isBosonicSignoffRequired: Bool { + requiresBosonicSignoff + } + + private let fockRuntimeSupportedKinds: Set = [ + .sourceVacuum, + .sourceCoherent, + .phase, + .displace, + .injectFock, + .injectCat, + ] + + init() { + nodes = [ + CircuitNode(kind: .squeeze, mode: 0, slot: 1, paramA: 0.8, paramB: 0.0), + CircuitNode(kind: .beamSplitter, mode: 0, slot: 3, paramA: 0.25, paramB: 0.0), + CircuitNode(kind: .phase, mode: 1, slot: 0, paramA: 0.13, paramB: 0.0), + CircuitNode(kind: .displace, mode: 1, slot: 3, paramA: 0.35, paramB: 0.20), + CircuitNode(kind: .beamSplitter, mode: 2, slot: 1, paramA: 0.10, paramB: 0.0), + CircuitNode(kind: .phase, mode: 3, slot: 1, paramA: -0.22, paramB: 0.0), + ] + let catalog = Self.loadFoundryRegistryCatalog() + foundryProfiles = catalog.allProfileNames + approvedProdFoundryProfiles = catalog.approvedProfileNames + foundryProfileRefsByName = catalog.profileRefsByName + kpiPolicies = Self.loadKPIPolicyCatalog() + selectedKPIPolicyID = kpiPolicies.first?.id ?? "" + + applySelectionContext( + triggerReason: "Initialization", + markDirty: false, + appendContextLog: false, + revalidate: false + ) + selectedNodeID = nodes.first?.id + validateAction() + } + + var globalStatusText: String { + let dirtyText = isDirty ? "dirty" : "clean" + let syncText = isDirty ? "local changes" : "synced" + return "\(dirtyText) | autosave: on | sync: \(syncText) | \(workspace)/\(project) | \(environment.rawValue) | \(userRole.rawValue)" + } + + var calibratedNoiseProfileDisplayName: String { + guard let profile = calibratedNoiseProfile else { return "None" } + return "\(profile.deviceName) (\(profile.deviceID))" + } + + var calibratedNoiseModeCoverageText: String { + guard let profile = calibratedNoiseProfile else { return "Coverage: n/a" } + let declaredModes = max(profile.modeCount ?? modeCount, 1) + let effectiveModes = max(modeCount, 1) + return "Coverage: \(min(declaredModes, effectiveModes))/\(effectiveModes) modes" + } + + var groupedPalette: [(PaletteSection, [GateKind])] { + PaletteSection.allCases.compactMap { section in + let components = GateKind.allCases.filter { kind in + kind.section == section && matchesPaletteFilter(kind) && matchesSearch(kind) + } + return components.isEmpty ? nil : (section, components) + } + } + + var canDeleteSelection: Bool { + selectedNodeID != nil || validSelectedModeIndex != nil + } + + private func handleWorkspaceOrProjectChange(triggerReason: String) { + guard !isApplyingContext else { return } + applySelectionContext(triggerReason: triggerReason, markDirty: true, appendContextLog: true) + } + + private func handleEnvironmentChange() { + guard !isApplyingContext else { return } + applySelectionContext( + triggerReason: "Environment changed to \(environment.rawValue.uppercased())", + markDirty: true, + appendContextLog: true + ) + } + + private var validSelectedModeIndex: Int? { + guard let selectedModeIndex else { return nil } + guard selectedModeIndex >= 0, selectedModeIndex < modeCount else { return nil } + return selectedModeIndex + } + + private func handleConfigurationChange( + oldValue: T, + newValue: T, + reason: String, + revalidate: Bool + ) { + guard !isApplyingContext else { return } + guard oldValue != newValue else { return } + markDirty(reason) + if revalidate { + validateAction() + } + } + + private func handleSlotCountChange(oldValue: Int) { + let clamped = max(1, min(slotCount, 64)) + if slotCount != clamped { + slotCount = clamped + return + } + + guard !isApplyingContext else { return } + guard oldValue != slotCount else { return } + + if slotCount < oldValue { + nodes.removeAll { $0.slot >= slotCount } + if let selectedNodeID, !nodes.contains(where: { $0.id == selectedNodeID }) { + self.selectedNodeID = nodes.first?.id + self.selectedModeIndex = nodes.first?.mode + } + } + + markDirty("Slot count changed to \(slotCount)") + validateAction() + } + + private func availableProjects(for workspace: String) -> [String] { + let scopedProjects = Self.workspaceProjectContexts + .filter { $0.workspace == workspace } + .map(\.project) + if !scopedProjects.isEmpty { + return Self.uniqueOrdered(scopedProjects) + } + return Self.uniqueOrdered(Self.workspaceProjectContexts.map(\.project)) + } + + private func selectedContext() -> WorkspaceProjectContext { + if let exact = Self.workspaceProjectContexts.first(where: { $0.workspace == workspace && $0.project == project }) { + return exact + } + if let byWorkspace = Self.workspaceProjectContexts.first(where: { $0.workspace == workspace }) { + return byWorkspace + } + return Self.workspaceProjectContexts.first! + } + + private func preferredFoundryProfile( + for context: WorkspaceProjectContext, + environment: EnvironmentBadge + ) -> String { + switch environment { + case .dev: + return context.devFoundryProfile + case .stage: + return context.stageFoundryProfile + case .prod: + return context.prodFoundryProfile + } + } + + private func resolvedFoundryProfile( + preferred: String, + environment: EnvironmentBadge + ) -> String { + if foundryProfiles.contains(preferred) { + return preferred + } + + if environment == .prod, + !approvedProdFoundryProfiles.isEmpty, + let approved = foundryProfiles.first(where: { approvedProdFoundryProfiles.contains($0) }) { + return approved + } + + if let first = foundryProfiles.first { + return first + } + + return preferred + } + + private func applySelectionContext( + triggerReason: String, + markDirty shouldMarkDirty: Bool, + appendContextLog: Bool, + revalidate: Bool = true + ) { + isApplyingContext = true + defer { isApplyingContext = false } + + let available = availableProjects(for: workspace) + if let firstProject = available.first, !available.contains(project) { + project = firstProject + } + + let context = selectedContext() + if workspace != context.workspace { + workspace = context.workspace + } + if project != context.project { + project = context.project + } + + let previousBackend = backend + let previousSeed = seed + let previousPath = circuitJSONPath + let previousFoundry = foundryProfile + + backend = context.defaultBackend + seed = context.defaultSeed + circuitJSONPath = context.defaultCircuitPath + foundryProfile = resolvedFoundryProfile( + preferred: preferredFoundryProfile(for: context, environment: environment), + environment: environment + ) + let policyAutoSwitched = enforceKPIPolicyForSelectedBackend() + + if appendContextLog { + appendAudit("Context changed: workspace=\(workspace) project=\(project) environment=\(environment.rawValue)") + appendRunOutput( + "Context: workspace=\(workspace) project=\(project) environment=\(environment.rawValue) role=\(userRole.rawValue)" + ) + } + + let contextChanged = previousBackend != backend || + previousSeed != seed || + previousPath != circuitJSONPath || + previousFoundry != foundryProfile || + policyAutoSwitched + + if shouldMarkDirty, contextChanged { + markDirty(triggerReason) + } + + if revalidate { + validateAction() + } + } + + func matchesPaletteFilter(_ kind: GateKind) -> Bool { + paletteFilter == .all || kind.primaryFilter == paletteFilter + } + + func matchesSearch(_ kind: GateKind) -> Bool { + let query = paletteSearch.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + guard !query.isEmpty else { return true } + return kind.rawValue.contains(query) || kind.symbol.lowercased().contains(query) + } + + func disabledReason(for kind: GateKind) -> String? { + if userRole == .viewer { + return "disabled-by-policy: viewer role" + } + + if let runtimeUnsupportedReason = kind.runtimeUnsupportedReason { + return runtimeUnsupportedReason + } + + switch backend { + case .gaussian: + if kind == .injectFock || kind == .injectCat { + return "disabled-by-backend: gaussian path" + } + case .fock: + if !fockRuntimeSupportedKinds.contains(kind) { + return "disabled-by-backend: fock path" + } + case .hybrid: + break + } + + return nil + } + + var runtimeEngineUnavailableReason: String? { + nil + } + + private func resolveRuntimeExecutionEngine(action: String) -> RuntimeEngineSelection { + _ = action + return runtimeEngine + } + + func nodeAt(mode: Int, slot: Int) -> CircuitNode? { + nodes.first { $0.mode == mode && $0.slot == slot } + } + + func selectNode(_ node: CircuitNode?) { + selectedNodeID = node?.id + selectedModeIndex = node?.mode + } + + func selectMode(_ mode: Int?) { + guard let mode else { + selectedModeIndex = nil + return + } + guard mode >= 0, mode < modeCount else { return } + selectedModeIndex = mode + selectedNodeID = nil + } + + func handleDrop(kindRawValue: String, mode: Int, slot: Int) { + guard let kind = GateKind(rawValue: kindRawValue) else { return } + guard disabledReason(for: kind) == nil else { return } + + if let index = nodes.firstIndex(where: { $0.mode == mode && $0.slot == slot }) { + nodes[index].kind = kind + nodes[index].paramA = kind.defaultParamA + nodes[index].paramB = kind.defaultParamB + selectedNodeID = nodes[index].id + } else { + let node = CircuitNode(kind: kind, mode: mode, slot: slot) + nodes.append(node) + selectedNodeID = node.id + } + + markDirty("Updated canvas from drag/drop") + validateAction() + } + + func addModeLane() { + modeCount += 1 + markDirty("Added mode lane") + validateAction() + } + + func addSlotColumn() { + guard slotCount < 64 else { + statusText = "Slot count already at maximum (64)." + return + } + slotCount += 1 + } + + func removeSlotColumn() { + guard slotCount > 1 else { + statusText = "Slot count already at minimum (1)." + return + } + slotCount -= 1 + } + + func deleteModeLane(_ mode: Int) { + guard modeCount > 1 else { return } + guard mode >= 0, mode < modeCount else { return } + + nodes.removeAll { $0.mode == mode } + + for index in nodes.indices { + if nodes[index].mode > mode { + nodes[index].mode -= 1 + } + } + + modeCount -= 1 + + if let selectedNodeID, !nodes.contains(where: { $0.id == selectedNodeID }) { + self.selectedNodeID = nodes.first?.id + } + + if let selectedModeIndex { + if selectedModeIndex == mode { + self.selectedModeIndex = max(0, min(mode, modeCount - 1)) + } else if selectedModeIndex > mode { + self.selectedModeIndex = max(0, min(selectedModeIndex - 1, modeCount - 1)) + } + } + + markDirty("Deleted mode lane \(mode)") + validateAction() + } + + func deleteSelection() { + if let selectedNodeID, let index = nodes.firstIndex(where: { $0.id == selectedNodeID }) { + let removed = nodes.remove(at: index) + self.selectedNodeID = nil + self.selectedModeIndex = removed.mode + markDirty("Deleted \(removed.kind.displayName) at mode \(removed.mode), slot \(removed.slot)") + validateAction() + return + } + + if let selectedModeIndex = validSelectedModeIndex { + deleteModeLane(selectedModeIndex) + return + } + + if selectedModeIndex != nil { + self.selectedModeIndex = nil + } + + statusText = "No selected component or mode to delete." + } + + var selectedNode: CircuitNode? { + guard let selectedNodeID else { return nil } + return nodes.first(where: { $0.id == selectedNodeID }) + } + + func updateSelectedPrimaryParam(_ value: Double) { + guard let selectedNodeID, let idx = nodes.firstIndex(where: { $0.id == selectedNodeID }) else { return } + nodes[idx].paramA = value + markDirty("Updated gate parameter") + validateAction() + } + + func updateSelectedSecondaryParam(_ value: Double) { + guard let selectedNodeID, let idx = nodes.firstIndex(where: { $0.id == selectedNodeID }) else { return } + nodes[idx].paramB = value + markDirty("Updated gate parameter") + validateAction() + } + + func loadSelectedScientificCircuitPreset() { + guard let preset = selectedScientificCircuitPreset else { + statusText = "No scientific preset selected." + appendRunOutput(statusText) + return + } + + isApplyingContext = true + modeCount = max(1, preset.modeCount) + slotCount = max(1, preset.slotCount) + nodes = preset.nodes + selectedNodeID = nodes.first?.id + selectedModeIndex = nodes.first?.mode + backend = preset.recommendedBackend + seed = preset.suggestedSeed + circuitJSONPath = "scientific://\(preset.id)" + isApplyingContext = false + + clearMonteCarloResults() + lastRunResponse = nil + lastTraceResponse = nil + runtimeParitySummary = nil + stateHealthSummary = nil + bosonicSignoffSummary = nil + runtimePhase = .standby + rightInspectorTab = .circuitConfigInspector + bottomTab = .validationConsole + + markDirty("Loaded scientific preset \(preset.title)") + statusText = "Scientific preset loaded: \(preset.title)" + appendRunOutput("Scientific preset loaded: \(preset.title)") + appendRunOutput("Citation: \(preset.citation)") + appendRunOutput("DOI: \(preset.doiURL)") + appendAudit("Loaded scientific preset \(preset.id)") + validateAction() + } + + func analyzeAction() { + if (lastTraceResponse?.frames ?? []).isEmpty && lastRunResponse == nil { + statusText = "No run data yet. Run the circuit to populate analysis graphs." + appendRunOutput(statusText) + } else if (lastTraceResponse?.frames ?? []).isEmpty { + statusText = "Run data available; trace frames unavailable." + } else { + statusText = "Analysis ready" + } + isAnalysisPopupPresented = true + appendAudit("Analysis action") + } + + func clearMonteCarloResults() { + monteCarloRunResponses.removeAll() + monteCarloTraceResponses.removeAll() + monteCarloCompletedSamples = 0 + monteCarloLastRunTimestamp = nil + appendAudit("Cleared Monte Carlo analysis") + } + + func importCalibratedNoiseProfile() { + guard let sourceURL = CLIService.promptForNoiseProfileOpenURL( + existingURL: calibratedNoiseProfilePath.map { URL(fileURLWithPath: $0) } + ) else { + statusText = "Calibrated profile import canceled" + appendRunOutput("Calibrated profile import canceled.") + return + } + + do { + let loadedProfile = try CLIService.loadCalibratedNoiseProfile(from: sourceURL) + if let declaredModes = loadedProfile.modeCount, declaredModes < 1 { + throw CLIRunnerError.invalidNoiseProfile("`mode_count` must be >= 1 when provided.") + } + calibratedNoiseProfile = loadedProfile + calibratedNoiseProfilePath = sourceURL.path + useCalibratedNoiseProfile = true + clearMonteCarloResults() + statusText = "Calibrated profile loaded: \(loadedProfile.deviceName)" + appendRunOutput( + "Calibrated profile loaded: \(loadedProfile.deviceName) [\(loadedProfile.deviceID)] from \(sourceURL.lastPathComponent)" + ) + if let warning = calibratedTensorValidationWarning(loadedProfile, modeCount: modeCount) { + appendRunOutput("Calibrated profile warning: \(warning)") + } + appendAudit("Imported calibrated noise profile") + } catch { + statusText = "Calibrated profile import failed: \(error.localizedDescription)" + appendRunOutput(statusText) + appendAudit("Calibrated profile import failed") + bottomTab = .runOutputPanel + } + } + + func clearCalibratedNoiseProfile() { + calibratedNoiseProfile = nil + calibratedNoiseProfilePath = nil + useCalibratedNoiseProfile = false + clearMonteCarloResults() + statusText = "Calibrated profile cleared" + appendRunOutput("Calibrated profile cleared.") + appendAudit("Cleared calibrated noise profile") + } + + func runMonteCarloAnalysis() async { + guard !isMonteCarloRunning else { return } + + validateAction() + if validationIssues.contains(where: { $0.severity == .error }) { + statusText = "Monte Carlo blocked: fix validation errors first." + appendRunOutput(statusText) + bottomTab = .validationConsole + return + } + + let runtimeEngineForExecution = resolveRuntimeExecutionEngine(action: "monte-carlo") + + let requestedSamples = noiseModel.sampleCount + let sampleCount = max(4, min(requestedSamples, 128)) + let profileRef = currentFoundryProfileRef() + let baseNodes = nodes + let backendValue = backend + let roleValue = userRole + let environmentValue = environment + let modeCountValue = modeCount + let seedValue = seed + let cutoffValue = cutoff + let schemaVersionValue = schemaVersion + let foundryProfileValue = foundryProfile + let profileID = profileRef?.profileID + let profileVersion = profileRef?.version + let activeCalibration = useCalibratedNoiseProfile ? calibratedNoiseProfile : nil + + isMonteCarloRunning = true + runtimePhase = .running + monteCarloCompletedSamples = 0 + monteCarloRunResponses.removeAll(keepingCapacity: true) + monteCarloTraceResponses.removeAll(keepingCapacity: true) + appendAudit("Monte Carlo analysis started (\(sampleCount) samples)") + appendRunOutput("Monte Carlo: started \(sampleCount) samples.") + appendRunOutput("Monte Carlo runtime engine: \(runtimeEngineForExecution.rawValue)") + if let activeCalibration { + appendRunOutput( + "Monte Carlo calibration: device=\(activeCalibration.deviceName) id=\(activeCalibration.deviceID)" + ) + } + + var rng: any RandomNumberGenerator = SeededGenerator(seed: resolvedBaseSeed(seedValue)) + + do { + for sampleIndex in 0.. [ValidationIssue] { + let prefix = issueLocationPrefix(for: node) + return validationIssues.filter { $0.location.hasPrefix(prefix) } + } + + func validationSeverity(for node: CircuitNode) -> ValidationSeverity? { + let scoped = issues(for: node) + if scoped.contains(where: { $0.severity == .error }) { + return .error + } + if scoped.contains(where: { $0.severity == .warning }) { + return .warning + } + if scoped.contains(where: { $0.severity == .info }) { + return .info + } + return nil + } + + func validateAction() { + var issues: [ValidationIssue] = [] + + for node in nodes { + let location = issueLocation(for: node) + + if let reason = disabledReason(for: node.kind) { + issues.append( + ValidationIssue( + severity: .error, + code: "POLICY_DISABLED_GATE", + message: reason, + location: location + ) + ) + } + + if (node.kind == .squeeze || node.kind == .sourceSqueezed) && node.paramA > 1.5 { + issues.append( + ValidationIssue( + severity: .warning, + code: "GATE_SQUEEZE_RANGE", + message: "Squeeze factor r should be <= 1.50", + location: location + ) + ) + } + + if let irIssue = irValidationIssue(for: node) { + issues.append( + irIssue + ) + } + } + + if backend == .fock, modeCount > 1 { + issues.append( + ValidationIssue( + severity: .error, + code: "BACKEND_MODE_LIMIT", + message: "Fock backend currently supports only single-mode circuits.", + location: "Circuit" + ) + ) + } + + let trimmedSeed = seed.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedSeed.isEmpty, UInt64(trimmedSeed) == nil { + issues.append( + ValidationIssue( + severity: .error, + code: "SEED_INVALID", + message: "Seed must be an unsigned 64-bit integer (0...\(UInt64.max)).", + location: "Circuit Config Inspector" + ) + ) + } + + if !projects.contains(project) { + issues.append( + ValidationIssue( + severity: .error, + code: "CONTEXT_PROJECT_INVALID", + message: "Project '\(project)' does not belong to workspace '\(workspace)'.", + location: "GlobalTopBar" + ) + ) + } + + if environment == .prod { + if selectedKPIPolicy == nil { + issues.append( + ValidationIssue( + severity: .error, + code: "PROD_KPI_POLICY_REQUIRED", + message: "Prod environment requires an explicit KPI policy selection.", + location: "KPI Policy" + ) + ) + } + + if userRole != .approver && userRole != .admin { + issues.append( + ValidationIssue( + severity: .error, + code: "PROD_ROLE_REQUIRED", + message: "Prod environment requires role approver or admin.", + location: "GlobalTopBar" + ) + ) + } + + if approvedProdFoundryProfiles.isEmpty { + issues.append( + ValidationIssue( + severity: .warning, + code: "PROD_PROFILE_APPROVAL_UNAVAILABLE", + message: "Foundry registry approvals are unavailable; prod profile verification is degraded.", + location: "Foundry Profile" + ) + ) + } else if !approvedProdFoundryProfiles.contains(foundryProfile) { + issues.append( + ValidationIssue( + severity: .error, + code: "PROD_PROFILE_NOT_APPROVED", + message: "Foundry profile '\(foundryProfile)' is not approved for prod.", + location: "Foundry Profile" + ) + ) + } + + if currentFoundryProfileRef() == nil { + issues.append( + ValidationIssue( + severity: .error, + code: "PROD_PROFILE_REF_MISSING", + message: "Foundry profile '\(foundryProfile)' is missing registry profile_id/version mapping.", + location: "Foundry Profile" + ) + ) + } + } + + if environment == .stage, foundryProfile == "experimental-lab-v1" { + issues.append( + ValidationIssue( + severity: .warning, + code: "STAGE_PROFILE_EXPERIMENTAL", + message: "Using experimental profile in stage environment.", + location: "Foundry Profile" + ) + ) + } + + do { + try CLIService.validateRuntimeCircuit( + nodes: nodes, + modeCount: modeCount, + backend: backend + ) + } catch { + issues.append( + ValidationIssue( + severity: .error, + code: "RUNTIME_PAYLOAD_INVALID", + message: error.localizedDescription, + location: "Runtime payload" + ) + ) + } + + if environment == .prod, requiresBosonicSignoff, lastRunResponse != nil { + if let summary = bosonicSignoffSummary, summary.status != .pass { + let failedTitles = summary.checks + .filter { !$0.passed } + .map(\.title) + .joined(separator: ", ") + let failedText = failedTitles.isEmpty ? "criteria pending" : failedTitles + issues.append( + ValidationIssue( + severity: .warning, + code: "BOSONIC_SIGNOFF_INCOMPLETE", + message: "Bosonic signoff is incomplete (\(summary.passedChecks)/\(summary.requiredChecks)): \(failedText).", + location: "Provenance Inspector" + ) + ) + } else if bosonicSignoffSummary == nil { + issues.append( + ValidationIssue( + severity: .warning, + code: "BOSONIC_SIGNOFF_PENDING", + message: "Bosonic signoff is pending. Run the circuit to generate objective signoff checks.", + location: "Provenance Inspector" + ) + ) + } + } + + if issues.isEmpty { + issues.append( + ValidationIssue( + severity: .info, + code: "VALIDATION_OK", + message: "No validation issues detected.", + location: "Circuit" + ) + ) + } + + validationIssues = issues + statusText = "Validation complete" + appendAudit("Validate action") + appendRunOutput("Validation completed (\(issues.count) issue(s)).") + bottomTab = .validationConsole + } + + func compileAction() { + runtimePhase = .compiling + do { + let runtimeEngineForExecution = resolveRuntimeExecutionEngine(action: "compile") + let inputPath = try resolveRuntimeInputPath() + let profileRefText: String + if let profileRef = currentFoundryProfileRef() { + profileRefText = "\(profileRef.profileID):v\(profileRef.version)" + } else { + profileRefText = "n/a" + } + appendRunOutput( + "Context: workspace=\(workspace) project=\(project) environment=\(environment.rawValue) role=\(userRole.rawValue)" + ) + appendRunOutput("Compile runtime engine: \(runtimeEngineForExecution.rawValue)") + appendRunOutput( + "Compile: backend=\(backend.rawValue) gates=\(nodes.count) schema=\(schemaVersion) foundry_profile=\(foundryProfile) profile_ref=\(profileRefText)" + ) + appendRunOutput("Compile input ready: \(inputPath)") + statusText = "Compile completed" + runtimePhase = .finished + appendAudit("Compile action") + bottomTab = .runOutputPanel + } catch { + statusText = "Compile failed: \(error.localizedDescription)" + appendRunOutput(statusText) + runtimePhase = .standby + bottomTab = .runOutputPanel + } + } + + func runAction() async { + validateAction() + if validationIssues.contains(where: { $0.severity == .error }) { + statusText = "Run blocked: fix validation errors first." + appendRunOutput(statusText) + presentRunBlockedAlert() + runtimePhase = .standby + bottomTab = .validationConsole + return + } + + isRunning = true + runtimePhase = .running + statusText = "Running..." + runtimeParitySummary = nil + stateHealthSummary = nil + bosonicSignoffSummary = nil + appendAudit("Run action") + + do { + let runtimeEngineForExecution = resolveRuntimeExecutionEngine(action: "run") + if environment == .prod, selectedKPIPolicy == nil { + throw CLIRunnerError.commandFailed("Prod environment requires an explicit KPI policy selection before run.") + } + let profileRef = currentFoundryProfileRef() + let nodesValue = nodes + let backendValue = backend + let roleValue = userRole + let environmentValue = environment + let modeCountValue = modeCount + let seedValue = seed + let cutoffValue = cutoff + let schemaVersionValue = schemaVersion + let foundryProfileValue = foundryProfile + let noiseModelValue = noiseModel + let activeCalibration = useCalibratedNoiseProfile ? calibratedNoiseProfile : nil + let jsonPath = try resolveRuntimeInputPath() + appendRunOutput( + "Runtime context: workspace=\(workspace) project=\(project) environment=\(environment.rawValue) role=\(userRole.rawValue)" + ) + appendRunOutput("Run runtime engine: \(runtimeEngineForExecution.rawValue)") + appendRunOutput("Runtime input: \(jsonPath)") + + let alternateRuntime = pairedRuntimeEngine(for: runtimeEngineForExecution) + let runResponse = try await Task.detached(priority: .userInitiated) { + try CLIService.runCircuit( + jsonPath: jsonPath, + backend: backendValue, + environment: environmentValue, + runtimeEngine: runtimeEngineForExecution + ) + }.value + + let traceResponse = try await Task.detached(priority: .userInitiated) { + try CLIService.loadTrace( + jsonPath: jsonPath, + backend: backendValue, + role: roleValue, + environment: environmentValue, + runtimeEngine: runtimeEngineForExecution + ) + }.value + + let alternateRunResult = await Task.detached(priority: .utility) { () -> Result in + do { + let response = try CLIService.runCircuit( + jsonPath: jsonPath, + backend: backendValue, + environment: environmentValue, + runtimeEngine: alternateRuntime + ) + return .success(response) + } catch { + return .failure(error) + } + }.value + + lastRunResponse = runResponse + lastTraceResponse = traceResponse + statusText = "Run completed" + runtimePhase = .finished + + let photonText: String + if let value = runResponse.meanPhotonNumber { + photonText = String(format: "%.4f", value) + } else { + photonText = "n/a" + } + + appendRunOutput("Run: status=success backend=\(runResponse.backend ?? "n/a") mean_photon_number=\(photonText)") + appendRunOutput("Trace: backend=\(traceResponse.backend ?? "n/a") frame_count=\(traceResponse.traceFrameCount ?? 0)") + if let representation = runResponse.finalState?.representation { + appendRunOutput("State: representation=\(representation)") + } + + switch alternateRunResult { + case .success(let alternateRunResponse): + let summary = buildRuntimeParitySummary( + selectedRuntime: runtimeEngineForExecution, + selectedResponse: runResponse, + referenceRuntime: alternateRuntime, + referenceResponse: alternateRunResponse + ) + runtimeParitySummary = summary + appendRunOutput( + "Runtime parity: \(summary.status.title) selected=\(summary.selectedRuntime.rawValue) reference=\(summary.referenceRuntime.rawValue) \(summary.detail)" + ) + case .failure(let error): + runtimeParitySummary = RuntimeParitySummary( + status: .unmatched, + selectedRuntime: runtimeEngineForExecution, + referenceRuntime: alternateRuntime, + selectedBackendVersion: runResponse.provenance?.backendVersion ?? "n/a", + referenceBackendVersion: "unavailable", + meanPhotonDelta: nil, + stateDeltaMax: nil, + measurementDelta: nil, + detail: "Reference runtime unavailable: \(error.localizedDescription)" + ) + appendRunOutput("Runtime parity: UNMATCHED reference runtime unavailable (\(alternateRuntime.rawValue)).") + } + let robustnessContext = RobustnessContext( + nodes: nodesValue, + modeCount: modeCountValue, + backend: backendValue, + seed: seedValue, + cutoff: cutoffValue, + schemaVersion: schemaVersionValue, + foundryProfile: foundryProfileValue, + environment: environmentValue, + foundryProfileID: profileRef?.profileID, + foundryProfileVersion: profileRef?.version, + runtimeEngine: runtimeEngineForExecution, + noiseModel: noiseModelValue, + calibratedProfile: activeCalibration + ) + let kpiPolicy = selectedKPIPolicy + + let healthSummary = await buildStateHealthSummary( + runResponse: runResponse, + paritySummary: runtimeParitySummary, + robustnessContext: robustnessContext, + kpiPolicy: kpiPolicy + ) + stateHealthSummary = healthSummary + let signoffSummary = buildBosonicSignoffSummary( + healthSummary: healthSummary, + paritySummary: runtimeParitySummary + ) + bosonicSignoffSummary = signoffSummary + appendRunOutput( + "State health: \(healthSummary.status.title) validity=\(healthSummary.validity.title) confidence=\(healthSummary.confidence.title) robustness=\(healthSummary.robustness.status.title) kpi=\(healthSummary.kpi.status.title) checks=\(healthSummary.checksCompleted) issues=\(healthSummary.issueCount)" + ) + if let signoffSummary { + appendRunOutput( + "Bosonic signoff: \(signoffSummary.status.title) checks=\(signoffSummary.passedChecks)/\(signoffSummary.requiredChecks) \(signoffSummary.detail)" + ) + } + bottomTab = .runOutputPanel + } catch { + statusText = "Run failed: \(error.localizedDescription)" + appendRunOutput(statusText) + runtimePhase = .standby + bottomTab = .runOutputPanel + } + + isRunning = false + } + + func exportAction() { + do { + let url = try CLIService.exportCircuitSnapshot( + nodes: nodes, + metadata: snapshotMetadata() + ) + lastExportPath = url.path + appendRunOutput("Export: \(url.path)") + markClean("Export completed") + appendAudit("Export action") + bottomTab = .runOutputPanel + } catch { + statusText = "Export failed: \(error.localizedDescription)" + appendRunOutput(statusText) + } + } + + func saveAction() { + if let lastSavedDocumentURL { + persistCircuitDocument(to: lastSavedDocumentURL, format: nil, actionName: "Save") + return + } + saveAsAction() + } + + func newAction() { + isApplyingContext = true + modeCount = 4 + slotCount = 6 + nodes = [] + selectedNodeID = nil + selectedModeIndex = nil + isApplyingContext = false + + lastSavedDocumentURL = nil + lastSavedDocumentPath = nil + clearMonteCarloResults() + runtimeParitySummary = nil + stateHealthSummary = nil + bosonicSignoffSummary = nil + appendRunOutput("New circuit initialized.") + appendAudit("New action") + markClean("New circuit ready") + runtimePhase = .standby + validateAction() + bottomTab = .validationConsole + } + + func openAction() { + guard let sourceURL = CLIService.promptForCircuitOpenURL(existingURL: lastSavedDocumentURL) else { + statusText = "Open canceled" + appendRunOutput("Open canceled.") + runtimePhase = .standby + return + } + + do { + let document = try CLIService.loadCircuitDocument(from: sourceURL) + applyLoadedCircuit(document) + clearMonteCarloResults() + runtimeParitySummary = nil + stateHealthSummary = nil + bosonicSignoffSummary = nil + validateAction() + lastSavedDocumentURL = sourceURL + lastSavedDocumentPath = sourceURL.path + appendRunOutput("Open: \(sourceURL.path)") + appendAudit("Open action") + markClean("Opened circuit") + runtimePhase = .standby + bottomTab = .validationConsole + } catch { + statusText = "Open failed: \(error.localizedDescription)" + appendRunOutput(statusText) + runtimePhase = .standby + bottomTab = .runOutputPanel + } + } + + func saveAsAction() { + let suggestedBaseName = suggestedCircuitDocumentBaseName() + guard let destination = CLIService.promptForCircuitSaveDestination( + suggestedBaseName: suggestedBaseName, + existingURL: lastSavedDocumentURL + ) else { + statusText = "Save canceled" + appendRunOutput("Save canceled.") + return + } + persistCircuitDocument(to: destination.url, format: destination.format, actionName: "Save As") + } + + private func persistCircuitDocument(to url: URL, format: CLIService.CircuitSaveFormat?, actionName: String) { + do { + let savedURL = try CLIService.saveCircuitDocument( + nodes: nodes, + modeCount: modeCount, + slotCount: slotCount, + metadata: snapshotMetadata(), + to: url, + format: format + ) + lastSavedDocumentURL = savedURL + lastSavedDocumentPath = savedURL.path + appendRunOutput("\(actionName): \(savedURL.path)") + markClean("\(actionName) completed") + appendAudit("\(actionName) action") + bottomTab = .runOutputPanel + } catch { + statusText = "\(actionName) failed: \(error.localizedDescription)" + appendRunOutput(statusText) + bottomTab = .runOutputPanel + } + } + + private func applyLoadedCircuit(_ document: CLIService.CircuitSnapshotDocument) { + isApplyingContext = true + defer { isApplyingContext = false } + + if let workspace = document.workspace, workspaces.contains(workspace) { + self.workspace = workspace + } + + if let project = document.project { + let available = availableProjects(for: self.workspace) + if available.contains(project) { + self.project = project + } + } + + if let environment = document.environment { + self.environment = environment + } + + if let userRole = document.userRole { + self.userRole = userRole + } + + if let backend = document.backend { + self.backend = backend + } + + if let runtimeEngine = document.runtimeEngine { + self.runtimeEngine = runtimeEngine + } + + if let seed = document.seed, !seed.isEmpty { + self.seed = seed + } + + if let cutoff = document.cutoff { + self.cutoff = max(1, cutoff) + } + + if !document.schemaVersion.isEmpty { + self.schemaVersion = document.schemaVersion + } + + if let foundryProfile = document.foundryProfile, !foundryProfile.isEmpty { + if !foundryProfiles.contains(foundryProfile) { + foundryProfiles.append(foundryProfile) + } + self.foundryProfile = foundryProfile + } + + if let policyID = document.kpiPolicyID, kpiPolicies.contains(where: { $0.id == policyID }) { + selectedKPIPolicyID = policyID + } + + let nodes = document.nodes + let derivedModeCount = max(1, (nodes.map(\.mode).max() ?? -1) + 1) + let derivedSlotCount = max(1, (nodes.map(\.slot).max() ?? -1) + 1) + + self.modeCount = max(document.modeCount, derivedModeCount) + self.slotCount = max(document.slotCount, derivedSlotCount) + self.nodes = nodes + self.selectedNodeID = nodes.first?.id + self.selectedModeIndex = nodes.first?.mode + } + + private func resolveRuntimeInputPath() throws -> String { + if !nodes.isEmpty { + let profileRef = currentFoundryProfileRef() + let runtimeURL = try CLIService.materializeRuntimeCircuitInput( + nodes: nodes, + modeCount: modeCount, + backend: backend, + seed: seed, + cutoff: cutoff, + schemaVersion: schemaVersion, + foundryProfile: foundryProfile, + environment: environment, + foundryProfileID: profileRef?.profileID, + foundryProfileVersion: profileRef?.version + ) + return runtimeURL.path + } + + let trimmed = circuitJSONPath.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + throw CLIRunnerError.invalidRuntimeCircuit( + "Canvas is empty and no JSON path is set. Provide Circuit JSON Path or add gates to the canvas." + ) + } + return trimmed + } + + func clearRunOutput() { + runOutput.removeAll() + appendAudit("Cleared run output") + } + + private func snapshotMetadata() -> [String: String] { + let profileRef = currentFoundryProfileRef() + let kpiPolicy = selectedKPIPolicy + return [ + "schema_version": schemaVersion, + "runtime_engine": runtimeEngine.rawValue, + "backend": backend.rawValue, + "seed": seed, + "cutoff": String(cutoff), + "foundry_profile": foundryProfile, + "workspace": workspace, + "project": project, + "environment": environment.rawValue, + "role": userRole.rawValue, + "foundry_profile_id": profileRef?.profileID ?? "", + "foundry_profile_version": profileRef.map { String($0.version) } ?? "", + "kpi_policy_id": kpiPolicy?.id ?? "", + "kpi_policy_version": kpiPolicy.map { String($0.version) } ?? "", + ] + } + + private func suggestedCircuitDocumentBaseName() -> String { + let workspaceToken = workspace.lowercased().replacingOccurrences(of: " ", with: "_") + let projectToken = project.lowercased().replacingOccurrences(of: " ", with: "_") + return "\(workspaceToken)_\(projectToken)_circuit" + } + + private func currentFoundryProfileRef() -> FoundryProfileRef? { + foundryProfileRefsByName[foundryProfile] + } + + private static func loadFoundryRegistryCatalog() -> FoundryRegistryCatalog { + let fallbackCatalog = FoundryRegistryCatalog( + allProfileNames: fallbackFoundryProfiles, + approvedProfileNames: ["studio-default-v1"], + profileRefsByName: [:] + ) + + let registryURL = repoRootURL().appendingPathComponent("docs/config/foundry_registry.json") + guard let data = try? Data(contentsOf: registryURL) else { + return fallbackCatalog + } + + guard let document = try? JSONDecoder().decode(FoundryRegistryDocument.self, from: data) else { + return fallbackCatalog + } + + var profileNames: [String] = [] + var approved = Set() + var refs: [String: FoundryProfileRef] = [:] + + for profile in document.profiles { + let name = profile.spec.name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !name.isEmpty else { continue } + + profileNames.append(name) + refs[name] = FoundryProfileRef(profileID: profile.profileID, version: profile.version) + + if profile.status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "approved" { + approved.insert(name) + } + } + + let names = uniqueOrdered(profileNames + fallbackFoundryProfiles) + if approved.isEmpty { + approved.insert("studio-default-v1") + } + + return FoundryRegistryCatalog( + allProfileNames: names, + approvedProfileNames: approved, + profileRefsByName: refs + ) + } + + private static func loadKPIPolicyCatalog() -> [KPIPolicyDefinition] { + let fallback = fallbackKPIPolicies + let policyURL = repoRootURL().appendingPathComponent("docs/config/kpi_policies.json") + guard let data = try? Data(contentsOf: policyURL) else { + return fallback + } + + guard let document = try? JSONDecoder().decode(KPIPolicyCatalogDocument.self, from: data) else { + return fallback + } + + let normalized = document.policies.filter { !$0.id.isEmpty && !$0.name.isEmpty } + return normalized.isEmpty ? fallback : normalized + } + + private static func uniqueOrdered(_ values: [String]) -> [String] { + var seen = Set() + var ordered: [String] = [] + ordered.reserveCapacity(values.count) + for value in values where seen.insert(value).inserted { + ordered.append(value) + } + return ordered + } + + private static func repoRootURL() -> URL { + let fileURL = URL(fileURLWithPath: #filePath) + return fileURL + .deletingLastPathComponent() // schrosim-studio + .deletingLastPathComponent() // Sources + .deletingLastPathComponent() // core-swift + .deletingLastPathComponent() // repo root + } + + private func markDirty(_ reason: String) { + // Invalidate run-derived diagnostics whenever the working configuration mutates. + runtimeParitySummary = nil + stateHealthSummary = nil + bosonicSignoffSummary = nil + isDirty = true + statusText = "dirty: \(reason)" + } + + private func markClean(_ reason: String) { + isDirty = false + statusText = reason + } + + private func appendRunOutput(_ message: String) { + runOutput.append(message) + if runOutput.count > 300 { + runOutput.removeFirst(runOutput.count - 300) + } + } + + private func appendAudit(_ message: String) { + auditTrail.append(message) + if auditTrail.count > 300 { + auditTrail.removeFirst(auditTrail.count - 300) + } + } + + private func issueLocationPrefix(for node: CircuitNode) -> String { + "Mode \(node.mode), Slot \(node.slot)" + } + + private func issueLocation(for node: CircuitNode) -> String { + "\(issueLocationPrefix(for: node)) > \(node.kind.rawValue)" + } + + private func enforceBackendForSelectedKPIPolicy() -> Bool { + guard let policy = selectedKPIPolicy else { return false } + guard let enforcedBackend = backendRequired(for: policy, currentBackend: backend) else { return false } + + showValidation = true + bottomTab = .validationConsole + + guard backend != enforcedBackend else { return false } + + backend = enforcedBackend + statusText = "Backend auto-switched to \(enforcedBackend.title) for KPI policy '\(policy.name)'." + appendRunOutput(statusText) + appendAudit("Backend auto-switched to \(enforcedBackend.rawValue) for policy \(policy.id)") + return true + } + + private func enforceKPIPolicyForSelectedBackend() -> Bool { + guard let policy = preferredKPIPolicy(for: backend) else { return false } + guard selectedKPIPolicyID != policy.id else { return false } + + showValidation = true + bottomTab = .validationConsole + + selectedKPIPolicyID = policy.id + statusText = "KPI policy auto-switched to '\(policy.name)' for \(backend.title) backend." + appendRunOutput(statusText) + appendAudit("KPI policy auto-switched to \(policy.id) for backend \(backend.rawValue)") + return true + } + + private func backendRequired( + for policy: KPIPolicyDefinition, + currentBackend: BackendSelection + ) -> BackendSelection? { + switch policy.targetRepresentation { + case "fock_number_basis": + return .fock + case "gaussian_phase_space": + return currentBackend == .fock ? .hybrid : nil + default: + return nil + } + } + + private func preferredKPIPolicy(for backend: BackendSelection) -> KPIPolicyDefinition? { + switch backend { + case .fock: + return findPreferredKPIPolicy( + preferredID: Self.cvFockValidationPolicyID, + targetRepresentation: "fock_number_basis" + ) + case .gaussian, .hybrid: + if isQECPrototypeProject { + return findPreferredKPIPolicy( + preferredID: Self.cvQECGaussianPolicyID, + targetRepresentation: "gaussian_phase_space" + ) + } + return findPreferredKPIPolicy( + preferredID: Self.cvGaussianProductionPolicyID, + targetRepresentation: "gaussian_phase_space" + ) + } + } + + private var isQECPrototypeProject: Bool { + project.trimmingCharacters(in: .whitespacesAndNewlines).localizedCaseInsensitiveCompare("Quantum Error Correction") == .orderedSame + } + + private func findPreferredKPIPolicy( + preferredID: String, + targetRepresentation: String + ) -> KPIPolicyDefinition? { + if let exactMatch = kpiPolicies.first(where: { $0.id == preferredID }) { + return exactMatch + } + return kpiPolicies.first { $0.targetRepresentation == targetRepresentation } + } + + private func presentRunBlockedAlert() { + let errorIssues = validationIssues.filter { $0.severity == .error } + if errorIssues.isEmpty { + runBlockedAlertMessage = "Validation failed. Check your gates." + } else { + let preview = errorIssues.prefix(4).map { "[\($0.code)] \($0.message)" }.joined(separator: "\n") + runBlockedAlertMessage = "Error, check your gates.\n\n\(preview)" + } + isRunBlockedAlertPresented = true + } + + private func irValidationIssue(for node: CircuitNode) -> ValidationIssue? { + guard node.kind == .loss || node.kind == .thermalLoss else { + return nil + } + + do { + let circuit = try Circuit(modes: max(1, modeCount)) + switch node.kind { + case .loss: + try circuit.append(.loss(eta: node.paramA, mode: node.mode)) + case .thermalLoss: + try circuit.append(.thermalLoss(eta: node.paramA, nTh: node.paramB, mode: node.mode)) + default: + return nil + } + return nil + } catch let error as IRValidationError { + return ValidationIssue( + severity: .error, + code: "IR_VALIDATION", + message: error.description, + location: issueLocation(for: node) + ) + } catch { + return ValidationIssue( + severity: .error, + code: "IR_VALIDATION", + message: String(describing: error), + location: issueLocation(for: node) + ) + } + } + + private func perturbNodesForMonteCarlo( + _ source: [CircuitNode], + noiseModel: MonteCarloNoiseModel, + calibratedProfile: CalibratedNoiseTensorProfile?, + sampleIndex: Int, + modeCount: Int, + rng: inout any RandomNumberGenerator + ) -> [CircuitNode] { + var perturbed = source + let resolvedModeCount = max(modeCount, 1) + let driftModel = calibratedProfile?.driftModel ?? .none + let driftScale = Double(max(sampleIndex, 0)) + + let lossNoise = sampleCorrelatedModeNoise( + modeCount: resolvedModeCount, + fallbackSigma: noiseModel.lossEtaSigma, + covariance: calibratedProfile?.correlations.lossEta, + rng: &rng + ) + let thermalNoise = sampleCorrelatedModeNoise( + modeCount: resolvedModeCount, + fallbackSigma: noiseModel.thermalNoiseSigma, + covariance: calibratedProfile?.correlations.thermal, + rng: &rng + ) + let phaseNoise = sampleCorrelatedModeNoise( + modeCount: resolvedModeCount, + fallbackSigma: noiseModel.phaseSigmaRad, + covariance: calibratedProfile?.correlations.phaseRad, + rng: &rng + ) + let displacementNoiseA = sampleCorrelatedModeNoise( + modeCount: resolvedModeCount, + fallbackSigma: noiseModel.displacementSigma, + covariance: calibratedProfile?.correlations.displacement, + rng: &rng + ) + let displacementNoiseB = sampleCorrelatedModeNoise( + modeCount: resolvedModeCount, + fallbackSigma: noiseModel.displacementSigma, + covariance: calibratedProfile?.correlations.displacement, + rng: &rng + ) + let etaDrift = driftModel.lossEtaPerSample * driftScale + let thermalDrift = driftModel.thermalPerSample * driftScale + let phaseDrift = driftModel.phaseRadPerSample * driftScale + let displacementDrift = driftModel.displacementPerSample * driftScale + let detectorHomodyneSigma = calibratedProfile?.detectorModel.homodyneSigma ?? 0.0 + let detectorHeterodyneSigma = calibratedProfile?.detectorModel.heterodyneSigma ?? 0.0 + + for index in perturbed.indices { + let mode = clamp(perturbed[index].mode, to: 0...(resolvedModeCount - 1)) + switch perturbed[index].kind { + case .loss: + perturbed[index].paramA = clamp( + perturbed[index].paramA + lossNoise[mode] + etaDrift, + to: 0.0...1.0 + ) + case .thermalLoss: + perturbed[index].paramA = clamp( + perturbed[index].paramA + lossNoise[mode] + etaDrift, + to: 0.0...1.0 + ) + perturbed[index].paramB = max( + 0.0, + perturbed[index].paramB + thermalNoise[mode] + thermalDrift + ) + case .phase, .beamSplitter: + perturbed[index].paramA += phaseNoise[mode] + phaseDrift + case .displace, .sourceCoherent: + perturbed[index].paramA += displacementNoiseA[mode] + displacementDrift + perturbed[index].paramB += displacementNoiseB[mode] + displacementDrift + case .measureHomodyne: + perturbed[index].paramA += sampleGaussian(sigma: detectorHomodyneSigma, rng: &rng) + case .measureHeterodyne: + // Proxy detector noise by a small phase-equivalent jitter near readout. + perturbed[index].paramA += sampleGaussian(sigma: detectorHeterodyneSigma, rng: &rng) + default: + continue + } + } + + return perturbed + } + + private func sampleCorrelatedModeNoise( + modeCount: Int, + fallbackSigma: Double, + covariance: [[Double]]?, + rng: inout any RandomNumberGenerator + ) -> [Double] { + let resolvedModeCount = max(modeCount, 1) + var result = Array(repeating: 0.0, count: resolvedModeCount) + + guard fallbackSigma > 0 || covariance != nil else { + return result + } + + if fallbackSigma > 0 { + for index in result.indices { + result[index] = sampleGaussian(sigma: fallbackSigma, rng: &rng) + } + } + + guard + let covariance, + let normalized = normalizedCovarianceMatrix(covariance, modeCount: resolvedModeCount), + let factor = choleskyLowerTriangular(normalized) + else { + return result + } + + let n = factor.count + var z = Array(repeating: 0.0, count: n) + for index in 0.. String? { + let effectiveModes = max(modeCount, 1) + var warnings: [String] = [] + let tensors: [(String, [[Double]]?)] = [ + ("loss_eta", profile.correlations.lossEta), + ("thermal", profile.correlations.thermal), + ("phase_rad", profile.correlations.phaseRad), + ("displacement", profile.correlations.displacement), + ] + + for (name, matrix) in tensors { + guard let matrix else { continue } + guard !matrix.isEmpty else { + warnings.append("\(name) tensor is empty") + continue + } + let rows = matrix.count + let cols = matrix.map(\.count).max() ?? 0 + if rows != cols { + warnings.append("\(name) tensor is not square (\(rows)x\(cols))") + continue + } + if rows < effectiveModes { + warnings.append("\(name) tensor coverage \(rows) < current modes \(effectiveModes)") + } + } + + if warnings.isEmpty { + return nil + } + return warnings.joined(separator: "; ") + } + + private func normalizedCovarianceMatrix( + _ covariance: [[Double]], + modeCount: Int + ) -> [[Double]]? { + guard !covariance.isEmpty else { return nil } + let rowCount = covariance.count + guard rowCount > 0 else { return nil } + let matrixSize = min(modeCount, rowCount) + guard matrixSize > 0 else { return nil } + + for rowIndex in 0..= matrixSize else { + return nil + } + } + + var matrix = Array(repeating: Array(repeating: 0.0, count: matrixSize), count: matrixSize) + for i in 0.. [[Double]]? { + let n = matrix.count + guard n > 0 else { return nil } + + var lower = Array(repeating: Array(repeating: 0.0, count: n), count: n) + for i in 0.. 0 { + for k in 0.. String { + let base = UInt64(baseSeed.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0 + let jitter = UInt64.random(in: 0...999_999, using: &rng) + let sampleOffset = UInt64(sampleIndex + 1) + let resolved = base &+ sampleOffset &+ jitter + return String(resolved) + } + + private func resolvedBaseSeed(_ seed: String) -> UInt64 { + let trimmed = seed.trimmingCharacters(in: .whitespacesAndNewlines) + return UInt64(trimmed) ?? 1 + } + + private func sampleGaussian( + sigma: Double, + rng: inout any RandomNumberGenerator + ) -> Double { + guard sigma > 0 else { return 0.0 } + var u1 = Double.random(in: 0..<1, using: &rng) + if u1 < 1e-12 { + u1 = 1e-12 + } + let u2 = Double.random(in: 0..<1, using: &rng) + let z = sqrt(-2.0 * log(u1)) * cos(2.0 * Double.pi * u2) + return z * sigma + } + + private func clamp(_ value: Double, to range: ClosedRange) -> Double { + min(max(value, range.lowerBound), range.upperBound) + } + + private func clamp(_ value: Int, to range: ClosedRange) -> Int { + min(max(value, range.lowerBound), range.upperBound) + } + + private func pairedRuntimeEngine(for engine: RuntimeEngineSelection) -> RuntimeEngineSelection { + switch engine { + case .swift: + return .rust + case .rust: + return .swift + } + } + + private var requiresBosonicSignoff: Bool { + if project.localizedCaseInsensitiveContains("boson") { + return true + } + if circuitJSONPath.localizedCaseInsensitiveContains("boson") { + return true + } + if let preset = selectedScientificCircuitPreset { + if preset.id.localizedCaseInsensitiveContains("boson") { + return true + } + if preset.title.localizedCaseInsensitiveContains("boson") { + return true + } + } + return false + } + + private func buildBosonicSignoffSummary( + healthSummary: StateHealthSummary?, + paritySummary: RuntimeParitySummary? + ) -> BosonicSignoffSummary? { + guard requiresBosonicSignoff else { + return nil + } + + let checks: [BosonicSignoffCheck] = [ + BosonicSignoffCheck( + key: "physical_validity", + title: "Physical validity", + passed: healthSummary?.validity == .valid, + observed: healthSummary?.validity.title ?? "UNAVAILABLE", + expected: StateValidityStatus.valid.title, + detail: "Quantum state invariants and representation checks." + ), + BosonicSignoffCheck( + key: "numerical_confidence", + title: "Numerical confidence", + passed: healthSummary?.confidence == .match, + observed: healthSummary?.confidence.title ?? "UNAVAILABLE", + expected: StateConfidenceStatus.match.title, + detail: "Cross-runtime confidence from parity-equivalent outputs." + ), + BosonicSignoffCheck( + key: "runtime_parity", + title: "Runtime parity", + passed: paritySummary?.status == .match, + observed: paritySummary?.status.title ?? "UNAVAILABLE", + expected: RuntimeParityStatus.match.title, + detail: "Selected and reference runtimes must agree at strict tolerance." + ), + BosonicSignoffCheck( + key: "noise_robustness", + title: "Noise robustness", + passed: healthSummary?.robustness.status == .robust, + observed: healthSummary?.robustness.status.title ?? "UNAVAILABLE", + expected: StateRobustnessStatus.robust.title, + detail: "Calibrated Monte Carlo robustness must satisfy policy thresholds." + ), + BosonicSignoffCheck( + key: "kpi_policy", + title: "KPI policy", + passed: healthSummary?.kpi.status == .pass, + observed: healthSummary?.kpi.status.title ?? "UNAVAILABLE", + expected: KPIPolicyStatus.pass.title, + detail: "Explicit KPI contract gate for production-grade qualification." + ), + ] + + let requiredChecks = checks.count + let passedChecks = checks.filter(\.passed).count + + let status: BosonicSignoffStatus + if healthSummary == nil { + status = .unavailable + } else if passedChecks == requiredChecks { + status = .pass + } else { + status = .incomplete + } + + let failedChecks = checks.filter { !$0.passed } + let detailParts = [ + "checks=\(passedChecks)/\(requiredChecks)", + "failed=\(failedChecks.map(\.key).joined(separator: ","))" + ] + + let recommendation: String + if status == .pass { + recommendation = "Bosonic signoff completed. Circuit meets objective production criteria." + } else if status == .unavailable { + recommendation = "Run required to generate state health and parity evidence for bosonic signoff." + } else { + recommendation = "Resolve failed signoff checks before production qualification." + } + + return BosonicSignoffSummary( + status: status, + requiredChecks: requiredChecks, + passedChecks: passedChecks, + checks: checks, + recommendation: recommendation, + detail: detailParts.joined(separator: " ") + ) + } + + private func buildRuntimeParitySummary( + selectedRuntime: RuntimeEngineSelection, + selectedResponse: RunResponse, + referenceRuntime: RuntimeEngineSelection, + referenceResponse: RunResponse + ) -> RuntimeParitySummary { + let selectedVersion = selectedResponse.provenance?.backendVersion ?? "n/a" + let referenceVersion = referenceResponse.provenance?.backendVersion ?? "n/a" + let meanPhotonDelta = absoluteDelta(selectedResponse.meanPhotonNumber, referenceResponse.meanPhotonNumber) + let measurementDelta = delta(selectedResponse.measurementCount, referenceResponse.measurementCount) + let stateDeltaMax = maxStateDelta(selectedResponse.finalState, referenceResponse.finalState) + + let matchTolerance = 1e-9 + let driftTolerance = 1e-5 + + let status: RuntimeParityStatus + if let measurementDelta, measurementDelta != 0 { + status = .unmatched + } else { + let effectiveDelta = max(meanPhotonDelta ?? 0.0, stateDeltaMax ?? 0.0) + if effectiveDelta <= matchTolerance { + status = .match + } else if effectiveDelta <= driftTolerance { + status = .drift + } else { + status = .unmatched + } + } + + var detailParts: [String] = [] + if let meanPhotonDelta { + detailParts.append(String(format: "d_mean=%.8e", meanPhotonDelta)) + } + if let stateDeltaMax { + detailParts.append(String(format: "d_state=%.8e", stateDeltaMax)) + } + if let measurementDelta { + detailParts.append("d_meas=\(measurementDelta)") + } + if detailParts.isEmpty { + detailParts.append("No comparable metrics available") + } + + return RuntimeParitySummary( + status: status, + selectedRuntime: selectedRuntime, + referenceRuntime: referenceRuntime, + selectedBackendVersion: selectedVersion, + referenceBackendVersion: referenceVersion, + meanPhotonDelta: meanPhotonDelta, + stateDeltaMax: stateDeltaMax, + measurementDelta: measurementDelta, + detail: detailParts.joined(separator: " ") + ) + } + + private func buildStateHealthSummary( + runResponse: RunResponse, + paritySummary: RuntimeParitySummary?, + robustnessContext: RobustnessContext, + kpiPolicy: KPIPolicyDefinition? + ) async -> StateHealthSummary { + var checksCompleted = 0 + var errors: [String] = [] + var warnings: [String] = [] + + checksCompleted += 1 + if runResponse.status.lowercased() != "success" { + errors.append("Runtime returned status=\(runResponse.status)") + } + + checksCompleted += 1 + if let meanPhotonNumber = runResponse.meanPhotonNumber { + if !meanPhotonNumber.isFinite || meanPhotonNumber < -1e-9 { + errors.append("mean_photon_number is invalid") + } + } else { + warnings.append("mean_photon_number unavailable") + } + + checksCompleted += 1 + if let measurementCount = runResponse.measurementCount { + if measurementCount < 0 { + errors.append("measurement_count is negative") + } + } else { + warnings.append("measurement_count unavailable") + } + + if let state = runResponse.finalState { + checksCompleted += 1 + if state.representation?.isEmpty ?? true { + warnings.append("State representation unavailable") + } + + switch state.representation { + case "gaussian_phase_space": + evaluateGaussianStateHealth(state, checksCompleted: &checksCompleted, errors: &errors, warnings: &warnings) + case "fock_number_basis": + evaluateFockStateHealth(state, checksCompleted: &checksCompleted, errors: &errors, warnings: &warnings) + case .some: + warnings.append("Representation unsupported for deep validation") + case .none: + break + } + } else { + warnings.append("Final state snapshot unavailable") + } + + let validity: StateValidityStatus + if !errors.isEmpty { + validity = .invalid + } else if runResponse.finalState == nil { + validity = .unavailable + } else if !warnings.isEmpty { + validity = .partial + } else { + validity = .valid + } + + let confidence: StateConfidenceStatus + if let paritySummary { + switch paritySummary.status { + case .match: + confidence = .match + case .drift: + confidence = .drift + case .unmatched: + confidence = .unmatched + } + } else { + confidence = .unavailable + } + + let robustness = await buildStateRobustnessSummary( + baselineResponse: runResponse, + context: robustnessContext, + policy: kpiPolicy + ) + + let kpi = buildKPIPolicyEvaluationSummary( + runResponse: runResponse, + policy: kpiPolicy + ) + + let status: StateHealthStatus + switch validity { + case .invalid: + status = .invalid + case .unavailable: + status = .unavailable + case .partial, .valid: + if kpi.status == .fail || confidence == .unmatched || robustness.status == .fragile { + status = .review + } else if validity == .valid, + confidence == .match, + robustness.status == .robust, + kpi.status == .pass { + status = .healthy + } else { + status = .review + } + } + + let issueCount = errors.count + warnings.count + var detailParts: [String] = [] + detailParts.append("checks=\(checksCompleted)") + detailParts.append("errors=\(errors.count)") + detailParts.append("warnings=\(warnings.count)") + detailParts.append("robustness=\(robustness.status.title)") + detailParts.append("kpi=\(kpi.status.title)") + if let score = robustness.score { + detailParts.append(String(format: "robust_checks=%.1f%%", score)) + } + let issuePreview = (errors + warnings).prefix(2).joined(separator: "; ") + if !issuePreview.isEmpty { + detailParts.append(issuePreview) + } + + let recommendation = healthRecommendation( + validity: validity, + confidence: confidence, + robustness: robustness.status, + kpiStatus: kpi.status, + errors: errors, + warnings: warnings + ) + + let successRationale: String? + if status == .healthy { + successRationale = "State passed physical checks, runtime parity matched, KPI policy passed, and robustness checks stayed within policy thresholds." + } else if status == .review, validity == .valid, (confidence == .drift || confidence == .match) { + successRationale = "Core state is physically valid, but policy or robustness criteria indicate additional calibration work before production deployment." + } else { + successRationale = nil + } + + return StateHealthSummary( + status: status, + validity: validity, + confidence: confidence, + robustness: robustness, + kpi: kpi, + checksCompleted: checksCompleted, + issueCount: issueCount, + errors: errors, + warnings: warnings, + recommendation: recommendation, + successRationale: successRationale, + detail: detailParts.joined(separator: " ") + ) + } + + private func buildStateRobustnessSummary( + baselineResponse: RunResponse, + context: RobustnessContext, + policy: KPIPolicyDefinition? + ) async -> StateRobustnessSummary { + guard let policy else { + return StateRobustnessSummary( + status: .unavailable, + score: nil, + sampleCount: 0, + successRate: nil, + meanPhotonP95Delta: nil, + stateP95Delta: nil, + measurementMismatchRate: nil, + checks: [], + detail: "Robustness probe unavailable: no KPI policy selected." + ) + } + + let hasConfiguredNoise = context.calibratedProfile != nil + || context.noiseModel.lossEtaSigma > 0 + || context.noiseModel.thermalNoiseSigma > 0 + || context.noiseModel.phaseSigmaRad > 0 + || context.noiseModel.displacementSigma > 0 + + guard hasConfiguredNoise else { + return StateRobustnessSummary( + status: .unavailable, + score: nil, + sampleCount: 0, + successRate: nil, + meanPhotonP95Delta: nil, + stateP95Delta: nil, + measurementMismatchRate: nil, + checks: [], + detail: "Robustness probe unavailable: configure calibrated/noise model first." + ) + } + + guard !context.nodes.isEmpty else { + return StateRobustnessSummary( + status: .unavailable, + score: nil, + sampleCount: 0, + successRate: nil, + meanPhotonP95Delta: nil, + stateP95Delta: nil, + measurementMismatchRate: nil, + checks: [], + detail: "Robustness probe unavailable: canvas nodes required for perturbation." + ) + } + + let sampleCount = max(6, min(context.noiseModel.sampleCount, 24)) + let baseSeed = resolvedBaseSeed(context.seed) + var rng: any RandomNumberGenerator = SeededGenerator(seed: baseSeed) + var successfulRuns = 0 + var failedRuns = 0 + var meanDeltas: [Double] = [] + var stateDeltas: [Double] = [] + var measurementMismatchCount = 0 + + meanDeltas.reserveCapacity(sampleCount) + stateDeltas.reserveCapacity(sampleCount) + + for sampleIndex in 0.. Result in + do { + let runtimeURL = try CLIService.materializeRuntimeCircuitInput( + nodes: perturbedNodes, + modeCount: context.modeCount, + backend: context.backend, + seed: sampleSeed, + cutoff: context.cutoff, + schemaVersion: context.schemaVersion, + foundryProfile: context.foundryProfile, + environment: context.environment, + foundryProfileID: context.foundryProfileID, + foundryProfileVersion: context.foundryProfileVersion + ) + let response = try CLIService.runCircuit( + jsonPath: runtimeURL.path, + backend: context.backend, + environment: context.environment, + runtimeEngine: context.runtimeEngine + ) + return .success(response) + } catch { + return .failure(error) + } + }.value + + switch sampleResult { + case .success(let response): + guard response.status.lowercased() == "success" else { + failedRuns += 1 + continue + } + successfulRuns += 1 + + if let delta = absoluteDelta(response.meanPhotonNumber, baselineResponse.meanPhotonNumber), + delta.isFinite { + meanDeltas.append(delta) + } + if let delta = maxStateDelta(response.finalState, baselineResponse.finalState), + delta.isFinite { + stateDeltas.append(delta) + } + if let lhs = response.measurementCount, let rhs = baselineResponse.measurementCount, lhs != rhs { + measurementMismatchCount += 1 + } + case .failure: + failedRuns += 1 + } + } + + let successRate = sampleCount > 0 ? Double(successfulRuns) / Double(sampleCount) : nil + let mismatchRate = successfulRuns > 0 ? Double(measurementMismatchCount) / Double(successfulRuns) : nil + let meanP95 = percentile(meanDeltas, fraction: 0.95) + let stateP95 = percentile(stateDeltas, fraction: 0.95) + + var checks: [ThresholdCheckResult] = [] + checks.append( + lowerBoundCheck( + key: "robust_success_rate", + title: "Monte Carlo success rate", + value: successRate, + minAllowed: policy.thresholds.minSuccessRate, + noteIfMissing: "No successful robustness samples were produced." + ) + ) + checks.append( + upperBoundCheck( + key: "robust_mean_p95", + title: "P95 mean photon delta", + value: meanP95, + maxAllowed: policy.thresholds.maxMeanPhotonP95Delta, + noteIfMissing: "mean_photon_number deltas unavailable." + ) + ) + checks.append( + upperBoundCheck( + key: "robust_state_p95", + title: "P95 state delta", + value: stateP95, + maxAllowed: policy.thresholds.maxStateP95Delta, + noteIfMissing: "state delta unavailable." + ) + ) + checks.append( + upperBoundCheck( + key: "robust_meas_mismatch", + title: "Measurement mismatch rate", + value: mismatchRate, + maxAllowed: policy.thresholds.maxMeasurementMismatchRate, + noteIfMissing: "measurement mismatch rate unavailable." + ) + ) + + let missingRequiredMetric = checks.contains { $0.value == nil } + let passedCount = checks.filter(\.passed).count + let score = checks.isEmpty ? nil : (100.0 * Double(passedCount) / Double(checks.count)) + + let status: StateRobustnessStatus + if missingRequiredMetric { + status = .unavailable + } else if passedCount == checks.count { + status = .robust + } else if passedCount > 0 { + status = .sensitive + } else { + status = .fragile + } + + var detailParts: [String] = [] + detailParts.append(context.calibratedProfile == nil ? "model=noise" : "model=calibrated") + detailParts.append("policy=\(policy.id):v\(policy.version)") + detailParts.append("samples=\(sampleCount)") + detailParts.append("success=\(successfulRuns)") + detailParts.append("failed=\(failedRuns)") + if let successRate { + detailParts.append(String(format: "success_rate=%.3f", successRate)) + } + if let meanP95 { + detailParts.append(String(format: "p95_d_mean=%.4e", meanP95)) + } + if let stateP95 { + detailParts.append(String(format: "p95_d_state=%.4e", stateP95)) + } + if let mismatchRate { + detailParts.append(String(format: "meas_mismatch=%.3f", mismatchRate)) + } + if let score { + detailParts.append(String(format: "pass_rate=%.1f%%", score)) + } + + return StateRobustnessSummary( + status: status, + score: score, + sampleCount: sampleCount, + successRate: successRate, + meanPhotonP95Delta: meanP95, + stateP95Delta: stateP95, + measurementMismatchRate: mismatchRate, + checks: checks, + detail: detailParts.joined(separator: " ") + ) + } + + private func buildKPIPolicyEvaluationSummary( + runResponse: RunResponse, + policy: KPIPolicyDefinition? + ) -> KPIPolicyEvaluationSummary { + guard let policy else { + return KPIPolicyEvaluationSummary( + policyID: "none", + policyName: "No policy selected", + policyVersion: 0, + status: .unavailable, + checks: [], + recommendation: "Select a KPI policy to evaluate production readiness.", + detail: "KPI evaluation unavailable: no policy selected." + ) + } + + let representation = runResponse.finalState?.representation ?? "unavailable" + if policy.targetRepresentation != "any", policy.targetRepresentation != representation { + return KPIPolicyEvaluationSummary( + policyID: policy.id, + policyName: policy.name, + policyVersion: policy.version, + status: .unavailable, + checks: [], + recommendation: "Selected policy targets \(policy.targetRepresentation), but this run produced \(representation).", + detail: "KPI unavailable: representation mismatch." + ) + } + + let qecMetrics = qecMetricSnapshot(from: runResponse.qec) + var checks: [ThresholdCheckResult] = [] + if let minPhoton = policy.thresholds.minMeanPhotonNumber { + checks.append( + lowerBoundCheck( + key: "kpi_min_photon", + title: "Mean photon number minimum", + value: runResponse.meanPhotonNumber, + minAllowed: minPhoton, + noteIfMissing: "mean_photon_number unavailable." + ) + ) + } + if let maxPhoton = policy.thresholds.maxMeanPhotonNumber { + checks.append( + upperBoundCheck( + key: "kpi_max_photon", + title: "Mean photon number maximum", + value: runResponse.meanPhotonNumber, + maxAllowed: maxPhoton, + noteIfMissing: "mean_photon_number unavailable." + ) + ) + } + + if let minSqueezingDB = policy.thresholds.minSqueezingDB { + checks.append( + lowerBoundCheck( + key: "kpi_min_squeezing_db", + title: "Minimum mode squeezing (dB)", + value: runResponse.finalState.flatMap(gaussianMinModeSqueezingDB), + minAllowed: minSqueezingDB, + noteIfMissing: "Gaussian squeezing metric unavailable." + ) + ) + } + + if let minPurity = policy.thresholds.minGaussianPurity { + checks.append( + lowerBoundCheck( + key: "kpi_min_gaussian_purity", + title: "Gaussian purity minimum", + value: runResponse.finalState.flatMap(gaussianPurity), + minAllowed: minPurity, + noteIfMissing: "Gaussian purity unavailable." + ) + ) + } + + if let maxNormalizationError = policy.thresholds.maxFockNormalizationError { + checks.append( + upperBoundCheck( + key: "kpi_max_fock_norm_error", + title: "Fock normalization error", + value: runResponse.finalState.flatMap(fockNormalizationError), + maxAllowed: maxNormalizationError, + noteIfMissing: "Fock normalization metric unavailable." + ) + ) + } + + if let maxLogicalErrorRate = policy.thresholds.maxLogicalErrorRate { + checks.append( + upperBoundCheck( + key: "kpi_qec_logical_error_rate", + title: "QEC logical error rate", + value: qecMetrics?.logicalErrorRate, + maxAllowed: maxLogicalErrorRate, + noteIfMissing: "QEC logical error rate unavailable." + ) + ) + } + + if let minSuppressionFactor = policy.thresholds.minSuppressionFactor { + checks.append( + lowerBoundCheck( + key: "kpi_qec_suppression_factor", + title: "QEC suppression factor vs physical noise", + value: qecMetrics?.suppressionFactor, + minAllowed: minSuppressionFactor, + noteIfMissing: "QEC suppression factor unavailable." + ) + ) + } + + if let minBreakEvenGain = policy.thresholds.minBreakEvenGain { + checks.append( + lowerBoundCheck( + key: "kpi_qec_break_even_gain", + title: "QEC break-even gain", + value: qecMetrics?.breakEvenGain, + minAllowed: minBreakEvenGain, + noteIfMissing: "QEC break-even gain unavailable." + ) + ) + } + + let hasMissingMetric = checks.contains { $0.value == nil } + let passedCount = checks.filter(\.passed).count + let status: KPIPolicyStatus + if checks.isEmpty || hasMissingMetric { + status = .unavailable + } else if passedCount == checks.count { + status = .pass + } else { + status = .fail + } + + let recommendation: String + switch status { + case .pass: + recommendation = "Circuit meets all selected KPI policy thresholds." + case .fail: + recommendation = "Adjust circuit parameters or calibration to satisfy failed KPI thresholds before production use." + case .unavailable: + recommendation = "Required KPI metrics are unavailable for this run/policy combination." + } + + var detailParts: [String] = [] + detailParts.append("policy=\(policy.id):v\(policy.version)") + detailParts.append("checks=\(checks.count)") + detailParts.append("passed=\(passedCount)") + detailParts.append("status=\(status.title)") + if let rounds = qecMetrics?.roundsExecuted { + detailParts.append("qec_rounds=\(rounds)") + } + if let logicalErrorRate = qecMetrics?.logicalErrorRate { + detailParts.append(String(format: "qec_ler=%.4f", logicalErrorRate)) + } + if let suppressionFactor = qecMetrics?.suppressionFactor { + detailParts.append(String(format: "qec_suppr=%.3f", suppressionFactor)) + } + if let breakEvenGain = qecMetrics?.breakEvenGain { + detailParts.append(String(format: "qec_break_even=%.4f", breakEvenGain)) + } + + return KPIPolicyEvaluationSummary( + policyID: policy.id, + policyName: policy.name, + policyVersion: policy.version, + status: status, + checks: checks, + recommendation: recommendation, + detail: detailParts.joined(separator: " ") + ) + } + + private struct QECMetricSnapshot { + let roundsExecuted: Int? + let logicalErrorRate: Double? + let physicalErrorRateProxy: Double? + let suppressionFactor: Double? + let breakEvenGain: Double? + } + + private func qecMetricSnapshot(from report: QECReport?) -> QECMetricSnapshot? { + guard let report else { return nil } + + let roundsExecuted = report.roundsExecuted ?? report.rounds?.count + + let logicalErrorRate: Double? = { + if let value = finiteOrNil(report.logicalErrorRate) { + return value + } + guard let rounds = roundsExecuted, rounds > 0 else { return nil } + guard let logicalFailCount = report.logicalFailCount else { return nil } + return finiteOrNil(Double(logicalFailCount) / Double(rounds)) + }() + + let physicalErrorRateProxy: Double? = { + if let value = finiteOrNil(report.physicalErrorRateProxy) { + return value + } + guard let rounds = report.rounds, !rounds.isEmpty else { return nil } + + var valid = 0 + var flagged = 0 + for round in rounds { + guard let syndrome = round.syndromeValue, + let latticeSpacing = round.latticeSpacing, + latticeSpacing > 0, + let targetIndex = round.targetLatticeIndex + else { + continue + } + + valid += 1 + let target = Double(targetIndex) * latticeSpacing + let threshold = 0.25 * latticeSpacing + if abs(syndrome - target) >= threshold { + flagged += 1 + } + } + + guard valid > 0 else { return nil } + return finiteOrNil(Double(flagged) / Double(valid)) + }() + + let suppressionFactor: Double? = { + if let value = finiteOrNil(report.suppressionFactor) { + return value + } + guard let physicalErrorRateProxy, physicalErrorRateProxy >= 0 else { return nil } + guard let logicalErrorRate, logicalErrorRate >= 0 else { return nil } + if logicalErrorRate == 0 { + return physicalErrorRateProxy > 0 ? 1_000_000.0 : nil + } + return finiteOrNil(physicalErrorRateProxy / logicalErrorRate) + }() + + let breakEvenGain: Double? = { + if let value = finiteOrNil(report.breakEvenGain) { + return value + } + guard let physicalErrorRateProxy, let logicalErrorRate else { return nil } + return finiteOrNil(physicalErrorRateProxy - logicalErrorRate) + }() + + return QECMetricSnapshot( + roundsExecuted: roundsExecuted, + logicalErrorRate: logicalErrorRate, + physicalErrorRateProxy: physicalErrorRateProxy, + suppressionFactor: suppressionFactor, + breakEvenGain: breakEvenGain + ) + } + + private func finiteOrNil(_ value: Double?) -> Double? { + guard let value, value.isFinite else { return nil } + return value + } + + private func healthRecommendation( + validity: StateValidityStatus, + confidence: StateConfidenceStatus, + robustness: StateRobustnessStatus, + kpiStatus: KPIPolicyStatus, + errors: [String], + warnings: [String] + ) -> String { + if !errors.isEmpty { + return "Fix state validity errors first, then rerun parity and KPI/robustness checks." + } + if kpiStatus == .fail { + return "KPI policy failed. Tune circuit parameters and recalibrate hardware until every KPI threshold passes." + } + if confidence == .unmatched { + return "Runtime outputs diverge beyond tolerance; align backend support, seed/cutoff settings, and gate compatibility." + } + if robustness == .fragile { + return "Robustness thresholds failed under calibrated loss/noise; improve noise margin before production rollout." + } + if robustness == .sensitive { + return "Robustness is partially compliant; investigate failed robustness thresholds and rerun calibrated Monte Carlo." + } + if validity == .partial || !warnings.isEmpty { + return "Resolve remaining warnings for stronger production confidence." + } + if confidence == .drift { + return "Minor runtime drift detected; acceptable for R&D, but recheck precision before production promotion." + } + if kpiStatus == .unavailable { + return "KPI metrics are incomplete for the selected policy; ensure representation and outputs provide required observables." + } + return "State is physically valid, policy-compliant, and robust under configured calibrated noise." + } + + private func lowerBoundCheck( + key: String, + title: String, + value: Double?, + minAllowed: Double, + noteIfMissing: String + ) -> ThresholdCheckResult { + guard let value, value.isFinite else { + return ThresholdCheckResult( + key: key, + title: title, + passed: false, + value: nil, + threshold: minAllowed, + comparator: ">=", + note: noteIfMissing + ) + } + + return ThresholdCheckResult( + key: key, + title: title, + passed: value >= minAllowed, + value: value, + threshold: minAllowed, + comparator: ">=", + note: nil + ) + } + + private func upperBoundCheck( + key: String, + title: String, + value: Double?, + maxAllowed: Double, + noteIfMissing: String + ) -> ThresholdCheckResult { + guard let value, value.isFinite else { + return ThresholdCheckResult( + key: key, + title: title, + passed: false, + value: nil, + threshold: maxAllowed, + comparator: "<=", + note: noteIfMissing + ) + } + + return ThresholdCheckResult( + key: key, + title: title, + passed: value <= maxAllowed, + value: value, + threshold: maxAllowed, + comparator: "<=", + note: nil + ) + } + + private func gaussianMinModeSqueezingDB(_ state: QuantumStateSnapshot) -> Double? { + guard state.representation == "gaussian_phase_space", let covariance = state.covariance else { return nil } + guard !covariance.isEmpty, isSquareMatrix(covariance) else { return nil } + + let modeCount = covariance.count / 2 + guard modeCount > 0 else { return nil } + var minSqueezingDB: Double? + + for mode in 0.. Double? { + guard state.representation == "gaussian_phase_space", let covariance = state.covariance else { return nil } + guard !covariance.isEmpty, isSquareMatrix(covariance) else { return nil } + let dimension = covariance.count + var twoV = covariance + for row in 0.. 0, det.isFinite else { return nil } + let purity = 1.0 / sqrt(det) + return purity.isFinite ? purity : nil + } + + private func fockNormalizationError(_ state: QuantumStateSnapshot) -> Double? { + guard state.representation == "fock_number_basis", let probabilities = state.probabilities else { return nil } + guard !probabilities.isEmpty else { return nil } + let total = probabilities.reduce(0.0, +) + return abs(total - 1.0) + } + + private func determinant(_ matrix: [[Double]]) -> Double? { + guard !matrix.isEmpty, isSquareMatrix(matrix) else { return nil } + let n = matrix.count + var a = matrix + var sign = 1.0 + + for pivot in 0.. maxAbs { + maxAbs = value + maxRow = row + } + } + if maxAbs < 1e-18 { + return 0.0 + } + if maxRow != pivot { + a.swapAt(maxRow, pivot) + sign *= -1.0 + } + + let pivotValue = a[pivot][pivot] + for row in (pivot + 1).. Double? { + guard !values.isEmpty else { return nil } + let sorted = values.sorted() + let clamped = min(max(fraction, 0.0), 1.0) + let position = clamped * Double(sorted.count - 1) + let lowerIndex = Int(floor(position)) + let upperIndex = Int(ceil(position)) + if lowerIndex == upperIndex { + return sorted[lowerIndex] + } + let weight = position - Double(lowerIndex) + return sorted[lowerIndex] * (1.0 - weight) + sorted[upperIndex] * weight + } + + private func evaluateGaussianStateHealth( + _ state: QuantumStateSnapshot, + checksCompleted: inout Int, + errors: inout [String], + warnings: inout [String] + ) { + checksCompleted += 1 + guard let mean = state.mean, !mean.isEmpty else { + errors.append("Gaussian mean vector unavailable") + return + } + if !allFinite(mean) { + errors.append("Gaussian mean has NaN/Inf") + } + + checksCompleted += 1 + guard let covariance = state.covariance, !covariance.isEmpty else { + errors.append("Gaussian covariance unavailable") + return + } + if !isSquareMatrix(covariance) { + errors.append("Gaussian covariance is not square") + return + } + if !allFinite(covariance.flatMap { $0 }) { + errors.append("Gaussian covariance has NaN/Inf") + return + } + + checksCompleted += 1 + if let modes = state.modes, modes > 0 { + let expectedDim = modes * 2 + if mean.count != expectedDim { + warnings.append("Gaussian mean dimension mismatch") + } + if covariance.count != expectedDim { + warnings.append("Gaussian covariance dimension mismatch") + } + } + + checksCompleted += 1 + if covariance.indices.contains(where: { covariance[$0][$0] < -1e-9 }) { + errors.append("Gaussian covariance has negative diagonal entries") + } + + checksCompleted += 1 + let asymmetry = matrixAsymmetryMax(covariance) + if asymmetry > 1e-3 { + errors.append("Gaussian covariance asymmetry too large") + } else if asymmetry > 1e-6 { + warnings.append("Gaussian covariance asymmetry above nominal tolerance") + } + } + + private func evaluateFockStateHealth( + _ state: QuantumStateSnapshot, + checksCompleted: inout Int, + errors: inout [String], + warnings: inout [String] + ) { + checksCompleted += 1 + guard let probabilities = state.probabilities, !probabilities.isEmpty else { + errors.append("Fock probabilities unavailable") + return + } + if !allFinite(probabilities) { + errors.append("Fock probabilities contain NaN/Inf") + return + } + + checksCompleted += 1 + if probabilities.contains(where: { $0 < -1e-9 || $0 > 1.0 + 1e-9 }) { + errors.append("Fock probabilities outside [0, 1]") + } + + checksCompleted += 1 + let total = probabilities.reduce(0.0, +) + if abs(total - 1.0) > 1e-3 { + errors.append(String(format: "Fock normalization drift is %.3e", abs(total - 1.0))) + } else if abs(total - 1.0) > 1e-6 { + warnings.append(String(format: "Fock normalization near tolerance (%.3e)", abs(total - 1.0))) + } + } + + private func allFinite(_ values: [Double]) -> Bool { + values.allSatisfy { $0.isFinite } + } + + private func isSquareMatrix(_ matrix: [[Double]]) -> Bool { + guard !matrix.isEmpty else { return false } + let dimension = matrix.count + return matrix.allSatisfy { $0.count == dimension } + } + + private func matrixAsymmetryMax(_ matrix: [[Double]]) -> Double { + guard isSquareMatrix(matrix) else { return .infinity } + let dimension = matrix.count + var maxAsymmetry = 0.0 + for row in 0.. Double? { + guard let lhs, let rhs else { return nil } + return abs(lhs - rhs) + } + + private func delta(_ lhs: Int?, _ rhs: Int?) -> Int? { + guard let lhs, let rhs else { return nil } + return lhs - rhs + } + + private func maxStateDelta(_ lhs: QuantumStateSnapshot?, _ rhs: QuantumStateSnapshot?) -> Double? { + guard let lhs, let rhs else { return nil } + guard lhs.representation == rhs.representation else { return nil } + + switch lhs.representation { + case "gaussian_phase_space": + guard let lhsMean = lhs.mean, let rhsMean = rhs.mean, + let lhsCov = lhs.covariance, let rhsCov = rhs.covariance + else { + return nil + } + let meanDelta = maxAbsDelta(lhsMean, rhsMean) + let covDelta = maxAbsDelta(lhsCov.flatMap { $0 }, rhsCov.flatMap { $0 }) + return max(meanDelta ?? 0.0, covDelta ?? 0.0) + case "fock_number_basis": + guard let lhsProb = lhs.probabilities, let rhsProb = rhs.probabilities else { return nil } + return maxAbsDelta(lhsProb, rhsProb) + default: + return nil + } + } + + private func maxAbsDelta(_ lhs: [Double], _ rhs: [Double]) -> Double? { + guard lhs.count == rhs.count, !lhs.isEmpty else { return nil } + var maxDelta = 0.0 + for index in lhs.indices { + maxDelta = max(maxDelta, abs(lhs[index] - rhs[index])) + } + return maxDelta + } +} diff --git a/core-swift/Sources/schrosim-studio/StudioViews.swift b/core-swift/Sources/schrosim-studio/StudioViews.swift new file mode 100644 index 0000000..0249000 --- /dev/null +++ b/core-swift/Sources/schrosim-studio/StudioViews.swift @@ -0,0 +1,4059 @@ +import SwiftUI +import AppKit + +private struct ThemePalette { + let appBackground: Color + let panel: Color + let subpanel: Color + let inputBackground: Color + let border: Color + let dashedBorder: Color + let gridMinor: Color + let gridMajor: Color + + static func resolve(_ theme: AppTheme) -> ThemePalette { + switch theme { + case .white: + return ThemePalette( + appBackground: Color(red: 0.945, green: 0.952, blue: 0.965), + panel: .white, + subpanel: Color(red: 0.965, green: 0.972, blue: 0.985), + inputBackground: .white, + border: Color.black.opacity(0.10), + dashedBorder: Color.blue.opacity(0.45), + gridMinor: Color.black.opacity(0.04), + gridMajor: Color.black.opacity(0.08) + ) + case .black: + return ThemePalette( + appBackground: Color(red: 0.07, green: 0.08, blue: 0.10), + panel: Color(red: 0.10, green: 0.11, blue: 0.14), + subpanel: Color(red: 0.13, green: 0.15, blue: 0.19), + inputBackground: Color(red: 0.15, green: 0.17, blue: 0.21), + border: Color.white.opacity(0.16), + dashedBorder: Color.blue.opacity(0.60), + gridMinor: Color.white.opacity(0.05), + gridMajor: Color.white.opacity(0.12) + ) + } + } +} + +struct StudioAppShellView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(spacing: 0) { + GlobalTopBarView() + Divider() + + HSplitView { + LeftRailView() + .frame(minWidth: 220, idealWidth: 280, maxWidth: 340) + .layoutPriority(1) + + CenterWorkspaceView() + .frame(minWidth: 720, maxWidth: .infinity, maxHeight: .infinity) + .layoutPriority(0) + + RightInspectorView() + .frame(minWidth: 320, idealWidth: 380, maxWidth: 460) + .layoutPriority(2) + } + .frame(minHeight: 420) + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Divider() + BottomPanelView() + .frame(height: 220) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .font(.system(size: 13, weight: .regular, design: .default)) + .background(palette.appBackground.ignoresSafeArea()) + .sheet(isPresented: $store.isAnalysisPopupPresented) { + AnalysisPopupWindowView() + .environmentObject(store) + .frame(minWidth: 1160, minHeight: 760) + } + .alert("Error, check your gates.", isPresented: $store.isRunBlockedAlertPresented) { + Button("OK", role: .cancel) {} + } message: { + Text(store.runBlockedAlertMessage) + } + } +} + +private struct GlobalTopBarView: View { + @EnvironmentObject private var store: StudioStore + @State private var availableWidth: CGFloat = 0 + private var palette: ThemePalette { .resolve(store.theme) } + private let horizontalInset: CGFloat = 14 + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 10) { + Label("SchroSIM Intelligence", systemImage: "square.grid.2x2") + .font(.title3.weight(.semibold)) + Text("| Photonic Quantum Circuit Simulator") + .font(.headline) + .foregroundStyle(.secondary) + Spacer() + Picker("Theme", selection: $store.theme) { + ForEach(AppTheme.allCases) { theme in + Text(theme.rawValue).tag(theme) + } + } + .pickerStyle(.segmented) + .frame(width: 170) + .labelsHidden() + + HStack(spacing: 8) { + Button("New") { store.newAction() } + .keyboardShortcut("n", modifiers: [.command]) + Button("Open") { store.openAction() } + .keyboardShortcut("o", modifiers: [.command]) + Button("Save") { store.saveAction() } + .keyboardShortcut("s", modifiers: [.command]) + Button("Save As") { store.saveAsAction() } + .keyboardShortcut("s", modifiers: [.command, .shift]) + } + } + + HStack(alignment: .bottom, spacing: 12) { + LabeledContentBlock(title: "Workspace") { + Picker("Workspace", selection: $store.workspace) { + ForEach(store.workspaces, id: \.self) { value in + Text(value).tag(value) + } + } + .pickerStyle(.menu) + .labelsHidden() + .frame(width: 210, alignment: .leading) + } + + LabeledContentBlock(title: "Project") { + Picker("Project", selection: $store.project) { + ForEach(store.projects, id: \.self) { value in + Text(value).tag(value) + } + } + .pickerStyle(.menu) + .labelsHidden() + .frame(width: 210, alignment: .leading) + } + + LabeledContentBlock(title: "Environment") { + Picker("Environment", selection: $store.environment) { + ForEach(EnvironmentBadge.allCases) { value in + Text(value.rawValue.uppercased()).tag(value) + } + } + .pickerStyle(.menu) + .labelsHidden() + .frame(width: 120, alignment: .leading) + } + + LabeledContentBlock(title: "Role") { + Picker("Role", selection: $store.userRole) { + ForEach(UserRoleBadge.allCases) { role in + Text(role.rawValue).tag(role) + } + } + .pickerStyle(.menu) + .labelsHidden() + .frame(width: 105, alignment: .leading) + } + + Spacer(minLength: 0) + + HStack(spacing: 8) { + Button("Validate") { store.validateAction() } + Button("Compile") { store.compileAction() } + Button("Run") { Task { await store.runAction() } } + .buttonStyle(.borderedProminent) + .disabled(store.isRunning) + Button("Analysis") { store.analyzeAction() } + .buttonStyle(.bordered) + Button("Delete") { store.deleteSelection() } + .buttonStyle(.bordered) + .keyboardShortcut(.delete, modifiers: []) + .disabled(!store.canDeleteSelection) + Button("Export") { store.exportAction() } + RuntimePhaseIndicatorView() + .environmentObject(store) + .padding(.leading, 6) + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.trailing, 2) + } + } + .frame(minWidth: max(0, availableWidth - (horizontalInset * 2)), alignment: .leading) + } + .padding(.horizontal, horizontalInset) + .padding(.vertical, 10) + .background( + GeometryReader { proxy in + Color.clear + .onAppear { + availableWidth = proxy.size.width + } + .onChange(of: proxy.size.width) { newValue in + availableWidth = newValue + } + } + ) + .background(palette.panel) + } +} + +private struct RuntimePhaseIndicatorView: View { + @EnvironmentObject private var store: StudioStore + + private var phase: RuntimePhase { + store.runtimePhase + } + + private var isFlashing: Bool { + phase == .compiling || phase == .running + } + + private var phaseColor: Color { + switch phase { + case .standby: + return Color.gray + case .compiling: + return Color.orange + case .running: + return Color.blue + case .finished: + return Color.green + } + } + + var body: some View { + HStack(spacing: 7) { + TimelineView(.animation(minimumInterval: 0.12, paused: !isFlashing)) { timeline in + let now = timeline.date.timeIntervalSinceReferenceDate + let oscillation = (sin(now * 7.0) + 1.0) * 0.5 + let opacity = isFlashing ? (0.35 + oscillation * 0.65) : 1.0 + let scale = isFlashing ? (0.90 + oscillation * 0.20) : 1.0 + + Circle() + .fill(phaseColor) + .frame(width: 9, height: 9) + .scaleEffect(scale) + .opacity(opacity) + } + .frame(width: 11, height: 11) + + Text(phase.title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.primary) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(phaseColor.opacity(0.12)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(phaseColor.opacity(0.30), lineWidth: 1) + ) + .help("Runtime state: \(phase.title)") + } +} + +private struct LeftRailView: View { + @EnvironmentObject private var store: StudioStore + @State private var hoveredKind: GateKind? + private var palette: ThemePalette { .resolve(store.theme) } + private var iconCoverageText: String { + let total = GateKind.allCases.count + let mapped = GateKind.allCases.filter { !$0.systemIcon.isEmpty }.count + return "Icons \(mapped)/\(total)" + } + private var categorizedKinds: [(section: PaletteSection, items: [GateKind])] { + PaletteSection.allCases.compactMap { section in + let items = GateKind.allCases.filter { kind in + kind.section == section && store.matchesSearch(kind) && store.matchesPaletteFilter(kind) + } + return items.isEmpty ? nil : (section, items) + } + } + + private func sectionTitle(_ section: PaletteSection) -> String { + switch section { + case .sourceAndStatePrep: + return "Source & State Prep" + case .gaussianOps: + return "Gaussian Ops" + case .nonGaussianOps: + return "Non-Gaussian Ops" + case .channels: + return "Channels" + case .measurements: + return "Measurements" + case .classicalControl: + return "Control" + } + } + + private var primaryPaletteFilters: [PaletteFilter] { + [.all, .gaussian, .nonGaussian, .channels] + } + + private var secondaryPaletteFilters: [PaletteFilter] { + [.measurement, .control] + } + + private func paletteFilterChip(_ filter: PaletteFilter) -> some View { + let isSelected = filter == store.paletteFilter + return Button(filter.rawValue) { + store.paletteFilter = filter + } + .buttonStyle(.plain) + .font(.caption.weight(.semibold)) + .foregroundStyle(isSelected ? Color.white : Color.primary) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background( + Capsule() + .fill(isSelected ? Color.accentColor : palette.inputBackground) + .overlay( + Capsule() + .stroke(palette.border, lineWidth: isSelected ? 0 : 1) + ) + ) + } + + var body: some View { + CardSection { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass") + .foregroundStyle(.secondary) + TextField("Search", text: $store.paletteSearch) + .textFieldStyle(.plain) + .font(.caption) + } + .padding(.horizontal, 8) + .padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 6) { + ForEach(primaryPaletteFilters) { filter in + paletteFilterChip(filter) + } + } + + HStack(spacing: 6) { + ForEach(secondaryPaletteFilters) { filter in + paletteFilterChip(filter) + } + } + } + .padding(.vertical, 2) + + ScrollView { + VStack(alignment: .leading, spacing: 10) { + ForEach(categorizedKinds, id: \.section) { group in + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(sectionTitle(group.section)) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Spacer() + Text("\(group.items.count)") + .font(.caption2.monospacedDigit()) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(palette.inputBackground) + ) + } + + LazyVGrid( + columns: [GridItem(.adaptive(minimum: 44, maximum: 44), spacing: 8)], + alignment: .leading, + spacing: 8 + ) { + ForEach(group.items) { kind in + PaletteIconCell( + kind: kind, + disabledReason: store.disabledReason(for: kind), + theme: store.theme + ) { isHovering in + if isHovering { + hoveredKind = kind + } else if hoveredKind == kind { + hoveredKind = nil + } + } + } + } + } + .padding(8) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(palette.border, lineWidth: 1) + ) + ) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 4) + } + + Text(hoveredKind?.hoverDisplayName ?? "Hover icons for descriptions") + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, minHeight: 32) + + Text(iconCoverageText) + .font(.caption2) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } +} + +private struct PaletteIconCell: View { + let kind: GateKind + let disabledReason: String? + let theme: AppTheme + let onHoverState: (Bool) -> Void + private var palette: ThemePalette { .resolve(theme) } + + var body: some View { + let content = VStack(spacing: 4) { + Image(systemName: kind.systemIcon) + .font(.system(size: 16, weight: .semibold)) + Text(kind.symbol) + .font(.system(size: 10, weight: .semibold, design: .monospaced)) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + .frame(width: 44, height: 44) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke( + disabledReason == nil ? Color.blue.opacity(0.50) : Color.gray.opacity(0.35), + style: StrokeStyle(lineWidth: 1, dash: disabledReason == nil ? [5, 3] : []) + ) + ) + ) + .opacity(disabledReason == nil ? 1.0 : 0.45) + .help(disabledReason ?? kind.hoverDisplayName) + .onHover { isHovering in + onHoverState(isHovering) + } + + if disabledReason == nil { + content.draggable(kind.rawValue) + } else { + content + } + } +} + +private struct CenterWorkspaceView: View { + @EnvironmentObject private var store: StudioStore + @State private var zoom: CGFloat = 1.0 + @State private var committedZoom: CGFloat = 1.0 + @State private var panOffset: CGSize = .zero + @State private var committedPanOffset: CGSize = .zero + private var palette: ThemePalette { .resolve(store.theme) } + private let slotWidth: CGFloat = 140 + private let slotHeight: CGFloat = 68 + private let laneLabelWidth: CGFloat = 110 + private let rowSpacing: CGFloat = 10 + private let columnSpacing: CGFloat = 8 + private let canvasPadding: CGFloat = 10 + private let timelineHeaderHeight: CGFloat = 24 + private let minZoom: CGFloat = 0.6 + private let maxZoom: CGFloat = 2.4 + + var body: some View { + CardSection(title: "WorkSpace") { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 14) { + Toggle("Grid", isOn: $store.showGrid) + Toggle("Connector", isOn: $store.showConnectors) + Toggle("Photon Live", isOn: $store.showPhotonFlow) + Toggle("Selection", isOn: $store.showSelection) + Toggle("Validation", isOn: $store.showValidation) + Spacer() + Text("Zoom \(Int(zoom * 100))%") + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + Button("Reset View") { + resetView() + } + .buttonStyle(.bordered) + } + .toggleStyle(.checkbox) + .font(.caption) + + GeometryReader { proxy in + let viewportSize = proxy.size + let canvasSize = contentSize(for: viewportSize) + let laneTopInset = canvasPadding + timelineHeaderHeight + + ZStack(alignment: .topLeading) { + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(palette.border, lineWidth: 1) + ) + + if store.showGrid { + InfiniteMetricGridBackground( + theme: store.theme, + zoom: zoom, + panOffset: panOffset + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .allowsHitTesting(false) + } + + ZStack(alignment: .topLeading) { + SlotTimelineReferenceView( + slotCount: store.slotCount, + laneLabelWidth: laneLabelWidth, + slotWidth: slotWidth, + columnSpacing: columnSpacing, + leadingInset: canvasPadding, + topInset: canvasPadding + ) + + if store.showConnectors || store.showPhotonFlow { + ConnectorOverlayView( + modeCount: store.modeCount, + slotCount: store.slotCount, + laneLabelWidth: laneLabelWidth, + slotWidth: slotWidth, + slotHeight: slotHeight, + leadingInset: canvasPadding, + topInset: laneTopInset, + rowSpacing: rowSpacing, + columnSpacing: columnSpacing, + nodes: store.nodes, + zoom: zoom, + showConnectors: store.showConnectors, + showPhotonFlow: store.showPhotonFlow + ) + } + + if store.showPhotonFlow { + PhotonMechanicalGateOverlayView( + modeCount: store.modeCount, + slotCount: store.slotCount, + laneLabelWidth: laneLabelWidth, + slotWidth: slotWidth, + slotHeight: slotHeight, + topInset: laneTopInset, + rowSpacing: rowSpacing, + columnSpacing: columnSpacing, + leadingInset: canvasPadding, + nodes: store.nodes, + zoom: zoom + ) + } + + VStack(alignment: .leading, spacing: rowSpacing) { + ForEach(0..") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 6) + .padding(.vertical, 6) + .frame(width: laneLabelWidth, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8) + .fill( + (store.selectedModeIndex == mode && store.selectedNodeID == nil) + ? Color.accentColor.opacity(0.18) + : Color.clear + ) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke( + (store.selectedModeIndex == mode && store.selectedNodeID == nil) + ? Color.accentColor.opacity(0.70) + : Color.clear, + lineWidth: 1 + ) + ) + .contentShape(RoundedRectangle(cornerRadius: 8)) + .onTapGesture { + store.selectMode(mode) + } + + if store.showPhotonFlow { + Color.clear + .frame(width: slotTrackWidth, height: slotHeight) + } else { + ForEach(0.. CGSize { + let lanesWidth = laneLabelWidth + + columnSpacing + + CGFloat(store.slotCount) * slotWidth + + CGFloat(max(store.slotCount - 1, 0)) * columnSpacing + + canvasPadding * 2 + let lanesHeight = CGFloat(store.modeCount) * slotHeight + + CGFloat(max(store.modeCount - 1, 0)) * rowSpacing + + timelineHeaderHeight + + 84 + + canvasPadding * 2 + return CGSize( + width: max(viewport.width, lanesWidth), + height: max(viewport.height, lanesHeight) + ) + } + + private var slotTrackWidth: CGFloat { + CGFloat(store.slotCount) * slotWidth + + CGFloat(max(store.slotCount - 1, 0)) * columnSpacing + } + + private func resetView() { + committedZoom = 1.0 + zoom = 1.0 + committedPanOffset = .zero + panOffset = .zero + } + + private func clampedZoom(_ proposed: CGFloat) -> CGFloat { + min(max(proposed, minZoom), maxZoom) + } + + private func clampedOffset( + _ proposed: CGSize, + viewportSize: CGSize, + canvasSize: CGSize, + zoomScale: CGFloat + ) -> CGSize { + let contentWidth = canvasSize.width * zoomScale + let contentHeight = canvasSize.height * zoomScale + let slack: CGFloat = 96 + let overflowX = max(0, contentWidth - viewportSize.width) + let overflowY = max(0, contentHeight - viewportSize.height) + let minX = -overflowX - slack + let maxX = slack + let minY = -overflowY - slack + let maxY = slack + return CGSize( + width: min(max(proposed.width, minX), maxX), + height: min(max(proposed.height, minY), maxY) + ) + } + + private func normalizeView(viewportSize: CGSize, canvasSize: CGSize) { + let normalized = clampedOffset( + committedPanOffset, + viewportSize: viewportSize, + canvasSize: canvasSize, + zoomScale: committedZoom + ) + committedPanOffset = normalized + panOffset = normalized + } + + private func panGesture(viewportSize: CGSize, canvasSize: CGSize) -> some Gesture { + DragGesture(minimumDistance: 4) + .onChanged { value in + let proposed = CGSize( + width: committedPanOffset.width + value.translation.width, + height: committedPanOffset.height + value.translation.height + ) + panOffset = clampedOffset( + proposed, + viewportSize: viewportSize, + canvasSize: canvasSize, + zoomScale: zoom + ) + } + .onEnded { value in + let proposed = CGSize( + width: committedPanOffset.width + value.translation.width, + height: committedPanOffset.height + value.translation.height + ) + committedPanOffset = clampedOffset( + proposed, + viewportSize: viewportSize, + canvasSize: canvasSize, + zoomScale: zoom + ) + panOffset = committedPanOffset + } + } + + private func zoomGesture(viewportSize: CGSize, canvasSize: CGSize) -> some Gesture { + MagnificationGesture() + .onChanged { value in + let nextZoom = clampedZoom(committedZoom * value) + zoom = nextZoom + panOffset = clampedOffset( + panOffset, + viewportSize: viewportSize, + canvasSize: canvasSize, + zoomScale: nextZoom + ) + } + .onEnded { value in + committedZoom = clampedZoom(committedZoom * value) + zoom = committedZoom + committedPanOffset = clampedOffset( + panOffset, + viewportSize: viewportSize, + canvasSize: canvasSize, + zoomScale: committedZoom + ) + panOffset = committedPanOffset + } + } + + private func zoomWithMouseWheel( + deltaY: CGFloat, + cursorPosition: CGPoint, + viewportSize: CGSize, + canvasSize: CGSize + ) { + guard deltaY != 0 else { return } + + let multiplier = exp(deltaY * 0.01) + let nextZoom = clampedZoom(committedZoom * multiplier) + let zoomFactor = nextZoom / max(committedZoom, 0.0001) + + let nextOffset = CGSize( + width: cursorPosition.x - (cursorPosition.x - committedPanOffset.width) * zoomFactor, + height: cursorPosition.y - (cursorPosition.y - committedPanOffset.height) * zoomFactor + ) + + committedZoom = nextZoom + zoom = nextZoom + committedPanOffset = clampedOffset( + nextOffset, + viewportSize: viewportSize, + canvasSize: canvasSize, + zoomScale: nextZoom + ) + panOffset = committedPanOffset + } +} + +private struct MouseWheelMonitor: NSViewRepresentable { + let onScroll: (CGFloat, CGPoint) -> Void + + func makeNSView(context: Context) -> TrackingView { + let view = TrackingView() + view.onScroll = onScroll + return view + } + + func updateNSView(_ nsView: TrackingView, context: Context) { + nsView.onScroll = onScroll + } + + final class TrackingView: NSView { + var onScroll: ((CGFloat, CGPoint) -> Void)? + private var monitor: Any? + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + installMonitorIfNeeded() + } + + override func removeFromSuperview() { + super.removeFromSuperview() + removeMonitorIfNeeded() + } + + deinit { + removeMonitorIfNeeded() + } + + private func installMonitorIfNeeded() { + guard monitor == nil else { return } + + monitor = NSEvent.addLocalMonitorForEvents(matching: [.scrollWheel]) { [weak self] event in + guard let self else { return event } + let location = self.convert(event.locationInWindow, from: nil) + guard self.bounds.contains(location) else { return event } + self.onScroll?(event.scrollingDeltaY, location) + return nil + } + } + + private func removeMonitorIfNeeded() { + guard let monitor else { return } + NSEvent.removeMonitor(monitor) + self.monitor = nil + } + } +} + +private struct CircuitSlotCellView: View { + @EnvironmentObject private var store: StudioStore + let mode: Int + let slot: Int + + @State private var isTargeted = false + private var palette: ThemePalette { .resolve(store.theme) } + private var node: CircuitNode? { + store.nodeAt(mode: mode, slot: slot) + } + private var isSelected: Bool { + store.showSelection && node?.id == store.selectedNodeID + } + private var validationColor: Color? { + guard store.showValidation, let node else { return nil } + switch store.validationSeverity(for: node) { + case .error: + return .red + case .warning: + return .orange + case .info: + return .blue + case .none: + break + } + return nil + } + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: 4) { + if let node { + HStack(spacing: 8) { + Image(systemName: node.kind.systemIcon) + .font(.caption.weight(.semibold)) + .frame(width: 18) + Text(node.kind.displayName) + .font(.subheadline.weight(.semibold)) + .lineLimit(1) + } + + Text(node.subtitle) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } else { + VStack(spacing: 2) { + Text("Drop") + .font(.caption.weight(.semibold)) + .foregroundStyle(.blue) + Text("+") + .font(.headline.weight(.bold)) + .foregroundStyle(.blue) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .padding(8) + .frame(width: 140, height: 68, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(node == nil ? palette.subpanel : palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke( + isSelected ? Color.accentColor : + (isTargeted ? Color.blue : (validationColor ?? palette.border)), + style: StrokeStyle( + lineWidth: isSelected || validationColor != nil ? 2 : 1, + dash: node == nil ? [5, 4] : [] + ) + ) + ) + ) + + if let validationColor { + Image(systemName: "exclamationmark.circle.fill") + .font(.caption) + .foregroundStyle(validationColor) + .padding(5) + } + } + .contentShape(RoundedRectangle(cornerRadius: 8)) + .onTapGesture { + store.selectNode(node) + } + .dropDestination(for: String.self) { items, _ in + guard let first = items.first else { return false } + store.handleDrop(kindRawValue: first, mode: mode, slot: slot) + return true + } isTargeted: { targeted in + isTargeted = targeted + } + } +} + +private struct ConnectorOverlayView: View { + let modeCount: Int + let slotCount: Int + let laneLabelWidth: CGFloat + let slotWidth: CGFloat + let slotHeight: CGFloat + let leadingInset: CGFloat + let topInset: CGFloat + let rowSpacing: CGFloat + let columnSpacing: CGFloat + let nodes: [CircuitNode] + let zoom: CGFloat + let showConnectors: Bool + let showPhotonFlow: Bool + + var body: some View { + TimelineView(.animation(minimumInterval: 1.0 / 30.0, paused: !showPhotonFlow)) { timeline in + let t = timeline.date.timeIntervalSinceReferenceDate + + Canvas { context, _ in + guard modeCount > 0, slotCount > 0 else { return } + + let startX = leftLaneX + let endX = rightLaneX + let baseWidth = max(0.9, 1.8 / max(zoom, 0.01)) + let strong = Color.black.opacity(0.62) + let medium = Color.black.opacity(0.48) + let groupedBySlot = Dictionary(grouping: nodes) { $0.slot } + let laneLength = max(endX - startX, 1) + + if showConnectors { + for mode in 0.. 1 else { continue } + let x = slotCenterX(slot) + let sortedModes = slotNodes.map(\.mode).sorted() + + for idx in 0..<(sortedModes.count - 1) { + let m0 = sortedModes[idx] + let m1 = sortedModes[idx + 1] + var verticalPath = Path() + verticalPath.move(to: CGPoint(x: x, y: laneCenterY(m0))) + verticalPath.addLine(to: CGPoint(x: x, y: laneCenterY(m1))) + context.stroke( + verticalPath, + with: .color(strong), + style: StrokeStyle(lineWidth: baseWidth, lineCap: .round, lineJoin: .round) + ) + } + } + + let nodeLaneSlotKeys = Set(nodes.map { "\($0.mode):\($0.slot)" }) + let dotRadius = max(1.4, 2.6 / max(zoom, 0.01)) + for mode in 0.. 1 else { continue } + let x = slotCenterX(slot) + let sortedModes = slotNodes.map(\.mode).sorted() + let y0 = laneCenterY(sortedModes.first ?? 0) + let y1 = laneCenterY(sortedModes.last ?? 0) + + var coupler = Path() + coupler.move(to: CGPoint(x: x, y: y0)) + coupler.addLine(to: CGPoint(x: x, y: y1)) + context.stroke( + coupler, + with: .color(Color.white.opacity(0.55)), + style: StrokeStyle(lineWidth: max(0.9, baseWidth), lineCap: .round, lineJoin: .round) + ) + } + + // Static checkpoints along slots (green for idle, cyan for active node positions). + for mode in 0.. 1 else { continue } + let x = slotCenterX(slot) + let sortedModes = slotNodes.map(\.mode).sorted() + + for idx in 0..<(sortedModes.count - 1) { + let m0 = sortedModes[idx] + let m1 = sortedModes[idx + 1] + let y0 = laneCenterY(m0) + let y1 = laneCenterY(m1) + let travel = CGFloat((t * 0.25 + Double(slot) * 0.13).truncatingRemainder(dividingBy: 1)) + let y = y0 + (y1 - y0) * travel + + let couplingRect = CGRect( + x: x - pulseRadius * 0.9, + y: y - pulseRadius * 0.9, + width: pulseRadius * 1.8, + height: pulseRadius * 1.8 + ) + context.fill(Path(ellipseIn: couplingRect), with: .color(Color.green.opacity(0.9))) + } + } + } + } + } + .allowsHitTesting(false) + } + + private var leftLaneX: CGFloat { + leadingInset + laneLabelWidth + columnSpacing + 6 + } + + private var rightLaneX: CGFloat { + leftLaneX + CGFloat(slotCount) * slotWidth + CGFloat(max(slotCount - 1, 0)) * columnSpacing - 12 + } + + private func laneCenterY(_ mode: Int) -> CGFloat { + topInset + CGFloat(mode) * (slotHeight + rowSpacing) + slotHeight / 2 + } + + private func slotCenterX(_ slot: Int) -> CGFloat { + leftLaneX + CGFloat(slot) * (slotWidth + columnSpacing) + slotWidth / 2 + } +} + +private struct PhotonMechanicalGateOverlayView: View { + let modeCount: Int + let slotCount: Int + let laneLabelWidth: CGFloat + let slotWidth: CGFloat + let slotHeight: CGFloat + let topInset: CGFloat + let rowSpacing: CGFloat + let columnSpacing: CGFloat + let leadingInset: CGFloat + let nodes: [CircuitNode] + let zoom: CGFloat + + var body: some View { + let markerScale = max(0.75, min(1.35, 1.0 / max(zoom, 0.01))) + + ZStack(alignment: .topLeading) { + Canvas { context, _ in + guard modeCount > 0, slotCount > 0 else { return } + + let groupedBySlot = Dictionary(grouping: nodes) { $0.slot } + let mechanicLineWidth = max(0.8, 1.4 / max(zoom, 0.01)) + + for mode in 0.. 1 else { continue } + let x = slotCenterX(slot) + let sortedModes = slotNodes.map(\.mode).sorted() + let y0 = laneCenterY(sortedModes.first ?? 0) + let y1 = laneCenterY(sortedModes.last ?? 0) + + var couplingPath = Path() + couplingPath.move(to: CGPoint(x: x, y: y0)) + couplingPath.addLine(to: CGPoint(x: x, y: y1)) + context.stroke( + couplingPath, + with: .color(Color.cyan.opacity(0.35)), + style: StrokeStyle(lineWidth: mechanicLineWidth * 1.1, lineCap: .round) + ) + + let jointRadius = 3.2 * markerScale + for mode in Set(slotNodes.map(\.mode)) { + let y = laneCenterY(mode) + let rect = CGRect( + x: x - jointRadius, + y: y - jointRadius, + width: jointRadius * 2, + height: jointRadius * 2 + ) + context.fill(Path(ellipseIn: rect), with: .color(Color.cyan.opacity(0.88))) + } + } + } + + ForEach(visibleNodes) { node in + photonNodeIcon(for: node, markerScale: markerScale) + .position(x: slotCenterX(node.slot), y: laneCenterY(node.mode)) + } + } + .allowsHitTesting(false) + } + + private var visibleNodes: [CircuitNode] { + nodes.filter { node in + node.mode >= 0 && node.mode < modeCount && node.slot >= 0 && node.slot < slotCount + } + } + + private func photonNodeIcon(for node: CircuitNode, markerScale: CGFloat) -> some View { + let tint = color(for: node.kind) + let badgeDiameter = max(14.0, 18.0 * markerScale) + let iconSize = max(9.0, 11.5 * markerScale) + let symbolName = node.kind.systemIcon.isEmpty ? "questionmark.circle" : node.kind.systemIcon + + return ZStack { + Circle() + .fill(Color.black.opacity(0.58)) + + Circle() + .stroke(tint.opacity(0.85), lineWidth: max(1.0, 1.3 * markerScale)) + + Image(systemName: symbolName) + .font(.system(size: iconSize, weight: .semibold)) + .symbolRenderingMode(.monochrome) + .foregroundStyle(.white) + } + .frame(width: badgeDiameter, height: badgeDiameter) + .shadow(color: tint.opacity(0.26), radius: max(1.0, 2.0 * markerScale)) + .help(node.kind.hoverDisplayName) + } + + private func color(for kind: GateKind) -> Color { + switch kind.primaryFilter { + case .gaussian: + return Color.cyan.opacity(0.94) + case .nonGaussian: + return Color.green.opacity(0.90) + case .channels: + return Color.orange.opacity(0.92) + case .measurement: + return Color.red.opacity(0.92) + case .control: + return Color.gray.opacity(0.90) + case .all: + return Color.cyan.opacity(0.95) + } + } + + private var leftSlotX: CGFloat { + leadingInset + laneLabelWidth + columnSpacing + slotWidth / 2 + } + + private var rightSlotX: CGFloat { + leftSlotX + CGFloat(max(slotCount - 1, 0)) * (slotWidth + columnSpacing) + } + + private func laneCenterY(_ mode: Int) -> CGFloat { + topInset + CGFloat(mode) * (slotHeight + rowSpacing) + slotHeight / 2 + } + + private func slotCenterX(_ slot: Int) -> CGFloat { + leftSlotX + CGFloat(slot) * (slotWidth + columnSpacing) + } +} + +private struct InfiniteMetricGridBackground: View { + let theme: AppTheme + let zoom: CGFloat + let panOffset: CGSize + private var palette: ThemePalette { .resolve(theme) } + + var body: some View { + GeometryReader { _ in + Canvas { context, size in + let minorStep = max(8, 24 * zoom) + let majorStep = minorStep * 5 + let startMinorX = positiveModulo(panOffset.width, by: minorStep) + let startMinorY = positiveModulo(panOffset.height, by: minorStep) + let startMajorX = positiveModulo(panOffset.width, by: majorStep) + let startMajorY = positiveModulo(panOffset.height, by: majorStep) + + var x = startMinorX + while x <= size.width + minorStep { + let px = pixelAligned(x) + var path = Path() + path.move(to: CGPoint(x: px, y: 0)) + path.addLine(to: CGPoint(x: px, y: size.height)) + context.stroke(path, with: .color(palette.gridMinor), lineWidth: 0.8) + x += minorStep + } + + var y = startMinorY + while y <= size.height + minorStep { + let py = pixelAligned(y) + var path = Path() + path.move(to: CGPoint(x: 0, y: py)) + path.addLine(to: CGPoint(x: size.width, y: py)) + context.stroke(path, with: .color(palette.gridMinor), lineWidth: 0.8) + y += minorStep + } + + x = startMajorX + while x <= size.width + majorStep { + let px = pixelAligned(x) + var path = Path() + path.move(to: CGPoint(x: px, y: 0)) + path.addLine(to: CGPoint(x: px, y: size.height)) + context.stroke(path, with: .color(palette.gridMajor), lineWidth: 1.0) + x += majorStep + } + + y = startMajorY + while y <= size.height + majorStep { + let py = pixelAligned(y) + var path = Path() + path.move(to: CGPoint(x: 0, y: py)) + path.addLine(to: CGPoint(x: size.width, y: py)) + context.stroke(path, with: .color(palette.gridMajor), lineWidth: 1.0) + y += majorStep + } + } + } + } + + private func positiveModulo(_ value: CGFloat, by modulus: CGFloat) -> CGFloat { + guard modulus > 0 else { return 0 } + let result = value.truncatingRemainder(dividingBy: modulus) + return result >= 0 ? result : (result + modulus) + } + + private func pixelAligned(_ value: CGFloat) -> CGFloat { + floor(value) + 0.5 + } +} + +private struct MinimapView: View { + let modeCount: Int + let slotCount: Int + let nodes: [CircuitNode] + let theme: AppTheme + private var palette: ThemePalette { .resolve(theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text("Minimap") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + + Canvas { context, size in + let safeModes = max(modeCount, 1) + let safeSlots = max(slotCount, 1) + let cellWidth = size.width / CGFloat(safeSlots) + let cellHeight = size.height / CGFloat(safeModes) + + let bg = Path(CGRect(origin: .zero, size: size)) + context.fill(bg, with: .color(palette.subpanel)) + + for mode in 0...safeModes { + let y = CGFloat(mode) * cellHeight + var path = Path() + path.move(to: CGPoint(x: 0, y: y)) + path.addLine(to: CGPoint(x: size.width, y: y)) + context.stroke(path, with: .color(palette.gridMinor), lineWidth: 0.8) + } + + for slot in 0...safeSlots { + let x = CGFloat(slot) * cellWidth + var path = Path() + path.move(to: CGPoint(x: x, y: 0)) + path.addLine(to: CGPoint(x: x, y: size.height)) + context.stroke(path, with: .color(palette.gridMinor), lineWidth: 0.8) + } + + for node in nodes { + guard node.mode >= 0, node.mode < safeModes, node.slot >= 0, node.slot < safeSlots else { continue } + let rect = CGRect( + x: CGFloat(node.slot) * cellWidth + 1, + y: CGFloat(node.mode) * cellHeight + 1, + width: max(2, cellWidth - 2), + height: max(2, cellHeight - 2) + ) + context.fill( + Path(roundedRect: rect, cornerRadius: 2), + with: .color(Color.accentColor.opacity(0.75)) + ) + } + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + } + } +} + +private struct SlotTimelineReferenceView: View { + let slotCount: Int + let laneLabelWidth: CGFloat + let slotWidth: CGFloat + let columnSpacing: CGFloat + let leadingInset: CGFloat + let topInset: CGFloat + + var body: some View { + let safeSlotCount = max(slotCount, 1) + + Canvas { context, _ in + let axisY = topInset + 18 + let firstX = leadingInset + laneLabelWidth + columnSpacing + let stepX = slotWidth + columnSpacing + let lastX = firstX + + CGFloat(safeSlotCount) * slotWidth + + CGFloat(max(safeSlotCount - 1, 0)) * columnSpacing + + var axisPath = Path() + axisPath.move(to: CGPoint(x: firstX, y: axisY)) + axisPath.addLine(to: CGPoint(x: lastX, y: axisY)) + context.stroke(axisPath, with: .color(Color.secondary.opacity(0.70)), lineWidth: 1) + + for slot in 0.. Color { + switch severity { + case .info: + return .blue + case .warning: + return .orange + case .error: + return .red + } + } +} + +private struct CircuitConfigInspectorView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + private var seedBinding: Binding { + Binding( + get: { store.seed }, + set: { newValue in + store.seed = newValue.filter(\.isNumber) + } + ) + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 6) { + Text("Scientific Circuit Presets") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Picker("Scientific Circuit Presets", selection: $store.selectedScientificCircuitPresetID) { + ForEach(store.scientificCircuitPresets) { preset in + Text(preset.title).tag(preset.id) + } + } + .pickerStyle(.menu) + .labelsHidden() + + HStack(spacing: 8) { + Button("Load Preset") { + store.loadSelectedScientificCircuitPreset() + } + .buttonStyle(.borderedProminent) + + if let preset = store.selectedScientificCircuitPreset { + Text("Backend: \(preset.recommendedBackend.title) Seed: \(preset.suggestedSeed)") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + + if let preset = store.selectedScientificCircuitPreset { + Text(preset.citation) + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + Text(preset.mappingNote) + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: 10) { + if let doiURL = URL(string: preset.doiURL) { + Link("DOI", destination: doiURL) + .font(.caption) + } + if let sourceURL = URL(string: preset.sourceURL) { + Link("Source", destination: sourceURL) + .font(.caption) + } + } + } + } + + LabeledRuntimeEnginePicker(title: "Runtime Engine", selection: $store.runtimeEngine) + if let runtimeEngineUnavailableReason = store.runtimeEngineUnavailableReason { + Text(runtimeEngineUnavailableReason) + .font(.caption2) + .foregroundStyle(.secondary) + } + + LabeledPicker(title: "Backend Selector", selection: $store.backend) + VStack(alignment: .leading, spacing: 4) { + Text("Seed Control") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + HStack(spacing: 8) { + TextField("Seed Control", text: seedBinding) + .textFieldStyle(.roundedBorder) + .font(.caption.monospaced()) + Button("Random") { + store.seed = String(UInt64.random(in: 1...UInt64.max)) + } + .buttonStyle(.bordered) + } + } + + VStack(alignment: .leading, spacing: 4) { + Text("Cutoff Control") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Stepper(value: $store.cutoff, in: 1...128) { + Text("\(store.cutoff)") + } + } + + VStack(alignment: .leading, spacing: 4) { + Text("Slot Count") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Stepper(value: $store.slotCount, in: 1...64) { + Text("\(store.slotCount)") + } + } + + LabeledField(title: "Schema Version", text: $store.schemaVersion) + + VStack(alignment: .leading, spacing: 4) { + Text("Foundry Profile") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Picker("Foundry Profile Selector", selection: $store.foundryProfile) { + ForEach(store.foundryProfiles, id: \.self) { profile in + Text(profile).tag(profile) + } + } + .pickerStyle(.menu) + .labelsHidden() + } + + VStack(alignment: .leading, spacing: 4) { + Text("KPI Policy") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Picker("KPI Policy Selector", selection: $store.selectedKPIPolicyID) { + ForEach(store.kpiPolicies) { policy in + Text("\(policy.name) (v\(policy.version))").tag(policy.id) + } + } + .pickerStyle(.menu) + .labelsHidden() + + if let policy = store.selectedKPIPolicy { + Text(policy.summary) + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + Text("Target: \(policy.targetRepresentation)") + .font(.caption2.monospaced()) + .foregroundStyle(.secondary) + Text( + "Robustness thresholds success>=\(String(format: "%.3f", policy.thresholds.minSuccessRate)) p95_d_mean<=\(String(format: "%.3e", policy.thresholds.maxMeanPhotonP95Delta)) p95_d_state<=\(String(format: "%.3e", policy.thresholds.maxStateP95Delta)) mismatch<=\(String(format: "%.3f", policy.thresholds.maxMeasurementMismatchRate))" + ) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + if policy.thresholds.maxLogicalErrorRate != nil + || policy.thresholds.minSuppressionFactor != nil + || policy.thresholds.minBreakEvenGain != nil { + Text( + "QEC thresholds ler<=\(policy.thresholds.maxLogicalErrorRate.map { String(format: "%.4f", $0) } ?? "n/a") suppression>=\(policy.thresholds.minSuppressionFactor.map { String(format: "%.3f", $0) } ?? "n/a") break_even>=\(policy.thresholds.minBreakEvenGain.map { String(format: "%.4f", $0) } ?? "n/a")" + ) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + } + + LabeledField(title: "Circuit JSON Path", text: $store.circuitJSONPath) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct StatusInspectorView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + private var syncStateText: String { store.isDirty ? "local changes" : "synced" } + private var cleanlinessText: String { store.isDirty ? "dirty" : "clean" } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Status") + .font(.headline) + + VStack(alignment: .leading, spacing: 4) { + KeyValueRow(title: "State", value: cleanlinessText) + KeyValueRow(title: "Autosave", value: "on") + KeyValueRow(title: "Sync", value: syncStateText) + KeyValueRow(title: "Workspace", value: store.workspace) + KeyValueRow(title: "Project", value: store.project) + KeyValueRow(title: "Environment", value: store.environment.rawValue.uppercased()) + KeyValueRow(title: "Role", value: store.userRole.rawValue) + } + .padding(.horizontal, 10) + .padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + + if let savedPath = store.lastSavedDocumentPath, !savedPath.isEmpty { + KeyValueRow(title: "Last Save", value: savedPath) + } + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct QuantumStateInspectorView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + private var state: QuantumStateSnapshot? { + store.lastRunResponse?.finalState ?? store.lastTraceResponse?.finalState + } + + private var backendLabel: String { + state?.backend ?? store.lastRunResponse?.backend ?? store.lastTraceResponse?.backend ?? "n/a" + } + + private var representationLabel: String { + state?.representation ?? "n/a" + } + + private var measurementCountLabel: String { + if let measurementCount = store.lastRunResponse?.measurementCount { + return "\(measurementCount)" + } + if let measurementCount = store.lastTraceResponse?.frames?.last?.measurementCount { + return "\(measurementCount)" + } + return "n/a" + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Quantum State") + .font(.headline) + + if let state { + VStack(alignment: .leading, spacing: 4) { + KeyValueRow(title: "Backend", value: backendLabel) + KeyValueRow(title: "Representation", value: representationLabel) + KeyValueRow(title: "Modes", value: state.modes.map(String.init) ?? "n/a") + KeyValueRow(title: "Cutoff", value: state.cutoff.map(String.init) ?? "n/a") + KeyValueRow(title: "Measurements", value: measurementCountLabel) + } + .padding(.horizontal, 10) + .padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + + if let mean = state.mean, !mean.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Mean (q,p)") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Text(formattedMeanByMode(mean)) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + } + } + + if let covariance = state.covariance, !covariance.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Covariance") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + ScrollView(.horizontal, showsIndicators: true) { + Text(formattedMatrix(covariance)) + .font(.system(.caption2, design: .monospaced)) + .textSelection(.enabled) + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + } + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + } + } + + if let topProbabilities = state.topProbabilities, !topProbabilities.isEmpty { + VStack(alignment: .leading, spacing: 4) { + Text("Top Fock Components") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + ForEach(topProbabilities) { component in + KeyValueRow( + title: "n=\(component.n)", + value: String(format: "%.6f", component.probability) + ) + } + if let probabilities = state.probabilities, !probabilities.isEmpty { + KeyValueRow( + title: "Total Probability", + value: String(format: "%.6f", probabilities.reduce(0.0, +)) + ) + } + } + .padding(.horizontal, 10) + .padding(.vertical, 7) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + } + } else { + Text("No state snapshot yet. Run the circuit to capture quantum state.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func formattedMeanByMode(_ mean: [Double]) -> String { + let modeCount = mean.count / 2 + guard modeCount > 0 else { return "n/a" } + + var lines: [String] = [] + lines.reserveCapacity(modeCount) + for mode in 0.. String { + matrix + .map { row in row.map(format).joined(separator: " ") } + .joined(separator: "\n") + } + + private func format(_ value: Double) -> String { + String(format: "% .5f", value) + } +} + +private enum ProvenanceDetailSheet: String, Identifiable { + case stateHealth + case validity + case bosonicSignoff + case parity + case kpi + + var id: String { rawValue } +} + +private struct ProvenanceInspectorView: View { + @EnvironmentObject private var store: StudioStore + @State private var activeSheet: ProvenanceDetailSheet? + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Provenance Inspector") + .font(.headline) + + KeyValueRow( + title: "Foundry Hash", + value: store.lastRunResponse?.provenance?.foundryProfileHash ?? "n/a" + ) + KeyValueRow( + title: "Git SHA", + value: store.lastRunResponse?.provenance?.gitSHA ?? "n/a" + ) + KeyValueRow( + title: "Backend Version", + value: store.lastRunResponse?.provenance?.backendVersion ?? "n/a" + ) + if let health = store.stateHealthSummary { + HStack(spacing: 8) { + Text("State Health") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Button { + activeSheet = .stateHealth + } label: { + Text(health.status.title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(color(for: health.status)) + ) + } + .buttonStyle(.plain) + } + HStack { + Text("Physical Validity") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Button(health.validity.title) { + activeSheet = .validity + } + .buttonStyle(.plain) + .font(.caption.monospaced()) + } + KeyValueRow(title: "Numerical Confidence", value: health.confidence.title) + KeyValueRow(title: "Noise Robustness", value: robustnessLabel(health.robustness)) + HStack { + Text("KPI Policy") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Button(health.kpi.status.title) { + activeSheet = .kpi + } + .buttonStyle(.plain) + .font(.caption.monospaced()) + } + Text(health.detail) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + if store.isBosonicSignoffRequired { + HStack(spacing: 8) { + Text("Bosonic Signoff") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Button { + activeSheet = .bosonicSignoff + } label: { + Text(store.bosonicSignoffSummary?.status.title ?? "PENDING") + .font(.caption2.weight(.semibold)) + .foregroundStyle(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(color(for: store.bosonicSignoffSummary?.status ?? .unavailable)) + ) + } + .buttonStyle(.plain) + } + if let signoff = store.bosonicSignoffSummary { + KeyValueRow( + title: "Signoff Checks", + value: "\(signoff.passedChecks)/\(signoff.requiredChecks)" + ) + Text(signoff.detail) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + if let parity = store.runtimeParitySummary { + HStack(spacing: 8) { + Text("Runtime Parity") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Button { + activeSheet = .parity + } label: { + Text(parity.status.title) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background( + Capsule() + .fill(color(for: parity.status)) + ) + } + .buttonStyle(.plain) + } + KeyValueRow( + title: "Compared", + value: "\(parity.selectedRuntime.rawValue) (\(parity.selectedBackendVersion)) vs \(parity.referenceRuntime.rawValue) (\(parity.referenceBackendVersion))" + ) + Text(parity.detail) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + KeyValueRow( + title: "Signature Status", + value: store.lastRunResponse?.provenance?.foundryProfileHash == nil ? "Pending" : "Verified" + ) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + .sheet(item: $activeSheet) { sheet in + ProvenanceDetailPopupView(sheet: sheet) + .environmentObject(store) + .frame(minWidth: 620, minHeight: 420) + } + } + + private func robustnessLabel(_ robustness: StateRobustnessSummary) -> String { + guard let score = robustness.score else { return robustness.status.title } + return "\(robustness.status.title) (\(String(format: "%.1f", score)))" + } + + private func color(for status: RuntimeParityStatus) -> Color { + switch status { + case .match: + return .green + case .drift: + return .orange + case .unmatched: + return .indigo + } + } + + private func color(for status: StateHealthStatus) -> Color { + switch status { + case .healthy: + return .green + case .review: + return .orange + case .invalid: + return .purple + case .unavailable: + return .gray + } + } + + private func color(for status: BosonicSignoffStatus) -> Color { + switch status { + case .pass: + return .green + case .incomplete: + return .orange + case .unavailable: + return .gray + } + } +} + +private struct ProvenanceDetailPopupView: View { + @EnvironmentObject private var store: StudioStore + @Environment(\.dismiss) private var dismiss + let sheet: ProvenanceDetailSheet + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text(title) + .font(.title3.weight(.semibold)) + Spacer() + Button("Close") { dismiss() } + .buttonStyle(.bordered) + } + + ScrollView { + VStack(alignment: .leading, spacing: 8) { + switch sheet { + case .stateHealth: + stateHealthContent + case .validity: + validityContent + case .bosonicSignoff: + bosonicSignoffContent + case .parity: + parityContent + case .kpi: + kpiContent + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(12) + .background(palette.panel) + } + + private var title: String { + switch sheet { + case .stateHealth: + return "State Health Details" + case .validity: + return "Physical Validity Details" + case .bosonicSignoff: + return "Bosonic Signoff Details" + case .parity: + return "Runtime Parity Details" + case .kpi: + return "KPI Policy Details" + } + } + + @ViewBuilder + private var stateHealthContent: some View { + if let health = store.stateHealthSummary { + KeyValueRow(title: "State Health", value: health.status.title) + KeyValueRow(title: "Physical Validity", value: health.validity.title) + KeyValueRow(title: "Numerical Confidence", value: health.confidence.title) + KeyValueRow(title: "Noise Robustness", value: health.robustness.status.title) + KeyValueRow(title: "KPI Policy", value: "\(health.kpi.policyName) v\(health.kpi.policyVersion)") + KeyValueRow(title: "KPI Status", value: health.kpi.status.title) + if let score = health.robustness.score { + KeyValueRow(title: "Robustness Pass Rate", value: String(format: "%.1f%%", score)) + } + KeyValueRow(title: "Checks", value: "\(health.checksCompleted)") + KeyValueRow(title: "Issues", value: "\(health.issueCount)") + Text("Recommendation: \(health.recommendation)") + .font(.caption) + if let rationale = health.successRationale { + Text("Why successful: \(rationale)") + .font(.caption) + .foregroundStyle(.secondary) + } + Text("Robustness: \(health.robustness.detail)") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + ForEach(health.robustness.checks) { check in + PolicyCheckRow(check: check) + } + Text("Summary: \(health.detail)") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } else { + Text("No state health data yet. Run the circuit to generate diagnostics.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var validityContent: some View { + if let health = store.stateHealthSummary { + KeyValueRow(title: "Physical Validity", value: health.validity.title) + KeyValueRow(title: "Checks", value: "\(health.checksCompleted)") + KeyValueRow(title: "Errors", value: "\(health.errors.count)") + KeyValueRow(title: "Warnings", value: "\(health.warnings.count)") + + if !health.errors.isEmpty { + Text("Errors") + .font(.caption.weight(.semibold)) + ForEach(Array(health.errors.prefix(12).enumerated()), id: \.offset) { index, issue in + Text("\(index + 1). \(issue)") + .font(.caption) + } + } + if !health.warnings.isEmpty { + Text("Warnings") + .font(.caption.weight(.semibold)) + ForEach(Array(health.warnings.prefix(12).enumerated()), id: \.offset) { index, issue in + Text("\(index + 1). \(issue)") + .font(.caption) + } + } + if health.errors.isEmpty, health.warnings.isEmpty { + Text("All physical checks passed for this run.") + .font(.caption) + .foregroundStyle(.secondary) + } + } else { + Text("No validity data yet. Run the circuit to evaluate physical checks.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var bosonicSignoffContent: some View { + if let signoff = store.bosonicSignoffSummary { + KeyValueRow(title: "Status", value: signoff.status.title) + KeyValueRow(title: "Completed Checks", value: "\(signoff.passedChecks)/\(signoff.requiredChecks)") + Text("Recommendation: \(signoff.recommendation)") + .font(.caption) + Text("Summary: \(signoff.detail)") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + + ForEach(signoff.checks) { check in + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 8) { + Image(systemName: check.passed ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") + .foregroundStyle(check.passed ? Color.green : Color.orange) + Text(check.title) + .font(.caption.weight(.semibold)) + Spacer() + Text(check.observed) + .font(.caption.monospaced()) + Text("->") + .font(.caption2) + .foregroundStyle(.secondary) + Text(check.expected) + .font(.caption.monospaced()) + } + Text(check.detail) + .font(.caption2) + .foregroundStyle(.secondary) + } + .padding(.vertical, 2) + } + } else { + Text("No bosonic signoff data yet. Run the circuit to evaluate objective signoff checks.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var parityContent: some View { + if let parity = store.runtimeParitySummary { + KeyValueRow(title: "Status", value: parity.status.title) + KeyValueRow( + title: "Compared", + value: "\(parity.selectedRuntime.rawValue) (\(parity.selectedBackendVersion)) vs \(parity.referenceRuntime.rawValue) (\(parity.referenceBackendVersion))" + ) + if let meanDelta = parity.meanPhotonDelta { + KeyValueRow(title: "Delta Mean Photon", value: String(format: "%.8e", meanDelta)) + } + if let stateDelta = parity.stateDeltaMax { + KeyValueRow(title: "Delta State Max", value: String(format: "%.8e", stateDelta)) + } + if let measurementDelta = parity.measurementDelta { + KeyValueRow(title: "Delta Measurements", value: "\(measurementDelta)") + } + Text("Detail: \(parity.detail)") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + Text("Recommendation: \(parityRecommendation(parity.status))") + .font(.caption) + } else { + Text("No runtime parity data yet. Run the circuit to compare runtimes.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private var kpiContent: some View { + if let health = store.stateHealthSummary { + KeyValueRow(title: "Policy", value: "\(health.kpi.policyName) (\(health.kpi.policyID))") + KeyValueRow(title: "Version", value: "v\(health.kpi.policyVersion)") + KeyValueRow(title: "Status", value: health.kpi.status.title) + Text("Recommendation: \(health.kpi.recommendation)") + .font(.caption) + Text("Summary: \(health.kpi.detail)") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + ForEach(health.kpi.checks) { check in + PolicyCheckRow(check: check) + } + if health.kpi.checks.isEmpty { + Text("No KPI checks were produced for this run/policy pair.") + .font(.caption) + .foregroundStyle(.secondary) + } + } else { + Text("No KPI data yet. Run the circuit to evaluate policy checks.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + private func parityRecommendation(_ status: RuntimeParityStatus) -> String { + switch status { + case .match: + return "Both runtimes agree within strict tolerance. This run is numerically consistent." + case .drift: + return "Small drift detected. Validate cutoff/seed configuration and track trends across multiple runs." + case .unmatched: + return "Runtimes diverged beyond tolerance. Review backend support, state payload compatibility, and gate-level constraints." + } + } +} + +private struct PolicyCheckRow: View { + let check: ThresholdCheckResult + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 8) { + Text(check.title) + .font(.caption) + Spacer() + Text(check.passed ? "PASS" : "FAIL") + .font(.caption2.weight(.semibold)) + .foregroundStyle(check.passed ? .green : .orange) + } + HStack(spacing: 6) { + Text("value: \(metricText(check.value))") + Text("\(check.comparator)") + Text("threshold: \(metricText(check.threshold))") + } + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + if let note = check.note, !note.isEmpty { + Text(note) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 2) + } + + private func metricText(_ value: Double?) -> String { + guard let value, value.isFinite else { return "n/a" } + return String(format: "%.6e", value) + } +} + +private struct BottomPanelView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Picker("BottomTabs", selection: $store.bottomTab) { + ForEach(BottomTab.allCases) { tab in + Text(tab.title).tag(tab) + } + } + .pickerStyle(.segmented) + .labelsHidden() + .frame(maxWidth: 720) + + ZStack { + switch store.bottomTab { + case .validationConsole: + ValidationConsoleView() + case .runOutputPanel: + RunOutputPanelView() + case .auditTrailPanel: + AuditTrailPanelView() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + .padding(12) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(palette.panel) + } +} + +private struct ValidationConsoleView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 6) { + ForEach(store.validationIssues) { issue in + HStack(spacing: 10) { + Text(issue.severity.rawValue) + .font(.caption.weight(.semibold)) + .foregroundStyle(color(for: issue.severity)) + .frame(width: 64, alignment: .leading) + Text(issue.code) + .font(.caption.monospaced()) + .frame(width: 170, alignment: .leading) + Text(issue.message) + .frame(maxWidth: .infinity, alignment: .leading) + Text(issue.location) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(.vertical, 4) + Divider() + } + } + .padding(8) + } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func color(for severity: ValidationSeverity) -> Color { + switch severity { + case .info: + return .blue + case .warning: + return .orange + case .error: + return .red + } + } +} + +private struct RunOutputPanelView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 4) { + Text("Status: \(store.statusText)") + .font(.subheadline.weight(.semibold)) + if let export = store.lastExportPath { + Text("Last export: \(export)") + .font(.caption) + .foregroundStyle(.secondary) + } + Divider() + ForEach(Array(store.runOutput.enumerated()), id: \.offset) { _, line in + Text(line) + .font(.system(.caption, design: .monospaced)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(8) + } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct MetricsPanelView: View { + @EnvironmentObject private var store: StudioStore + + private var gaussianCount: Int { + store.nodes.filter { $0.kind.primaryFilter == .gaussian }.count + } + + private var nonGaussianCount: Int { + store.nodes.filter { $0.kind.primaryFilter == .nonGaussian }.count + } + + private var depth: Int { + (store.nodes.map(\.slot).max() ?? -1) + 1 + } + + var body: some View { + HStack(spacing: 10) { + MetricCard(title: "Gate Count", value: "\(store.nodes.count)", subtitle: "Gaussian: \(gaussianCount) Non-Gaussian: \(nonGaussianCount)") + MetricCard(title: "Depth", value: "\(depth)", subtitle: "\(store.slotCount) slots") + MetricCard(title: "Modes", value: "\(store.modeCount)", subtitle: "Active") + MetricCard(title: "Est. Runtime", value: String(format: "%.2f s", 0.25 + Double(store.nodes.count) * 0.08), subtitle: "\(store.backend.title) backend") + } + } +} + +private struct ConfidenceBandSeries { + let lower: [Double] + let mean: [Double] + let upper: [Double] +} + +private struct AnalysisPanelView: View { + @EnvironmentObject private var store: StudioStore + var includeRunOutput: Bool = false + private var palette: ThemePalette { .resolve(store.theme) } + + private var totalGateCount: Int { + store.nodes.count + } + + private var circuitDepth: Int { + (store.nodes.map(\.slot).max() ?? -1) + 1 + } + + private var frames: [TraceResponse.Frame] { + store.lastTraceResponse?.frames ?? [] + } + + private var photonSeries: [Double] { + frames.map(\.meanPhotonNumber) + } + + private var latencySeries: [Double] { + frames.map(\.frameLatencyMs) + } + + private var measurementSeries: [Double] { + frames.map { Double($0.measurementCount) } + } + + private var photonDeltaSeries: [Double] { + guard let first = photonSeries.first else { return [] } + var deltas: [Double] = [0.0] + deltas.reserveCapacity(photonSeries.count) + var previous = first + for value in photonSeries.dropFirst() { + deltas.append(value - previous) + previous = value + } + return deltas + } + + private var cumulativeLatencySeries: [Double] { + var running = 0.0 + return latencySeries.map { latency in + running += latency + return running + } + } + + private var qecReport: QECReport? { + store.lastRunResponse?.qec ?? store.lastTraceResponse?.qec + } + + private var qecRounds: [QECReportRound] { + qecReport?.rounds ?? [] + } + + private var qecSyndromeSeries: [Double] { + qecRounds.compactMap(\.syndromeValue) + } + + private var qecResidualSeries: [Double] { + qecRounds.compactMap(\.residual) + } + + private var isQECProject: Bool { + store.project.trimmingCharacters(in: .whitespacesAndNewlines).localizedCaseInsensitiveCompare("Quantum Error Correction") == .orderedSame + } + + private var monteCarloFrameMatrix: [[TraceResponse.Frame]] { + store.monteCarloTraceResponses.compactMap(\.frames).filter { !$0.isEmpty } + } + + private var monteCarloFinalPhotonSeries: [Double] { + store.monteCarloTraceResponses.compactMap { $0.frames?.last?.meanPhotonNumber } + } + + private var monteCarloTraceTotalSeries: [Double] { + store.monteCarloTraceResponses.compactMap(\.traceTotalMs) + } + + private var monteCarloPeakLatencySeries: [Double] { + store.monteCarloTraceResponses.compactMap { response in + response.frames?.map(\.frameLatencyMs).max() + } + } + + private var photonBand: ConfidenceBandSeries? { + store.showConfidenceBands ? confidenceBand { $0.meanPhotonNumber } : nil + } + + private var latencyBand: ConfidenceBandSeries? { + store.showConfidenceBands ? confidenceBand { $0.frameLatencyMs } : nil + } + + private var measurementBand: ConfidenceBandSeries? { + store.showConfidenceBands ? confidenceBand { Double($0.measurementCount) } : nil + } + + private var gateHistogram: [(String, Int)] { + let counts = Dictionary(grouping: frames.filter { $0.gateType != "initial" }, by: \.gateType) + .mapValues(\.count) + return counts + .sorted { lhs, rhs in + if lhs.value == rhs.value { + return lhs.key < rhs.key + } + return lhs.value > rhs.value + } + .prefix(8) + .map { ($0.key, $0.value) } + } + + private var monteCarloSummaryText: String? { + guard !store.monteCarloTraceResponses.isEmpty else { return nil } + let done = store.monteCarloCompletedSamples + let total = store.noiseModel.sampleCount + return "Monte Carlo samples: \(done)/\(total)" + } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + NoiseModelPanelView() + .environmentObject(store) + + if frames.isEmpty { + Text("Run a circuit to populate analysis graphs.") + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + qecAnalysisSection() + } else { + HStack(spacing: 10) { + MetricCard( + title: "Total Gates", + value: "\(totalGateCount)", + subtitle: "Circuit nodes" + ) + MetricCard( + title: "Depth", + value: "\(circuitDepth)", + subtitle: "Slot layers" + ) + MetricCard( + title: "Trace Frames", + value: "\(frames.count)", + subtitle: "Rendered" + ) + MetricCard( + title: "Mean Photon", + value: String(format: "%.4f", store.lastRunResponse?.meanPhotonNumber ?? photonSeries.last ?? 0.0), + subtitle: "Final estimate" + ) + MetricCard( + title: "Trace Time", + value: String(format: "%.2f ms", store.lastTraceResponse?.traceTotalMs ?? 0.0), + subtitle: "Total execution" + ) + } + + if let monteCarloSummaryText { + Text(monteCarloSummaryText) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + qecAnalysisSection() + + SparklineCard( + title: "Photon Number vs Frame", + values: photonSeries, + stroke: .blue, + confidenceBand: photonBand, + xAxisLabel: "Frame Index", + yAxisLabel: "Mean Photon Number" + ) + + SparklineCard( + title: "Photon Delta per Frame", + values: photonDeltaSeries, + stroke: .cyan, + confidenceBand: nil, + xAxisLabel: "Frame Index", + yAxisLabel: "Delta Mean Photon Number" + ) + + SparklineCard( + title: "Frame Latency (ms)", + values: latencySeries, + stroke: .orange, + confidenceBand: latencyBand, + xAxisLabel: "Frame Index", + yAxisLabel: "Latency (ms)" + ) + + SparklineCard( + title: "Cumulative Trace Time (ms)", + values: cumulativeLatencySeries, + stroke: .mint, + confidenceBand: nil, + xAxisLabel: "Frame Index", + yAxisLabel: "Cumulative Time (ms)" + ) + + SparklineCard( + title: "Measurement Count Progression", + values: measurementSeries, + stroke: .purple, + confidenceBand: measurementBand, + xAxisLabel: "Frame Index", + yAxisLabel: "Measurement Count" + ) + + if monteCarloFinalPhotonSeries.count > 1 { + SparklineCard( + title: "Monte Carlo Final Mean Photon", + values: monteCarloFinalPhotonSeries, + stroke: .green, + confidenceBand: nil, + xAxisLabel: "Sample Index", + yAxisLabel: "Final Mean Photon" + ) + } + + if monteCarloTraceTotalSeries.count > 1 { + SparklineCard( + title: "Monte Carlo Trace Time (ms)", + values: monteCarloTraceTotalSeries, + stroke: .red, + confidenceBand: nil, + xAxisLabel: "Sample Index", + yAxisLabel: "Trace Time (ms)" + ) + } + + if monteCarloPeakLatencySeries.count > 1 { + SparklineCard( + title: "Monte Carlo Peak Frame Latency", + values: monteCarloPeakLatencySeries, + stroke: .indigo, + confidenceBand: nil, + xAxisLabel: "Sample Index", + yAxisLabel: "Peak Latency (ms)" + ) + } + + GateHistogramCard(items: gateHistogram) + + if includeRunOutput { + RunOutputSummaryCard(lines: store.runOutput, statusText: store.statusText) + } + } + } + .padding(8) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func confidenceBand(metric: (TraceResponse.Frame) -> Double) -> ConfidenceBandSeries? { + let matrix = monteCarloFrameMatrix + guard !matrix.isEmpty else { return nil } + let frameCount = matrix.map { $0.count }.min() ?? 0 + guard frameCount > 0 else { return nil } + + var lower: [Double] = [] + var mean: [Double] = [] + var upper: [Double] = [] + lower.reserveCapacity(frameCount) + mean.reserveCapacity(frameCount) + upper.reserveCapacity(frameCount) + + for frameIndex in 0.. Double { + guard !sortedValues.isEmpty else { return 0.0 } + let clamped = min(max(fraction, 0.0), 1.0) + let position = clamped * Double(sortedValues.count - 1) + let lowerIndex = Int(floor(position)) + let upperIndex = Int(ceil(position)) + if lowerIndex == upperIndex { + return sortedValues[lowerIndex] + } + let weight = position - Double(lowerIndex) + return sortedValues[lowerIndex] * (1.0 - weight) + sortedValues[upperIndex] * weight + } + + private func metricText(_ value: Double?) -> String { + guard let value, value.isFinite else { return "n/a" } + return String(format: "%.6f", value) + } + + @ViewBuilder + private func qecAnalysisSection() -> some View { + if let qecReport { + HStack(spacing: 10) { + MetricCard( + title: "QEC Rounds", + value: "\(qecReport.roundsExecuted ?? qecRounds.count)", + subtitle: "Decoder \(qecReport.decoder ?? "n/a")" + ) + MetricCard( + title: "Logical Error Rate", + value: metricText(qecReport.logicalErrorRate), + subtitle: "fail \(qecReport.logicalFailCount ?? 0) / pass \(qecReport.logicalPassCount ?? 0)" + ) + MetricCard( + title: "Suppression Factor", + value: metricText(qecReport.suppressionFactor), + subtitle: "vs physical noise proxy" + ) + MetricCard( + title: "Break-even Gain", + value: metricText(qecReport.breakEvenGain), + subtitle: (qecReport.breakEvenPass == true) ? "pass" : "review" + ) + } + + QECSyndromeRoundCard(report: qecReport) + QECLatticeCheckerCard(report: qecReport) + + if qecSyndromeSeries.count > 1 { + SparklineCard( + title: "QEC Syndrome by Round", + values: qecSyndromeSeries, + stroke: .pink, + confidenceBand: nil, + xAxisLabel: "Round Index", + yAxisLabel: "Syndrome Value" + ) + } + + if qecResidualSeries.count > 1 { + SparklineCard( + title: "QEC Residual by Round", + values: qecResidualSeries, + stroke: .teal, + confidenceBand: nil, + xAxisLabel: "Round Index", + yAxisLabel: "Residual" + ) + } + } else if isQECProject { + VStack(alignment: .leading, spacing: 6) { + Text("QEC Analysis") + .font(.caption.weight(.semibold)) + Text("No QEC payload found for this run. Use a circuit with `gkp_decode_displace` on Gaussian/Hybrid backend, then run again.") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + } +} + +private struct NoiseModelPanelView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + private var sampleCountBinding: Binding { + Binding( + get: { store.noiseModel.sampleCount }, + set: { store.noiseModel.sampleCount = max(4, min($0, 128)) } + ) + } + + private var lossSigmaBinding: Binding { + Binding( + get: { store.noiseModel.lossEtaSigma }, + set: { store.noiseModel.lossEtaSigma = min(max($0, 0.0), 0.20) } + ) + } + + private var thermalSigmaBinding: Binding { + Binding( + get: { store.noiseModel.thermalNoiseSigma }, + set: { store.noiseModel.thermalNoiseSigma = min(max($0, 0.0), 1.0) } + ) + } + + private var phaseSigmaBinding: Binding { + Binding( + get: { store.noiseModel.phaseSigmaRad }, + set: { store.noiseModel.phaseSigmaRad = min(max($0, 0.0), 0.30) } + ) + } + + private var displacementSigmaBinding: Binding { + Binding( + get: { store.noiseModel.displacementSigma }, + set: { store.noiseModel.displacementSigma = min(max($0, 0.0), 0.30) } + ) + } + + private var detectorSummaryText: String { + guard let profile = store.calibratedNoiseProfile else { return "Detector: n/a" } + return "Detector homodyne sigma \(format(profile.detectorModel.homodyneSigma)) heterodyne sigma \(format(profile.detectorModel.heterodyneSigma))" + } + + private var driftSummaryText: String { + guard let profile = store.calibratedNoiseProfile else { return "Drift: n/a" } + return "Drift/sample deta \(format(profile.driftModel.lossEtaPerSample)) dn_th \(format(profile.driftModel.thermalPerSample)) dtheta \(format(profile.driftModel.phaseRadPerSample)) ddisp \(format(profile.driftModel.displacementPerSample))" + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Noise Model") + .font(.caption.weight(.semibold)) + Spacer() + Toggle("Calibrated", isOn: $store.useCalibratedNoiseProfile) + .toggleStyle(.switch) + .disabled(store.calibratedNoiseProfile == nil) + .font(.caption) + Toggle("Bands", isOn: $store.showConfidenceBands) + .toggleStyle(.switch) + .font(.caption) + if store.isMonteCarloRunning { + ProgressView() + .progressViewStyle(.circular) + .controlSize(.small) + } + } + + HStack(spacing: 8) { + Button { + store.importCalibratedNoiseProfile() + } label: { + Label("Import Tensor", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .disabled(store.isMonteCarloRunning) + + Button { + store.clearCalibratedNoiseProfile() + } label: { + Label("Clear Tensor", systemImage: "trash") + } + .buttonStyle(.bordered) + .disabled(store.calibratedNoiseProfile == nil || store.isMonteCarloRunning) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Profile: \(store.calibratedNoiseProfileDisplayName)") + .font(.caption.monospaced()) + Text(store.calibratedNoiseModeCoverageText) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + Text(detectorSummaryText) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + Text(driftSummaryText) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + + HStack(spacing: 10) { + Stepper(value: sampleCountBinding, in: 4...128) { + Text("Samples \(store.noiseModel.sampleCount)") + .font(.caption.monospacedDigit()) + } + .frame(maxWidth: 220, alignment: .leading) + + Button("Run Monte Carlo") { + Task { await store.runMonteCarloAnalysis() } + } + .buttonStyle(.borderedProminent) + .disabled(store.isMonteCarloRunning) + + Button("Reset") { + store.clearMonteCarloResults() + } + .buttonStyle(.bordered) + .disabled(store.isMonteCarloRunning) + } + + VStack(alignment: .leading, spacing: 6) { + NoiseSliderRow( + title: "Loss eta sigma", + value: lossSigmaBinding, + range: 0.0...0.20 + ) + NoiseSliderRow( + title: "Thermal noise sigma", + value: thermalSigmaBinding, + range: 0.0...1.0 + ) + NoiseSliderRow( + title: "Phase sigma (rad)", + value: phaseSigmaBinding, + range: 0.0...0.30 + ) + NoiseSliderRow( + title: "Displacement sigma", + value: displacementSigmaBinding, + range: 0.0...0.30 + ) + } + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func format(_ value: Double) -> String { + String(format: "%.5f", value) + } +} + +private struct NoiseSliderRow: View { + let title: String + @Binding var value: Double + let range: ClosedRange + + var body: some View { + HStack(spacing: 8) { + Text(title) + .font(.caption) + .frame(width: 170, alignment: .leading) + Slider(value: $value, in: range) + Text(String(format: "%.4f", value)) + .font(.caption.monospacedDigit()) + .frame(width: 62, alignment: .trailing) + } + } +} + +private struct AnalysisPopupWindowView: View { + @EnvironmentObject private var store: StudioStore + @Environment(\.dismiss) private var dismiss + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Run Analysis") + .font(.title3.weight(.semibold)) + Text("Merged metrics, traces, and run output") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Button("Close") { dismiss() } + .buttonStyle(.bordered) + } + + ScrollView { + AnalysisPanelView(includeRunOutput: true) + .environmentObject(store) + .padding(.trailing, 2) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(palette.panel) + } + .padding(12) + .background(palette.panel) + } +} + +private struct SparklineCard: View { + @EnvironmentObject private var store: StudioStore + let title: String + let values: [Double] + let stroke: Color + let confidenceBand: ConfidenceBandSeries? + let xAxisLabel: String + let yAxisLabel: String + private var palette: ThemePalette { .resolve(store.theme) } + private let chartHeight: CGFloat = 120 + private let yScaleWidth: CGFloat = 84 + + private var plotCount: Int { + guard let confidenceBand else { return values.count } + return min(values.count, confidenceBand.lower.count, confidenceBand.mean.count, confidenceBand.upper.count) + } + + private var plottedValues: [Double] { + Array(values.prefix(plotCount)) + } + + private var plottedBand: ConfidenceBandSeries? { + guard let confidenceBand else { return nil } + return ConfidenceBandSeries( + lower: Array(confidenceBand.lower.prefix(plotCount)), + mean: Array(confidenceBand.mean.prefix(plotCount)), + upper: Array(confidenceBand.upper.prefix(plotCount)) + ) + } + + private var stats: (min: Double, max: Double) { + guard let first = plottedValues.first else { return (0, 0) } + var minValue = first + var maxValue = first + for value in plottedValues.dropFirst() { + minValue = min(minValue, value) + maxValue = max(maxValue, value) + } + return (minValue, maxValue) + } + + private var chartMinMax: (min: Double, max: Double) { + var chartMin = stats.min + var chartMax = stats.max + if let plottedBand { + for value in plottedBand.lower { + chartMin = min(chartMin, value) + chartMax = max(chartMax, value) + } + for value in plottedBand.upper { + chartMin = min(chartMin, value) + chartMax = max(chartMax, value) + } + } + return (chartMin, chartMax) + } + + private var yScaleValues: [Double] { + if plottedValues.isEmpty { + return [1.0, 0.5, 0.0] + } + + if abs(chartMinMax.max - chartMinMax.min) < 1e-9 { + let delta = max(abs(chartMinMax.max) * 0.1, 0.1) + return [chartMinMax.max + delta, chartMinMax.max, chartMinMax.max - delta] + } + + let midpoint = (chartMinMax.max + chartMinMax.min) * 0.5 + return [chartMinMax.max, midpoint, chartMinMax.min] + } + + private var xScaleIndices: [Int] { + let last = max(plotCount - 1, 0) + return [0, last / 2, last] + } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(title) + .font(.caption.weight(.semibold)) + Spacer() + Text("min \(format(stats.min)) max \(format(stats.max))") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + + HStack(alignment: .top, spacing: 8) { + VStack(alignment: .trailing, spacing: 4) { + Text(yAxisLabel) + .font(.caption2) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .trailing) + + VStack(alignment: .trailing) { + Text(format(yScaleValues[0])) + Spacer(minLength: 0) + Text(format(yScaleValues[1])) + Spacer(minLength: 0) + Text(format(yScaleValues[2])) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) + } + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + .frame(width: yScaleWidth, height: chartHeight, alignment: .trailing) + + VStack(alignment: .leading, spacing: 4) { + Sparkline( + values: plottedValues, + stroke: stroke, + yScaleValues: yScaleValues, + xScaleIndices: xScaleIndices, + confidenceBand: plottedBand + ) + .frame(height: chartHeight) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + + HStack(spacing: 0) { + Text("\(xScaleIndices[0])") + Spacer(minLength: 0) + Text("\(xScaleIndices[1])") + Spacer(minLength: 0) + Text("\(xScaleIndices[2])") + } + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + .padding(.horizontal, 2) + + Text(xAxisLabel) + .font(.caption2) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func format(_ value: Double) -> String { + String(format: "%.4f", value) + } +} + +private struct Sparkline: View { + let values: [Double] + let stroke: Color + let yScaleValues: [Double] + let xScaleIndices: [Int] + let confidenceBand: ConfidenceBandSeries? + + var body: some View { + GeometryReader { _ in + Canvas { context, size in + guard !values.isEmpty else { return } + let bandLower = confidenceBand?.lower ?? [] + let bandUpper = confidenceBand?.upper ?? [] + let minValue = min(values.min() ?? 0, bandLower.min() ?? (values.min() ?? 0)) + let maxValue = max(values.max() ?? 0, bandUpper.max() ?? (values.max() ?? 0)) + let yRange = max(maxValue - minValue, 1e-9) + let xStep = values.count > 1 ? size.width / CGFloat(values.count - 1) : 0 + + func yPosition(for value: Double) -> CGFloat { + let normalized = (value - minValue) / yRange + return size.height - CGFloat(normalized) * size.height + } + + for yScale in yScaleValues { + let y = yPosition(for: yScale) + var gridPath = Path() + gridPath.move(to: CGPoint(x: 0, y: y)) + gridPath.addLine(to: CGPoint(x: size.width, y: y)) + context.stroke( + gridPath, + with: .color(.secondary.opacity(0.20)), + style: StrokeStyle(lineWidth: 1) + ) + } + + for xScale in xScaleIndices { + let x = CGFloat(xScale) * xStep + var gridPath = Path() + gridPath.move(to: CGPoint(x: x, y: 0)) + gridPath.addLine(to: CGPoint(x: x, y: size.height)) + context.stroke( + gridPath, + with: .color(.secondary.opacity(0.16)), + style: StrokeStyle(lineWidth: 1) + ) + } + + if let confidenceBand, + confidenceBand.lower.count == values.count, + confidenceBand.upper.count == values.count { + var bandPath = Path() + bandPath.move(to: CGPoint(x: 0, y: yPosition(for: confidenceBand.upper[0]))) + for idx in confidenceBand.upper.indices.dropFirst() { + bandPath.addLine(to: CGPoint(x: CGFloat(idx) * xStep, y: yPosition(for: confidenceBand.upper[idx]))) + } + for idx in confidenceBand.lower.indices.reversed() { + bandPath.addLine(to: CGPoint(x: CGFloat(idx) * xStep, y: yPosition(for: confidenceBand.lower[idx]))) + } + bandPath.closeSubpath() + + context.fill( + bandPath, + with: .color(stroke.opacity(0.16)) + ) + + if confidenceBand.mean.count == values.count { + var meanPath = Path() + meanPath.move(to: CGPoint(x: 0, y: yPosition(for: confidenceBand.mean[0]))) + for idx in confidenceBand.mean.indices.dropFirst() { + meanPath.addLine(to: CGPoint(x: CGFloat(idx) * xStep, y: yPosition(for: confidenceBand.mean[idx]))) + } + context.stroke( + meanPath, + with: .color(stroke.opacity(0.55)), + style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round, dash: [4, 3]) + ) + } + } + + var path = Path() + path.move(to: CGPoint(x: 0, y: yPosition(for: values[0]))) + for idx in values.indices.dropFirst() { + path.addLine(to: CGPoint(x: CGFloat(idx) * xStep, y: yPosition(for: values[idx]))) + } + + context.stroke( + path, + with: .color(stroke), + style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round) + ) + + if let last = values.last { + let center = CGPoint(x: size.width, y: yPosition(for: last)) + let markerRect = CGRect(x: center.x - 3, y: center.y - 3, width: 6, height: 6) + context.fill(Path(ellipseIn: markerRect), with: .color(stroke)) + } + } + } + } +} + +private struct QECSyndromeRoundCard: View { + @EnvironmentObject private var store: StudioStore + let report: QECReport + private var palette: ThemePalette { .resolve(store.theme) } + + private var rounds: [QECReportRound] { + report.rounds ?? [] + } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("QEC Syndrome Log") + .font(.caption.weight(.semibold)) + Text("X-axis: Round Index Y-axis: Syndrome Value") + .font(.caption2) + .foregroundStyle(.secondary) + + if rounds.isEmpty { + Text("No syndrome rounds available in this run.") + .font(.caption) + .foregroundStyle(.secondary) + } else { + HStack(spacing: 8) { + Text("Round") + .frame(width: 48, alignment: .leading) + Text("Syndrome") + .frame(width: 108, alignment: .trailing) + Text("Residual") + .frame(width: 108, alignment: .trailing) + Text("Lattice") + .frame(width: 72, alignment: .trailing) + Text("LER") + .frame(width: 70, alignment: .trailing) + Text("Status") + .frame(width: 56, alignment: .trailing) + } + .font(.caption2.weight(.semibold)) + .foregroundStyle(.secondary) + + ForEach(Array(rounds.enumerated()), id: \.offset) { index, round in + HStack(spacing: 8) { + Text("r\(round.round ?? (index + 1))") + .frame(width: 48, alignment: .leading) + Text(metricText(round.syndromeValue)) + .frame(width: 108, alignment: .trailing) + Text(metricText(round.residual)) + .frame(width: 108, alignment: .trailing) + Text("\(round.nearestLatticeIndex ?? 0)") + .frame(width: 72, alignment: .trailing) + Text(metricText(cumulativeLER(at: index))) + .frame(width: 70, alignment: .trailing) + Text((round.logicalPass == true) ? "PASS" : "FAIL") + .frame(width: 56, alignment: .trailing) + .foregroundStyle((round.logicalPass == true) ? Color.green : Color.orange) + } + .font(.caption2.monospacedDigit()) + } + } + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func cumulativeLER(at index: Int) -> Double? { + guard index >= 0, index < rounds.count else { return nil } + var failCount = 0 + var observed = 0 + for offset in 0...index { + guard let logicalPass = rounds[offset].logicalPass else { continue } + observed += 1 + if !logicalPass { + failCount += 1 + } + } + guard observed > 0 else { return nil } + return Double(failCount) / Double(observed) + } + + private func metricText(_ value: Double?) -> String { + guard let value, value.isFinite else { return "n/a" } + return String(format: "%.5f", value) + } +} + +private struct QECLatticeCheckerCard: View { + @EnvironmentObject private var store: StudioStore + let report: QECReport + private var palette: ThemePalette { .resolve(store.theme) } + + private struct Point: Identifiable { + let id: Int + let round: Int + let syndrome: Double + let target: Double + let spacing: Double + let pass: Bool + } + + private var points: [Point] { + let rounds = report.rounds ?? [] + var extracted: [Point] = [] + extracted.reserveCapacity(rounds.count) + for (index, round) in rounds.enumerated() { + guard let syndrome = round.syndromeValue, syndrome.isFinite else { continue } + guard let spacing = round.latticeSpacing, spacing.isFinite, spacing > 0 else { continue } + guard let targetIndex = round.targetLatticeIndex else { continue } + let logicalPass = round.logicalPass ?? false + extracted.append( + Point( + id: index, + round: round.round ?? (index + 1), + syndrome: syndrome, + target: Double(targetIndex) * spacing, + spacing: spacing, + pass: logicalPass + ) + ) + } + return extracted.sorted { $0.round < $1.round } + } + + private var baseSpacing: Double? { + points.first?.spacing + } + + private var baseTarget: Double? { + points.first?.target + } + + private var domain: (min: Double, max: Double)? { + guard !points.isEmpty else { return nil } + let values = points.map(\.syndrome) + points.map(\.target) + guard let minValue = values.min(), let maxValue = values.max() else { return nil } + let spacing = baseSpacing ?? 1.0 + let span = maxValue - minValue + let padding = max(0.2, max(0.6 * spacing, 0.12 * span)) + return (minValue - padding, maxValue + padding) + } + + private var latticeCenters: [Double] { + guard let spacing = baseSpacing, let domain else { return [] } + let minIndex = Int(floor(domain.min / spacing)) + let maxIndex = Int(ceil(domain.max / spacing)) + guard minIndex <= maxIndex else { return [] } + let clampedMin = max(minIndex, -128) + let clampedMax = min(maxIndex, 128) + if clampedMin > clampedMax { return [] } + return (clampedMin...clampedMax).map { Double($0) * spacing } + } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("QEC Lattice Checker") + .font(.caption.weight(.semibold)) + Text("X-axis: Lattice Coordinate Y-axis: Round Index") + .font(.caption2) + .foregroundStyle(.secondary) + + if points.isEmpty { + Text("Load and run a QEC circuit to visualize lattice/syndrome positions.") + .font(.caption) + .foregroundStyle(.secondary) + } else { + GeometryReader { proxy in + Canvas { context, size in + guard let domain else { return } + let width = max(size.width, 1) + let top: CGFloat = 8 + let bottom: CGFloat = size.height - 8 + let height = max(bottom - top, 1) + let scale = max(domain.max - domain.min, 1e-9) + + func x(_ value: Double) -> CGFloat { + CGFloat((value - domain.min) / scale) * width + } + + func y(_ index: Int) -> CGFloat { + if points.count <= 1 { return top + (height / 2) } + return top + (CGFloat(index) * height / CGFloat(points.count - 1)) + } + + if let spacing = baseSpacing, let target = baseTarget { + let threshold = 0.25 * spacing + let left = x(target - threshold) + let right = x(target + threshold) + let rect = CGRect( + x: min(left, right), + y: top, + width: abs(right - left), + height: height + ) + context.fill(Path(rect), with: .color(Color.green.opacity(0.10))) + } + + for center in latticeCenters { + var line = Path() + let xPos = x(center) + line.move(to: CGPoint(x: xPos, y: top)) + line.addLine(to: CGPoint(x: xPos, y: bottom)) + context.stroke( + line, + with: .color(Color.secondary.opacity(0.35)), + style: StrokeStyle(lineWidth: 1) + ) + } + + if let target = baseTarget { + var targetLine = Path() + let xPos = x(target) + targetLine.move(to: CGPoint(x: xPos, y: top)) + targetLine.addLine(to: CGPoint(x: xPos, y: bottom)) + context.stroke( + targetLine, + with: .color(Color.blue.opacity(0.85)), + style: StrokeStyle(lineWidth: 2) + ) + } + + if let first = points.first { + var trajectory = Path() + trajectory.move(to: CGPoint(x: x(first.syndrome), y: y(0))) + for (index, point) in points.enumerated().dropFirst() { + trajectory.addLine(to: CGPoint(x: x(point.syndrome), y: y(index))) + } + context.stroke( + trajectory, + with: .color(Color.pink.opacity(0.75)), + style: StrokeStyle(lineWidth: 1.8, lineCap: .round, lineJoin: .round) + ) + } + + for (index, point) in points.enumerated() { + let center = CGPoint(x: x(point.syndrome), y: y(index)) + let marker = CGRect(x: center.x - 3.0, y: center.y - 3.0, width: 6.0, height: 6.0) + context.fill( + Path(ellipseIn: marker), + with: .color(point.pass ? Color.green : Color.orange) + ) + } + } + } + .frame(height: 165) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + + HStack(spacing: 12) { + Text("target=\(metricText(baseTarget))") + Text("spacing=\(metricText(baseSpacing))") + if let domain { + Text("domain=[\(metricText(domain.min)), \(metricText(domain.max))]") + } + } + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } + + private func metricText(_ value: Double?) -> String { + guard let value, value.isFinite else { return "n/a" } + return String(format: "%.5f", value) + } +} + +private struct GateHistogramCard: View { + @EnvironmentObject private var store: StudioStore + let items: [(String, Int)] + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Gate Frequency") + .font(.caption.weight(.semibold)) + Text("X-axis: Gate Type Y-axis: Frequency") + .font(.caption2) + .foregroundStyle(.secondary) + if items.isEmpty { + Text("No gate frames available.") + .font(.caption) + .foregroundStyle(.secondary) + } else { + let maxCount = items.map { $0.1 }.max() ?? 1 + ForEach(items, id: \.0) { gateType, count in + HStack(spacing: 8) { + Text(gateType) + .font(.caption.monospaced()) + .frame(width: 120, alignment: .leading) + GeometryReader { proxy in + let width = proxy.size.width * CGFloat(count) / CGFloat(maxCount) + RoundedRectangle(cornerRadius: 4) + .fill(Color.cyan.opacity(0.75)) + .frame(width: max(2, width), height: 12, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(height: 12) + Text("\(count)") + .font(.caption.monospacedDigit()) + .frame(width: 36, alignment: .trailing) + } + .frame(height: 16) + } + } + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct RunOutputSummaryCard: View { + @EnvironmentObject private var store: StudioStore + let lines: [String] + let statusText: String + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Run Output") + .font(.caption.weight(.semibold)) + Text("Status: \(statusText)") + .font(.caption) + .foregroundStyle(.secondary) + + VStack(alignment: .leading, spacing: 4) { + ForEach(Array(lines.enumerated()), id: \.offset) { _, line in + Text(line) + .font(.system(.caption, design: .monospaced)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(palette.inputBackground) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(palette.border, lineWidth: 1) + ) + ) + } + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct MetricCard: View { + @EnvironmentObject private var store: StudioStore + let title: String + let value: String + let subtitle: String + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Text(value) + .font(.title2.weight(.bold)) + Text(subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct AuditTrailPanelView: View { + @EnvironmentObject private var store: StudioStore + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 4) { + ForEach(Array(store.auditTrail.enumerated().reversed()), id: \.offset) { _, line in + Text(line) + .font(.system(.caption, design: .monospaced)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(8) + } + .background( + RoundedRectangle(cornerRadius: 10) + .fill(palette.subpanel) + ) + } +} + +private struct CardSection: View { + @EnvironmentObject private var store: StudioStore + var title: String? = nil + @ViewBuilder var content: Content + private var palette: ThemePalette { .resolve(store.theme) } + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + if let title { + Text(title) + .font(.title3.weight(.semibold)) + } + content + } + .padding(10) + .frame(maxHeight: .infinity, alignment: .top) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(palette.panel) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(palette.border, lineWidth: 1) + ) + .shadow(color: Color.black.opacity(0.04), radius: 6, x: 0, y: 2) + ) + .padding(8) + } +} + +private struct LabeledContentBlock: View { + let title: String + @ViewBuilder var content: Content + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + .lineLimit(1) + content + } + } +} + +private struct KeyValueRow: View { + let title: String + let value: String + + var body: some View { + HStack { + Text(title) + .foregroundStyle(.secondary) + Spacer() + Text(value) + .font(.caption.monospaced()) + .lineLimit(1) + .truncationMode(.middle) + } + .font(.caption) + } +} + +private struct LabeledField: View { + let title: String + @Binding var text: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + TextField(title, text: $text) + .textFieldStyle(.roundedBorder) + .font(.caption) + } + } +} + +private struct LabeledPicker: View { + let title: String + @Binding var selection: BackendSelection + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Picker(title, selection: $selection) { + ForEach(BackendSelection.allCases) { backend in + Text(backend.title).tag(backend) + } + } + .pickerStyle(.segmented) + .labelsHidden() + } + } +} + +private struct LabeledRuntimeEnginePicker: View { + let title: String + @Binding var selection: RuntimeEngineSelection + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Picker(title, selection: $selection) { + ForEach(RuntimeEngineSelection.allCases) { runtimeEngine in + Text(runtimeEngine.title).tag(runtimeEngine) + } + } + .pickerStyle(.segmented) + .labelsHidden() + } + } +} + +private struct NumericParameterEditor: View { + let title: String + @Binding var value: Double + let suggestedRange: ClosedRange? + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption) + HStack(spacing: 8) { + TextField( + title, + value: $value, + format: .number.precision(.fractionLength(0...6)) + ) + .textFieldStyle(.roundedBorder) + .font(.caption.monospaced()) + .frame(width: 120) + + if let suggestedRange { + Slider(value: $value, in: suggestedRange) + } + } + if let suggestedRange { + Text("Suggested range: \(format(suggestedRange.lowerBound)) ... \(format(suggestedRange.upperBound))") + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } else { + Text(format(value)) + .font(.caption2.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + } + + private func format(_ value: Double) -> String { + String(format: "%.6f", value) + } +} diff --git a/core-swift/Tests/SchroSIMStudioTests/StudioStoreQECTests.swift b/core-swift/Tests/SchroSIMStudioTests/StudioStoreQECTests.swift new file mode 100644 index 0000000..4c1c493 --- /dev/null +++ b/core-swift/Tests/SchroSIMStudioTests/StudioStoreQECTests.swift @@ -0,0 +1,35 @@ +import XCTest +@testable import schrosim_studio + +@MainActor +final class StudioStoreQECTests: XCTestCase { + func testQECProjectAutoBindsPolicyToBackend() { + let store = StudioStore() + + store.workspace = "Quantum Foundry" + store.project = "Quantum Error Correction" + XCTAssertEqual(store.backend, .hybrid) + XCTAssertEqual(store.selectedKPIPolicy?.id, "cv_qec_gaussian_v1") + + store.backend = .fock + XCTAssertEqual(store.selectedKPIPolicy?.id, "cv_fock_validation_v1") + + store.backend = .gaussian + XCTAssertEqual(store.selectedKPIPolicy?.id, "cv_qec_gaussian_v1") + + store.project = "Boson Sampling" + XCTAssertEqual(store.selectedKPIPolicy?.id, "cv_gaussian_production_v1") + } + + func testQECPolicyThresholdsLoadedFromCatalog() { + let store = StudioStore() + guard let policy = store.kpiPolicies.first(where: { $0.id == "cv_qec_gaussian_v1" }) else { + XCTFail("Expected cv_qec_gaussian_v1 policy in KPI catalog") + return + } + + XCTAssertEqual(policy.thresholds.maxLogicalErrorRate ?? -1.0, 0.34, accuracy: 1e-12) + XCTAssertEqual(policy.thresholds.minSuppressionFactor ?? -1.0, 1.0, accuracy: 1e-12) + XCTAssertEqual(policy.thresholds.minBreakEvenGain ?? -1.0, 0.0, accuracy: 1e-12) + } +} diff --git a/core-swift/Tests/SchroSIMTests/unit/backend_regression_tests.swift b/core-swift/Tests/SchroSIMTests/unit/backend_regression_tests.swift index 2b09f9a..e4f2ec3 100644 --- a/core-swift/Tests/SchroSIMTests/unit/backend_regression_tests.swift +++ b/core-swift/Tests/SchroSIMTests/unit/backend_regression_tests.swift @@ -15,7 +15,7 @@ final class BackendRegressionTests: XCTestCase { XCTAssertTrue(approxEqual(gaussian.mean, hybrid.mean, tol: 1e-12)) XCTAssertTrue(LA.approxEqual(gaussian.cov, hybrid.cov, tol: 1e-12)) - // Reference value from examples/foundry_loss_map.json circuit. + // Reference value from docs/examples/foundry_loss_map.json circuit. XCTAssertEqual(meanPhotonNumber(gaussian), 0.5647815555903229, accuracy: 1e-12) } diff --git a/docs/01-overview.md b/docs/01-overview.md index 3c54595..2f1b190 100644 --- a/docs/01-overview.md +++ b/docs/01-overview.md @@ -7,6 +7,7 @@ SchroSIM is a continuous-variable photonic quantum simulation stack with: - a Swift core simulation SDK, - a Swift CLI (`schrosim-cli`) for execution and tracing, - a Rust runtime (`schrosim-core`) for parity/benchmark workflows and runtime operations. +- SchroSIM Studio, an open-source SwiftUI surface for circuit design, validation evidence, and replay-oriented analysis. ## Who It Is For @@ -16,8 +17,8 @@ SchroSIM is a continuous-variable photonic quantum simulation stack with: ## Product Boundary -- Public repo: CLI/core/docs. -- Enterprise-specific materials: isolated under `enterprise/`. +- Public repo: CLI/core/docs/Studio UI. +- Public examples, config, benchmarks, and assets are kept under `docs/`. ## Why This Exists diff --git a/docs/02-quickstart.md b/docs/02-quickstart.md index 69663cb..0e173a3 100644 --- a/docs/02-quickstart.md +++ b/docs/02-quickstart.md @@ -23,8 +23,8 @@ Python is required for helper scripts, but there are currently no third-party Py ## First Demo Runs ```bash -schrosim run examples/runtime_default_foundry.json --backend hybrid -schrosim run examples/cv/qec_single_logical_gkp_memory_mvp.json --backend hybrid +schrosim run docs/examples/runtime_default_foundry.json --backend hybrid +schrosim run docs/examples/cv/qec_single_logical_gkp_memory_mvp.json --backend hybrid ``` ## Optional Contributor Workflow (CLion) diff --git a/docs/03-architecture.md b/docs/03-architecture.md index 00a6ff7..934218a 100644 --- a/docs/03-architecture.md +++ b/docs/03-architecture.md @@ -17,9 +17,9 @@ JSON Circuit Input - Swift CLI: `core-swift/Sources/schrosim-cli` - Rust runtime CLI: `core-rust/schrosim-core` - Config and governance: - - `config/foundry_registry.json` - - `config/trace_rbac_policy.json` - - `config/kpi_policies.json` + - `docs/config/foundry_registry.json` + - `docs/config/trace_rbac_policy.json` + - `docs/config/kpi_policies.json` ## Public Module Map diff --git a/docs/04-circuit-ir-and-schema.md b/docs/04-circuit-ir-and-schema.md index d3c3ffc..55d3cdc 100644 --- a/docs/04-circuit-ir-and-schema.md +++ b/docs/04-circuit-ir-and-schema.md @@ -41,6 +41,6 @@ Primary IR sources: ## Example Inputs -- `examples/runtime_default_foundry.json` -- `examples/fock_injection_smoke.json` -- `examples/cv/qec_single_logical_gkp_memory_mvp.json` +- `docs/examples/runtime_default_foundry.json` +- `docs/examples/fock_injection_smoke.json` +- `docs/examples/cv/qec_single_logical_gkp_memory_mvp.json` diff --git a/docs/05-compiler-and-foundry.md b/docs/05-compiler-and-foundry.md index 11c1433..b247e64 100644 --- a/docs/05-compiler-and-foundry.md +++ b/docs/05-compiler-and-foundry.md @@ -27,7 +27,7 @@ Reference implementation: Registry-backed production profiles: -- `config/foundry_registry.json` +- `docs/config/foundry_registry.json` - `foundry-admin add-draft` - `foundry-admin promote` (approval/signing) diff --git a/docs/08-tracing-observability-reproducibility.md b/docs/08-tracing-observability-reproducibility.md index 9b1db57..2e908bd 100644 --- a/docs/08-tracing-observability-reproducibility.md +++ b/docs/08-tracing-observability-reproducibility.md @@ -18,7 +18,7 @@ Trace flows support: Policies: -- `config/trace_rbac_policy.json` +- `docs/config/trace_rbac_policy.json` ## Determinism Contract diff --git a/docs/09-validation-and-benchmarks.md b/docs/09-validation-and-benchmarks.md index 3217b4c..7309f4e 100644 --- a/docs/09-validation-and-benchmarks.md +++ b/docs/09-validation-and-benchmarks.md @@ -37,9 +37,9 @@ bash scripts/compute-benchmark-scaling.sh ... ## Baseline Artifacts -- `benchmarks/schrosim-core-baseline.json` -- `benchmarks/schrosim-core-scaling-baseline.json` -- `benchmarks/compute-scaling-baseline.json` +- `docs/benchmarks/schrosim-core-baseline.json` +- `docs/benchmarks/schrosim-core-scaling-baseline.json` +- `docs/benchmarks/compute-scaling-baseline.json` ## Benchmark Interpretation Guide @@ -83,6 +83,6 @@ Recommended gate order: 1. `bash scripts/cv-validation-suite.sh --runtime both` 2. `bash scripts/trace-slo-check.sh --max-gates 512 --max-frame-ms 50` 3. `bash scripts/compute-benchmark.sh ... --max-p95-ms ` -4. `bash scripts/compute-benchmark-scaling.sh --baseline benchmarks/compute-scaling-baseline.json --max-regression-pct 180` +4. `bash scripts/compute-benchmark-scaling.sh --baseline docs/benchmarks/compute-scaling-baseline.json --max-regression-pct 180` If step 4 fails on routing mismatch, treat it as a policy/routing regression, not a raw latency fluctuation. diff --git a/docs/12-worked-examples.md b/docs/12-worked-examples.md index 95d35ea..1b7848f 100644 --- a/docs/12-worked-examples.md +++ b/docs/12-worked-examples.md @@ -2,22 +2,22 @@ ## Example 1: Runtime Default Foundry -- Input: `examples/runtime_default_foundry.json` +- Input: `docs/examples/runtime_default_foundry.json` - Goal: minimal end-to-end run path. ## Example 2: Fock Injection Smoke -- Input: `examples/fock_injection_smoke.json` +- Input: `docs/examples/fock_injection_smoke.json` - Goal: demonstrate backend routing to Fock-compatible execution. ## Example 3: Foundry Loss Map -- Input: `examples/foundry_loss_map.json` +- Input: `docs/examples/foundry_loss_map.json` - Goal: foundry validation and injected mode-loss behavior. ## Example 4: GKP QEC Memory MVP -- Input: `examples/cv/qec_single_logical_gkp_memory_mvp.json` +- Input: `docs/examples/cv/qec_single_logical_gkp_memory_mvp.json` - Goal: measurement-driven decode/correction loop and QEC metrics. ## Example Playbook (Command + Expected Result) @@ -27,7 +27,7 @@ Command: ```bash -swift run schrosim-cli run examples/runtime_default_foundry.json +swift run schrosim-cli run docs/examples/runtime_default_foundry.json ``` Expected backend resolution: @@ -52,7 +52,7 @@ Interpretation: Command: ```bash -swift run schrosim-cli run examples/fock_injection_smoke.json +swift run schrosim-cli run docs/examples/fock_injection_smoke.json ``` Expected backend resolution: @@ -76,7 +76,7 @@ Interpretation: Command: ```bash -swift run schrosim-cli run examples/foundry_loss_map.json +swift run schrosim-cli run docs/examples/foundry_loss_map.json ``` Expected backend resolution: @@ -100,7 +100,7 @@ Interpretation: Command: ```bash -swift run schrosim-cli run examples/cv/qec_single_logical_gkp_memory_mvp.json +swift run schrosim-cli run docs/examples/cv/qec_single_logical_gkp_memory_mvp.json ``` Expected backend resolution: diff --git a/docs/13-developer-guide.md b/docs/13-developer-guide.md index 1a412b0..6ad3487 100644 --- a/docs/13-developer-guide.md +++ b/docs/13-developer-guide.md @@ -5,8 +5,9 @@ - Swift package manifest: `Package.swift` - Rust workspace manifest: `Cargo.toml` - Swift core and CLI: `core-swift/Sources/*` +- SchroSIM Studio UI: `core-swift/Sources/schrosim-studio/*` - Rust runtime: `core-rust/schrosim-core/*` -- Scripts and baselines: `scripts/*`, `benchmarks/*` +- Scripts and baselines: `scripts/*`, `docs/benchmarks/*` ## Build and Test @@ -31,9 +32,9 @@ See detailed guide: ## Contribution and Security Policies -- [CONTRIBUTING.md](https://github.com/DennisWayo/SchroSIM/blob/master/CONTRIBUTING.md) -- [CODE_OF_CONDUCT.md](https://github.com/DennisWayo/SchroSIM/blob/master/CODE_OF_CONDUCT.md) -- [SECURITY.md](https://github.com/DennisWayo/SchroSIM/blob/master/SECURITY.md) +- [CONTRIBUTING.md](https://github.com/Gottesman-Software/SchroSIM/blob/master/CONTRIBUTING.md) +- [CODE_OF_CONDUCT.md](https://github.com/Gottesman-Software/SchroSIM/blob/master/CODE_OF_CONDUCT.md) +- [SECURITY.md](https://github.com/Gottesman-Software/SchroSIM/blob/master/SECURITY.md) ## Public Release Process (Current) @@ -50,8 +51,8 @@ Until full automation is added, use this manual flow for public releases: - Swift CLI version constant in `core-swift/Sources/schrosim-cli/main.swift` - Rust CLI version constant in `core-rust/schrosim-core/src/main.rs` 4. Public-scope hygiene: - - ensure `enterprise/` private material remains ignored, - - ensure README/docs do not depend on enterprise-only assets. + - ensure public UI source remains in `core-swift/Sources/schrosim-studio/`, + - ensure public examples, config, benchmarks, and assets live under `docs/`. 5. Tag and publish: - create release tag (`vX.Y.Z`), - publish release notes summarizing API/CLI changes, validation coverage, and known limitations. diff --git a/docs/14-schrosim-studio.md b/docs/14-schrosim-studio.md new file mode 100644 index 0000000..2d146b0 --- /dev/null +++ b/docs/14-schrosim-studio.md @@ -0,0 +1,27 @@ +# SchroSIM Studio + +SchroSIM Studio is the open-source UI surface for building and inspecting photonic circuit studies before execution through the SchroSIM core runtime. + +## What It Covers + +- Project-scoped circuit layouts and reusable presets. +- Backend-aware execution policy selection. +- Foundry-profile and runtime validation evidence. +- QEC-oriented result views, diagnostics, and replay summaries. +- Export paths that keep circuit, runtime, and report artifacts inspectable. + +## Run From Source + +```bash +swift run schrosim-studio +``` + +The Studio target is defined in the root Swift package and uses the same public SchroSIM core library as the CLI. + +## Source Layout + +- `core-swift/Sources/schrosim-studio/` contains the SwiftUI app. +- `core-swift/Tests/SchroSIMStudioTests/` contains Studio-focused Swift tests. +- `docs/config/profiles/` contains public sample profiles used by the UI. + +Generated exports and local workflow state are intentionally not tracked. diff --git a/docs/DEBUGGING.md b/docs/DEBUGGING.md index 67844e4..f5cb723 100644 --- a/docs/DEBUGGING.md +++ b/docs/DEBUGGING.md @@ -1,78 +1,56 @@ # SchroSIM Debugging Guide -## 1) Debug Rust GUI in CLion - -### Prerequisites -- Open the repo root (`SchroSIM`) in CLion. -- Use the Cargo project at `Cargo.toml` (workspace root). - -### Run configuration -1. Create a new `Cargo` run configuration: - - `Command`: `run` - - `Package`: `schrosim-gui` - - `Working directory`: `$ProjectFileDir$` -2. Add environment variables: - - `RUST_BACKTRACE=1` (also enforced via `.cargo/config.toml`) - - Optional for Swift attach flow: `SCHROSIM_WAIT_SWIFT_DEBUGGER=1` -3. Start with `Debug` (not `Run`). - -### Breakpoints to set -- `core-rust/schrosim-gui/src/main.rs` at `fn run_cli(...)` -- `core-rust/schrosim-gui/src/main.rs` at `fn parse_cli_output(...)` (JSON parsing logic) -- `core-rust/schrosim-gui/src/main.rs` inside `impl eframe::App for SchroSimApp` -> `fn update(...)` - -### Verify breakpoints are hit -1. Launch debugger in CLion. -2. In GUI, click `Run circuit`. -3. Confirm debugger stops in `update`, then in `run_cli`, then in `parse_cli_output`. - -## 2) Attach Xcode Debugger to Swift CLI Spawned by Rust GUI - -### Enable wait mode -- Start Rust GUI from CLion with: - - `SCHROSIM_WAIT_SWIFT_DEBUGGER=1` - -When GUI invokes CLI, `schrosim-cli` is launched with `--wait-debugger` and pauses itself via `SIGSTOP`. - -### Attach from Xcode -1. Open Xcode. -2. Choose `Debug` -> `Attach to Process by PID or Name...`. -3. Enter `schrosim-cli` and attach. -4. Press `Continue` in Xcode to resume the paused CLI process. - -### Swift breakpoints -- `core-swift/Sources/schrosim-cli/main.swift` at `waitForDebuggerIfRequested(...)` -- `core-swift/Sources/schrosim-cli/main.swift` at JSON decode line in `handleRun(...)` -- `core-swift/Sources/schrosim-cli/main.swift` at `emitJSON(...)` - -### Verify -1. With Rust GUI running under CLion debug, click `Run circuit`. -2. Attach Xcode to `schrosim-cli`. -3. Confirm Swift breakpoints trigger, then Rust returns to `parse_cli_output`. - -## 3) Rust Workspace Layout - -Rust is configured as a workspace at repo root, with crates in `core-rust/`: - -```text -Cargo.toml # workspace + dev profile (debug symbols on) -core-rust/schrosim-gui/ # member crate +## Swift CLI + +Use the CLI path for reproducible debugging and CI parity: + +```bash +swift run schrosim-cli run docs/examples/runtime_default_foundry.json --backend hybrid +swift run schrosim-cli trace-stream docs/examples/foundry_loss_map.json --backend hybrid --stream-format ndjson +``` + +Useful breakpoints: + +- `core-swift/Sources/schrosim-cli/main.swift` at input parsing and command dispatch. +- `core-swift/Sources/SchroSIM/src/compiler/ir/circuit_ir.swift` for circuit validation. +- `core-swift/Sources/SchroSIM/src/compiler/foundry/foundry_spec.swift` for foundry policy checks. +- `core-swift/Sources/SchroSIM/src/core/simulator.swift` for backend execution. + +## SwiftUI Studio + +Run the public Studio UI from the root package: + +```bash +swift run schrosim-studio ``` -To add more Rust crates later, create a crate directory and add it to `members` in root `Cargo.toml`. +Useful breakpoints: + +- `core-swift/Sources/schrosim-studio/SchroSIMStudioApp.swift` for app launch. +- `core-swift/Sources/schrosim-studio/StudioStore.swift` for workspace state, validation, and replay logic. +- `core-swift/Sources/schrosim-studio/CLIService.swift` for CLI execution boundaries. +- `core-swift/Sources/schrosim-studio/StudioViews.swift` for UI interactions. -## 4) Fix `swift test` / XCTest in CLion +## Tests -If `swift test` fails with `no such module 'XCTest'`, CLion is typically using only Command Line Tools (`/Library/Developer/CommandLineTools`). +Run the Swift test suite: + +```bash +swift test +``` + +Run the Rust runtime tests: + +```bash +cargo test -p schrosim-core +``` -### Required setup +## CLion and Xcode Notes -1. Install full Xcode (App Store or Developer download). -2. In CLion, use the shared run config: `Swift Test (core-swift)`. -3. If your Xcode path differs, edit `DEVELOPER_DIR` in that run config. +If `swift test` fails with `no such module 'XCTest'`, CLion is usually using only Command Line Tools rather than full Xcode. -Shared assets in repo: -- Script: `scripts/swift-test.sh` -- Run config: `.run/Swift Test (core-swift).run.xml` +Recommended setup: -The script auto-selects `Xcode.app` or `Xcode-beta.app`, sets `SDKROOT=macosx`, and runs `swift test`. +- Install full Xcode. +- Use `scripts/swift-test.sh` for local test runs when IDE environment variables are inconsistent. +- Keep CLI commands as the reference path for bug reports and CI reproduction. diff --git a/docs/README.md b/docs/README.md index 4ea7ace..b000fad 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,11 +17,12 @@ This directory is the working documentation set for SchroSIM. 11. [Mathematical Proofs](11-mathematical-proofs.md) 12. [Worked Examples](12-worked-examples.md) 13. [Developer Guide](13-developer-guide.md) +14. [SchroSIM Studio](14-schrosim-studio.md) ## Scope Notes -- Public repo scope: CLI/core/docs workflows. -- Enterprise-only material is kept under `enterprise/`. +- Public repo scope: CLI/core/docs/Studio UI workflows. +- Public examples, config, benchmarks, and assets live under `docs/`. - Existing debug playbook: [DEBUGGING.md](DEBUGGING.md). ## Suggested Reading Order diff --git a/assets/schrosim-logo.png b/docs/assets/schrosim-logo.png similarity index 100% rename from assets/schrosim-logo.png rename to docs/assets/schrosim-logo.png diff --git a/benchmarks/compute-scaling-baseline.json b/docs/benchmarks/compute-scaling-baseline.json similarity index 100% rename from benchmarks/compute-scaling-baseline.json rename to docs/benchmarks/compute-scaling-baseline.json diff --git a/benchmarks/schrosim-core-baseline.json b/docs/benchmarks/schrosim-core-baseline.json similarity index 100% rename from benchmarks/schrosim-core-baseline.json rename to docs/benchmarks/schrosim-core-baseline.json diff --git a/benchmarks/schrosim-core-scaling-baseline.json b/docs/benchmarks/schrosim-core-scaling-baseline.json similarity index 100% rename from benchmarks/schrosim-core-scaling-baseline.json rename to docs/benchmarks/schrosim-core-scaling-baseline.json diff --git a/config/foundry_registry.json b/docs/config/foundry_registry.json similarity index 91% rename from config/foundry_registry.json rename to docs/config/foundry_registry.json index b8bd0cf..6ed8bd0 100644 --- a/config/foundry_registry.json +++ b/docs/config/foundry_registry.json @@ -18,7 +18,7 @@ "timestamp" : "2026-04-24T09:56:31.282Z" } ], - "profile_id" : "enterprise-default", + "profile_id" : "studio-default", "signature" : "3b16e307c522022e23757e677a89e42d17e90ea5d039c0c837c1d740d776c471", "spec" : { "allow_measurements" : true, @@ -29,7 +29,7 @@ "mode_loss_eta" : [ 0.995 ], - "name" : "enterprise-default-v1" + "name" : "studio-default-v1" }, "status" : "approved", "valid_from" : "2026-04-24T09:56:18.108Z", diff --git a/config/kpi_policies.json b/docs/config/kpi_policies.json similarity index 91% rename from config/kpi_policies.json rename to docs/config/kpi_policies.json index a353b62..f519965 100644 --- a/config/kpi_policies.json +++ b/docs/config/kpi_policies.json @@ -7,7 +7,7 @@ "target_representation": "gaussian_phase_space", "summary": "Production readiness policy for Gaussian CV circuits using deterministic parity plus calibrated-noise robustness thresholds.", "references": [ - "Internal enterprise qualification standard: CV-GAUSS-PROD-v1" + "Open-source studio qualification standard: CV-GAUSS-PROD-v1" ], "thresholds": { "min_success_rate": 0.95, @@ -27,7 +27,7 @@ "target_representation": "gaussian_phase_space", "summary": "QEC qualification policy for Gaussian/Hybrid backends using logical error rate, suppression factor against physical shift noise, and break-even gain.", "references": [ - "Internal enterprise qualification standard: CV-QEC-GAUSS-v1" + "Open-source studio qualification standard: CV-QEC-GAUSS-v1" ], "thresholds": { "min_success_rate": 0.90, @@ -48,7 +48,7 @@ "target_representation": "fock_number_basis", "summary": "Validation policy for Fock-basis runs emphasizing normalization fidelity and robust runtime consistency.", "references": [ - "Internal enterprise qualification standard: CV-FOCK-VAL-v1" + "Open-source studio qualification standard: CV-FOCK-VAL-v1" ], "thresholds": { "min_success_rate": 0.95, diff --git a/docs/config/profiles/studio-default-v1.json b/docs/config/profiles/studio-default-v1.json new file mode 100644 index 0000000..0030534 --- /dev/null +++ b/docs/config/profiles/studio-default-v1.json @@ -0,0 +1,9 @@ +{ + "name": "studio-default-v1", + "max_modes": 64, + "max_squeezing_r": 1.2, + "allow_non_gaussian": true, + "allow_measurements": true, + "inject_mode_loss": true, + "mode_loss_eta": [0.995] +} diff --git a/config/sample_calibrated_noise_profile.json b/docs/config/sample_calibrated_noise_profile.json similarity index 92% rename from config/sample_calibrated_noise_profile.json rename to docs/config/sample_calibrated_noise_profile.json index e69ded2..66841f4 100644 --- a/config/sample_calibrated_noise_profile.json +++ b/docs/config/sample_calibrated_noise_profile.json @@ -41,5 +41,5 @@ "phase_rad_per_sample": 0.00018, "displacement_per_sample": 0.00012 }, - "notes": "Sample calibrated tensor profile for SchroSIM enterprise UI Monte Carlo analysis." + "notes": "Sample calibrated tensor profile for SchroSIM Studio Monte Carlo analysis." } diff --git a/config/trace_rbac_policy.json b/docs/config/trace_rbac_policy.json similarity index 100% rename from config/trace_rbac_policy.json rename to docs/config/trace_rbac_policy.json diff --git a/examples/basic_loss.json b/docs/examples/basic_loss.json similarity index 100% rename from examples/basic_loss.json rename to docs/examples/basic_loss.json diff --git a/examples/cv/foundry_validation_single_mode_fock_response.json b/docs/examples/cv/foundry_validation_single_mode_fock_response.json similarity index 100% rename from examples/cv/foundry_validation_single_mode_fock_response.json rename to docs/examples/cv/foundry_validation_single_mode_fock_response.json diff --git a/examples/cv/foundry_validation_single_mode_squeezing.json b/docs/examples/cv/foundry_validation_single_mode_squeezing.json similarity index 100% rename from examples/cv/foundry_validation_single_mode_squeezing.json rename to docs/examples/cv/foundry_validation_single_mode_squeezing.json diff --git a/examples/cv/foundry_validation_thermal_heterodyne.json b/docs/examples/cv/foundry_validation_thermal_heterodyne.json similarity index 100% rename from examples/cv/foundry_validation_thermal_heterodyne.json rename to docs/examples/cv/foundry_validation_thermal_heterodyne.json diff --git a/examples/cv/foundry_validation_two_mode_epr_loss.json b/docs/examples/cv/foundry_validation_two_mode_epr_loss.json similarity index 100% rename from examples/cv/foundry_validation_two_mode_epr_loss.json rename to docs/examples/cv/foundry_validation_two_mode_epr_loss.json diff --git a/examples/cv/loss_channel.swift b/docs/examples/cv/loss_channel.swift similarity index 100% rename from examples/cv/loss_channel.swift rename to docs/examples/cv/loss_channel.swift diff --git a/examples/cv/qec_single_logical_gkp_memory_mvp.json b/docs/examples/cv/qec_single_logical_gkp_memory_mvp.json similarity index 100% rename from examples/cv/qec_single_logical_gkp_memory_mvp.json rename to docs/examples/cv/qec_single_logical_gkp_memory_mvp.json diff --git a/examples/fock_injection_smoke.json b/docs/examples/fock_injection_smoke.json similarity index 100% rename from examples/fock_injection_smoke.json rename to docs/examples/fock_injection_smoke.json diff --git a/examples/foundry_loss_map.json b/docs/examples/foundry_loss_map.json similarity index 100% rename from examples/foundry_loss_map.json rename to docs/examples/foundry_loss_map.json diff --git a/examples/foundry_registry_ref.json b/docs/examples/foundry_registry_ref.json similarity index 83% rename from examples/foundry_registry_ref.json rename to docs/examples/foundry_registry_ref.json index 8f25eb7..5f563f7 100644 --- a/examples/foundry_registry_ref.json +++ b/docs/examples/foundry_registry_ref.json @@ -4,7 +4,7 @@ "modes": 1, "backend": "gaussian", "foundry_profile": { - "profile_id": "enterprise-default", + "profile_id": "studio-default", "version": 1 }, "gates": [ diff --git a/examples/runtime_default_foundry.json b/docs/examples/runtime_default_foundry.json similarity index 100% rename from examples/runtime_default_foundry.json rename to docs/examples/runtime_default_foundry.json diff --git a/examples/seeded_homodyne.json b/docs/examples/seeded_homodyne.json similarity index 100% rename from examples/seeded_homodyne.json rename to docs/examples/seeded_homodyne.json diff --git a/docs/ui-mockups/schrosim-enterprise-preview.gif b/docs/ui-mockups/schrosim-studio-preview.gif similarity index 100% rename from docs/ui-mockups/schrosim-enterprise-preview.gif rename to docs/ui-mockups/schrosim-studio-preview.gif diff --git a/mkdocs.yml b/mkdocs.yml index 65f82f5..c653cd1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,12 +1,12 @@ site_name: SchroSIM site_description: Continuous-variable photonic quantum simulation stack -site_url: https://denniswayo.github.io/SchroSIM/ +site_url: https://gottesman-software.github.io/SchroSIM/ docs_dir: docs exclude_docs: README.md -repo_url: https://github.com/DennisWayo/SchroSIM -repo_name: DennisWayo/SchroSIM +repo_url: https://github.com/Gottesman-Software/SchroSIM +repo_name: Gottesman-Software/SchroSIM edit_uri: edit/master/docs/ theme: @@ -32,4 +32,5 @@ nav: - Mathematical Proofs: 11-mathematical-proofs.md - Developer: - Worked Examples: 12-worked-examples.md + - SchroSIM Studio: 14-schrosim-studio.md - Developer Guide: 13-developer-guide.md diff --git a/pyproject.toml b/pyproject.toml index fd60ca9..411df36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,10 @@ classifiers = [ dependencies = [] [project.urls] -Homepage = "https://github.com/DennisWayo/SchroSIM" -Documentation = "https://denniswayo.github.io/SchroSIM/" -Repository = "https://github.com/DennisWayo/SchroSIM" -Issues = "https://github.com/DennisWayo/SchroSIM/issues" +Homepage = "https://github.com/Gottesman-Software/SchroSIM" +Documentation = "https://gottesman-software.github.io/SchroSIM/" +Repository = "https://github.com/Gottesman-Software/SchroSIM" +Issues = "https://github.com/Gottesman-Software/SchroSIM/issues" [project.scripts] schrosim = "schrosim.cli:main" diff --git a/scripts/cv-validation-suite.sh b/scripts/cv-validation-suite.sh index 6afed65..11d88ce 100755 --- a/scripts/cv-validation-suite.sh +++ b/scripts/cv-validation-suite.sh @@ -23,10 +23,10 @@ if [[ "$RUNTIME" != "swift" && "$RUNTIME" != "rust" && "$RUNTIME" != "both" ]]; fi CASES=( - "examples/cv/foundry_validation_single_mode_squeezing.json" - "examples/cv/foundry_validation_two_mode_epr_loss.json" - "examples/cv/foundry_validation_thermal_heterodyne.json" - "examples/cv/foundry_validation_single_mode_fock_response.json" + "docs/examples/cv/foundry_validation_single_mode_squeezing.json" + "docs/examples/cv/foundry_validation_two_mode_epr_loss.json" + "docs/examples/cv/foundry_validation_thermal_heterodyne.json" + "docs/examples/cv/foundry_validation_single_mode_fock_response.json" ) extract_summary() { diff --git a/studio/README.md b/studio/README.md new file mode 100644 index 0000000..4dc2bff --- /dev/null +++ b/studio/README.md @@ -0,0 +1,16 @@ +# SchroSIM Studio + +SchroSIM Studio is the public research UI for designing, inspecting, and replaying SchroSIM photonic circuit studies before running them through the CLI/core runtime. + +The open-source SwiftUI implementation lives in: + +- `core-swift/Sources/schrosim-studio/` +- `core-swift/Tests/SchroSIMStudioTests/` + +Local planning notes under `studio/docs/` are intentionally ignored. + +Run Studio from the root package: + +```bash +swift run schrosim-studio +```