diff --git a/quon_na/src/lib.rs b/quon_na/src/lib.rs index ea149c1..a58f4b9 100644 --- a/quon_na/src/lib.rs +++ b/quon_na/src/lib.rs @@ -114,9 +114,9 @@ pub use qec_schedule::{round_barrier_cuts, run_from_qec_workload}; pub use report::{ AtomLossBudget, BottleneckKind, ErrorBudgetContributions, RESOURCE_REPORT_EVIDENCE_DISCLAIMER, RESOURCE_REPORT_EVIDENCE_KIND, ReportError, ResourceReport, ScheduleOptimality, - TemporalAtomMetrics, attach_qec_error_budget, build_resource_report, - require_target_error_model, resource_report_to_json, resource_report_to_markdown, - simultaneous_layer_time, + TargetReportOverlay, TemporalAtomMetrics, attach_qec_error_budget, build_resource_report, + overlay_target_report, require_target_error_model, resource_report_to_json, + resource_report_to_markdown, simultaneous_layer_time, }; pub use schedule::{ AtomMove, EntanglingAction, LocalGateKind, MeasurementBasis, MovementGroup, NeutralAtomAction, diff --git a/quon_na/src/pipeline.rs b/quon_na/src/pipeline.rs index 4663752..efcece0 100644 --- a/quon_na/src/pipeline.rs +++ b/quon_na/src/pipeline.rs @@ -8,7 +8,7 @@ //! 2. **`schedule_from_graph`** → **`schedule_entangling_layers`** //! 3. **Zoned** (`schedule_zoned`) **or** flat AOD (`place` + `plan_aod_movement`) //! 4. **Optional compaction** ([`compact_schedule`], fail-closed) -//! 5. **Resource report** ([`build_resource_report`]) +//! 5. **Resource report** ([`build_resource_report`], then [`overlay_target_report`]) //! //! Fixed (OpenQASM) physical passes live in `mlir_bridge::pipeline`. @@ -25,7 +25,9 @@ use crate::graph::{InteractionGraph, LogicalQubitId}; use crate::layout::AtomId; use crate::movement::MovementParams; use crate::placement::PlacementStrategy; -use crate::report::{ResourceReport, attach_qec_error_budget, build_resource_report}; +use crate::report::{ + ResourceReport, TargetReportOverlay, build_resource_report, overlay_target_report, +}; use crate::schedule::{MeasurementBasis, NeutralAtomAction, ScheduleLayer}; use crate::schedule_entry::{GraphScheduleRequest, schedule_from_graph}; use crate::stats::{CompactionConfig, EffectiveConfig, NaStats, StageTimingsUs}; @@ -782,38 +784,20 @@ fn finish_pipeline( let stage_started = Instant::now(); let report = build_resource_report(&req.layers, None, Some(logical_qubits.max(1)))?; - let report = match aware_search_status { - Some((completed, fell_back)) => report.with_aware_search_status(completed, fell_back), - None => report, - }; - let report = match schedule_optimality { - Some(optimality) => report.with_schedule_optimality(optimality), - None => report, - }; - let report = report.with_agnostic_placer_mechanism(agnostic_placer_mechanism); - // Production path: attach analytic error_budget whenever the target carries - // an error_model (ADR-0017). `--emit-resource-report` in quonc additionally - // hard-requires the model so missing budgets fail at emit time. - let report = match na.error_model.as_ref() { - Some(model) => attach_qec_error_budget(report, Some(model))?, - None => report, - }; - // Analytic end-to-end fidelity estimate (Enola Eq. (1), issue #305). - // Unlike `error_model`, `NeutralAtomTarget::fidelity` is a mandatory - // field, so this overlay always applies once a target is available — - // `gate_fidelity_product`/`estimated_fidelity` are `Some` on every - // production report from this pipeline. - let report = report.with_fidelity_estimate(&req.layers, &na.fidelity); - // Analytic per-atom movement-heating / atom-loss budget (issue #310, - // \[Atomique\] Eqs. (1)–(2)). Optional like `error_model`: attached only - // when the target carries `atom_loss_model`, else the section is omitted. - // Distance is measured against the zoned schedule's layout (the real - // √-law travel); `req.layout` is `None` only for non-zoned hand-built - // schedules, in which case the budget is emitted zeroed (observable). - let report = match na.atom_loss_model.as_ref() { - Some(model) => report.with_atom_loss_budget(&req.layers, req.layout.as_ref(), model), - None => report, - }; + // Shared with `qec_schedule::schedule_expanded` (issue #468): error + // budget, fidelity, atom-loss, and the diagnostic labels. Emit-time + // `--emit-resource-report` still hard-requires `error_model` separately. + let report = overlay_target_report( + report, + &TargetReportOverlay { + layers: &req.layers, + layout: req.layout.as_ref(), + target: na, + aware_search: aware_search_status, + agnostic_placer_mechanism, + schedule_optimality, + }, + )?; let resource_report_us = elapsed_us(stage_started); let stats = NaStats { diff --git a/quon_na/src/qec_schedule.rs b/quon_na/src/qec_schedule.rs index 792abd5..a972a19 100644 --- a/quon_na/src/qec_schedule.rs +++ b/quon_na/src/qec_schedule.rs @@ -6,7 +6,8 @@ //! 2. Per round: Misra–Gries entangling → place→move (or zoned) → terminals //! 3. Append a durable [`NeutralAtomAction::Wait`] after each memory round //! 4. Concatenate rounds; compact with Barrier cuts at Wait markers (fail-closed) -//! 5. Resource report sized from code blocks + memory-round metadata +//! 5. Resource report sized from code blocks + memory-round metadata, then +//! [`crate::report::overlay_target_report`] use std::time::Instant; @@ -35,7 +36,7 @@ use crate::plan::{QecStageAccumulator, plan_backend}; use crate::qec::code_blocks_from_expanded; #[cfg(feature = "solver")] use crate::report::ScheduleOptimality; -use crate::report::{attach_qec_error_budget, build_resource_report}; +use crate::report::{TargetReportOverlay, build_resource_report, overlay_target_report}; use crate::schedule::{LocalGateKind, MeasurementBasis, NeutralAtomAction, ScheduleLayer}; use crate::schedule_entry::{GraphScheduleRequest, schedule_from_graph}; use crate::stats::{CompactionConfig, EffectiveConfig, NaStats, StageTimingsUs}; @@ -222,33 +223,24 @@ fn schedule_expanded( let mut report = build_resource_report(&req.layers, Some(&code_blocks), None)?; // Distance ownership: `with_code_blocks` via `CodeFamily::distance()`. report.memory_rounds = Some(expanded.memory_round_count() as u64); - // Magic-state-consuming logical operation counts (issue #283). + // Magic-state-consuming logical operation counts (issue #283). These + // stay here: the flat pipeline has no workload to size them from. report.t_count = Some(expanded.t_count() as u64); report.tdag_count = Some(expanded.tdag_count() as u64); report.ccz_count = Some(expanded.ccz_count() as u64); report.magic_state_demand = Some(expanded.magic_state_demand() as u64); - let report = match na.error_model.as_ref() { - Some(model) => attach_qec_error_budget(report, Some(model))?, - None => report, - }; - // Analytic end-to-end fidelity estimate (Enola Eq. (1), issue #305) — - // same unconditional overlay as the flat/zoned pipeline - // (`pipeline::finish_pipeline`); `NeutralAtomTarget::fidelity` is - // mandatory, so this always applies once a target is available. - let report = report.with_fidelity_estimate(&req.layers, &na.fidelity); - // Analytic per-atom movement-heating / atom-loss budget (issue #310, - // \[Atomique\] Eqs. (1)–(2)). Optional like `error_model`: attached only - // when the target carries `atom_loss_model`; distance measured against - // the zoned schedule's layout (real √-law travel), else zeroed/omitted. - let report = match na.atom_loss_model.as_ref() { - Some(model) => report.with_atom_loss_budget(&req.layers, req.layout.as_ref(), model), - None => report, - }; - let report = report.with_agnostic_placer_mechanism(stage_acc.agnostic_placer_mechanism); - let report = match stage_acc.schedule_optimality { - Some(optimality) => report.with_schedule_optimality(optimality), - None => report, - }; + // Shared with `pipeline::finish_pipeline` (issue #468). + let report = overlay_target_report( + report, + &TargetReportOverlay { + layers: &req.layers, + layout: req.layout.as_ref(), + target: na, + aware_search: None, + agnostic_placer_mechanism: stage_acc.agnostic_placer_mechanism, + schedule_optimality: stage_acc.schedule_optimality, + }, + )?; let resource_report_us = elapsed_us(stage_started); let req = project_request_to_logical(req); diff --git a/quon_na/src/report.rs b/quon_na/src/report.rs index 0b9efb0..9ecf0aa 100644 --- a/quon_na/src/report.rs +++ b/quon_na/src/report.rs @@ -13,7 +13,10 @@ //! are **analytic** (ADR-0020) and distinct from `error_budget` — see //! [`ResourceReport::with_fidelity_estimate`]. -use backend::{BackendError, NeutralAtomErrorModel, NeutralAtomFidelity, NeutralAtomLossModel}; +use backend::{ + BackendError, NeutralAtomErrorModel, NeutralAtomFidelity, NeutralAtomLossModel, + NeutralAtomTarget, +}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use thiserror::Error; @@ -376,13 +379,14 @@ pub struct ResourceReport { // `None` until [`ResourceReport::with_fidelity_estimate`] is called with a // target's [`NeutralAtomFidelity`] — the same "None until overlaid" // convention `error_budget` uses (ADR-0017 §11.4), not a per-field schema - // migration marker. In production, `quon_na::pipeline::finish_pipeline` - // and `quon_na::qec_schedule::schedule_expanded` always call the overlay - // when a `NeutralAtomTarget` is available (its `fidelity` field is - // mandatory, unlike the optional `error_model`), so both fields are - // `Some` on every `--emit-resource-report` output; only reports built - // directly from `ResourceReport::from_layers` / `build_resource_report` - // without that overlay (library use, most unit tests) leave them unset. + // migration marker. In production, `overlay_target_report` always applies + // the fidelity overlay when a `NeutralAtomTarget` is available (its + // `fidelity` field is mandatory, unlike the optional `error_model`). The + // bare pipeline and the hybrid QEC schedule both call that function, so + // both fields are `Some` on every `--emit-resource-report` output; only + // reports built directly from `ResourceReport::from_layers` / + // `build_resource_report` without that overlay (library use, most unit + // tests) leave them unset. // // **Analytic estimate, not a logical error rate or threshold claim** // (ADR-0020) — same discipline as `error_budget`, kept as a visibly @@ -423,10 +427,10 @@ pub struct ResourceReport { // `None` until [`ResourceReport::with_atom_loss_budget`] is overlaid with // a target's [`NeutralAtomLossModel`] — the same "None until overlaid" // convention `error_budget` uses (ADR-0017 §11.4). In production, - // `quon_na::pipeline::finish_pipeline` and - // `quon_na::qec_schedule::schedule_expanded` attach it whenever the - // target carries `atom_loss_model` (its presence is optional, unlike the - // mandatory `fidelity`); reports built directly from `from_layers` / + // `overlay_target_report` attaches it whenever the target carries + // `atom_loss_model` (its presence is optional, unlike the mandatory + // `fidelity`). The bare pipeline and the hybrid QEC schedule both call + // that function; reports built directly from `from_layers` / // `build_resource_report` without that overlay leave it `None`. // // **Analytic, not Monte Carlo** (ADR-0020): a per-atom `heating → loss` @@ -1142,6 +1146,74 @@ pub fn attach_qec_error_budget( Ok(report.with_error_budget(model)) } +/// Borrowed inputs for [`overlay_target_report`]. +/// +/// The schedule layers and layout are the ones [`build_resource_report`] +/// already counted. Path-specific diagnostics are optional: the bare +/// pipeline fills `aware_search`; both pipelines may fill the placer +/// mechanism and schedule-optimality label. QEC workload counts +/// (`memory_rounds`, `t_count`, and the magic-state counters) are not +/// inputs — the hybrid caller sets those on the report before this overlay. +#[derive(Clone, Copy, Debug)] +pub struct TargetReportOverlay<'a> { + /// Compiled schedule the report was built from. + pub layers: &'a [ScheduleLayer], + /// Zoned site map. `None` zero-fills an atom-loss budget when a loss + /// model is present, matching the previous per-path overlay. + pub layout: Option<&'a NeutralAtomLayout>, + /// Target whose fidelity, error model, and atom-loss model drive the + /// analytic sections. + pub target: &'a NeutralAtomTarget, + /// Routing-aware search counts. `None` leaves the aware-search fields + /// untouched (the hybrid QEC path never sets them). + pub aware_search: Option<(u64, u64)>, + /// Routing-agnostic placer label. Always written, including `None`. + pub agnostic_placer_mechanism: Option, + /// Exact-vs-heuristic label. `None` leaves the field untouched. + pub schedule_optimality: Option, +} + +/// Shared production report-overlay stage. +/// +/// Both the bare pipeline (`finish_pipeline`) and the hybrid QEC schedule +/// (`schedule_expanded`) call this after [`build_resource_report`]. One +/// place applies the analytic sections those paths used to duplicate: +/// +/// 1. aware-search status, when `aware_search` is `Some` +/// 2. schedule-optimality label, when `schedule_optimality` is `Some` +/// 3. routing-agnostic placer mechanism (always, including `None`) +/// 4. physical error budget, when the target carries `error_model` +/// 5. end-to-end fidelity estimate (`fidelity` is mandatory on the target) +/// 6. atom-loss budget, when the target carries `atom_loss_model` +/// +/// Those writes touch disjoint fields, so this order yields the same report +/// as either previous inline chain. Workload counts set on `report` before +/// the call are left in place. +pub fn overlay_target_report( + report: ResourceReport, + overlay: &TargetReportOverlay<'_>, +) -> Result { + let report = match overlay.aware_search { + Some((completed, fell_back)) => report.with_aware_search_status(completed, fell_back), + None => report, + }; + let report = match overlay.schedule_optimality { + Some(optimality) => report.with_schedule_optimality(optimality), + None => report, + }; + let report = report.with_agnostic_placer_mechanism(overlay.agnostic_placer_mechanism); + let report = match overlay.target.error_model.as_ref() { + Some(model) => attach_qec_error_budget(report, Some(model))?, + None => report, + }; + let report = report.with_fidelity_estimate(overlay.layers, &overlay.target.fidelity); + let report = match overlay.target.atom_loss_model.as_ref() { + Some(model) => report.with_atom_loss_budget(overlay.layers, overlay.layout, model), + None => report, + }; + Ok(report) +} + /// Resolve a target's error model for QEC error artifacts. /// /// Maps [`BackendError::MissingErrorModel`] only — call sites match that @@ -1485,7 +1557,10 @@ pub fn resource_report_to_markdown(report: &ResourceReport) -> String { #[cfg(test)] mod tests { - use backend::{BackendError, NeutralAtomErrorModel, NeutralAtomFidelity, NeutralAtomLossModel}; + use backend::{ + BackendError, NeutralAtomErrorModel, NeutralAtomFidelity, NeutralAtomLossModel, + NeutralAtomTarget, + }; use serde_json::json; use super::*; @@ -2947,4 +3022,121 @@ mod tests { insta::assert_snapshot!("atom_loss_budget_section_json", json); insta::assert_snapshot!("atom_loss_budget_section_md", md); } + + fn load_generic_rna() -> NeutralAtomTarget { + let path = std::path::Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../targets/neutral_atom/generic_rna_v0.json" + )); + let loaded = backend::json::load(path).expect("load target"); + loaded.neutral_atom_target().expect("na target").clone() + } + + /// The shared overlay matches the bare pipeline's previous inline order + /// and the hybrid QEC schedule's previous inline order. Those orders + /// differed; the fields they write do not overlap. + #[test] + fn overlay_target_report_matches_both_previous_chains() { + let mut na = load_generic_rna(); + na.atom_loss_model = Some(example_loss_model()); + let layout = loss_layout(); + let layers = vec![ + move_layer(atom(0), site(0), site(1)), + move_layer(atom(1), site(2), site(3)), + ]; + let mut base = ResourceReport::from_layers(&layers); + base.memory_rounds = Some(3); + base.t_count = Some(1); + base.tdag_count = Some(2); + base.ccz_count = Some(0); + base.magic_state_demand = Some(1); + + let shared = overlay_target_report( + base.clone(), + &TargetReportOverlay { + layers: &layers, + layout: Some(&layout), + target: &na, + aware_search: Some((4, 1)), + agnostic_placer_mechanism: Some(AgnosticPlacerMechanism::Matching), + schedule_optimality: Some(ScheduleOptimality::Heuristic), + }, + ) + .expect("overlay"); + + // Bare pipeline order: diagnostics, then error budget, fidelity, loss. + let bare = { + let report = base + .clone() + .with_aware_search_status(4, 1) + .with_schedule_optimality(ScheduleOptimality::Heuristic) + .with_agnostic_placer_mechanism(Some(AgnosticPlacerMechanism::Matching)); + let report = attach_qec_error_budget(report, na.error_model.as_ref()).expect("budget"); + let report = report.with_fidelity_estimate(&layers, &na.fidelity); + let model = na.atom_loss_model.as_ref().expect("loss model"); + report.with_atom_loss_budget(&layers, Some(&layout), model) + }; + assert_eq!(shared, bare); + + // Hybrid QEC order: error budget, fidelity, loss, then diagnostics. + // Aware-search stays unset on that path. + let qec = { + let report = + attach_qec_error_budget(base.clone(), na.error_model.as_ref()).expect("budget"); + let report = report.with_fidelity_estimate(&layers, &na.fidelity); + let model = na.atom_loss_model.as_ref().expect("loss model"); + report + .with_atom_loss_budget(&layers, Some(&layout), model) + .with_agnostic_placer_mechanism(Some(AgnosticPlacerMechanism::Matching)) + .with_schedule_optimality(ScheduleOptimality::Heuristic) + }; + let qec_shared = overlay_target_report( + base, + &TargetReportOverlay { + layers: &layers, + layout: Some(&layout), + target: &na, + aware_search: None, + agnostic_placer_mechanism: Some(AgnosticPlacerMechanism::Matching), + schedule_optimality: Some(ScheduleOptimality::Heuristic), + }, + ) + .expect("qec overlay"); + assert_eq!(qec_shared, qec); + assert_eq!(qec_shared.memory_rounds, Some(3)); + assert_eq!(qec_shared.t_count, Some(1)); + assert_eq!(qec_shared.magic_state_demand, Some(1)); + assert_eq!(qec_shared.aware_search_completed_layers, None); + assert!(qec_shared.error_budget.is_some()); + assert!(qec_shared.gate_fidelity_product.is_some()); + assert!(qec_shared.estimated_fidelity.is_some()); + let loss = qec_shared.atom_loss_budget.expect("loss section"); + assert!(loss.expected_atoms_lost > 0.0); + } + + #[test] + fn overlay_target_report_omits_optional_sections_without_models() { + let mut na = load_generic_rna(); + na.error_model = None; + na.atom_loss_model = None; + let layers = toy_layers(); + let report = overlay_target_report( + ResourceReport::from_layers(&layers), + &TargetReportOverlay { + layers: &layers, + layout: None, + target: &na, + aware_search: None, + agnostic_placer_mechanism: None, + schedule_optimality: None, + }, + ) + .expect("overlay"); + assert!(report.error_budget.is_none()); + assert!(report.atom_loss_budget.is_none()); + assert!(report.gate_fidelity_product.is_some()); + assert!(report.estimated_fidelity.is_some()); + assert!(report.agnostic_placer_mechanism.is_none()); + assert!(report.schedule_optimality.is_none()); + } }