From d36bc60d098095a4b4b3941af52155f08b1022ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 1 Oct 2026 11:46:25 +0000 Subject: [PATCH] [na] Refuse entangle merges inside isolation spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Order-only compaction merged disjoint entangle layers without checking replayed positions. A weighted stage split could put non-partner atoms 15.62 µm apart in one cycle, inside the 18.75 µm isolation spacing. Refuse that merge and keep the split. Pairs outside isolation still merge. The default time objective is unchanged. Refs #484 Co-authored-by: Arnab Ghosh --- quon_na/src/compaction.rs | 168 +++++++++++++++++++++++++++++++++++- quon_na/src/lib.rs | 1 + quon_na/src/pipeline.rs | 15 ++-- quon_na/src/qec_schedule.rs | 13 ++- quon_na/tests/compaction.rs | 118 ++++++++++++++++++++++++- 5 files changed, 296 insertions(+), 19 deletions(-) diff --git a/quon_na/src/compaction.rs b/quon_na/src/compaction.rs index 1230f8f4..e5711ff4 100644 --- a/quon_na/src/compaction.rs +++ b/quon_na/src/compaction.rs @@ -53,10 +53,16 @@ //! //! Position-aware R2/R3 runs **iff** `request.layout` **and** //! [`CompactionOptions::legality`] are both set (MLIR-free). Without `legality`, -//! geometry is **unchecked** even when a layout is present (AC2 / default opts +//! that static check is off even when a layout is present (AC2 / default opts //! may accept physically illegal E0 merges). Do **not** claim R2/R3 runs //! “whenever layout is present.” Zone re-validate is best-effort on static //! bindings for entangle-only merges. +//! +//! [`CompactionOptions::entangle_isolation_um`] is separate. When it is set, +//! an entangle merge is refused if replaying moves from the declared start +//! puts non-partner atoms inside that distance at the entangle cycle. Order-only +//! compaction sets it so a weighted stage split is not merged back into one +//! illegal entangle cycle. use std::collections::{BTreeMap, BTreeSet}; @@ -64,7 +70,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::graph::{InteractionId, LogicalQubitId, VertexId}; -use crate::layout::{AodTrapRef, AtomId, NeutralAtomLayout, Position, TrapBinding}; +use crate::layout::{AodTrapRef, AtomId, NeutralAtomLayout, Position, SiteId, TrapBinding}; use crate::schedule::{NeutralAtomAction, ScheduleLayer}; use crate::schedule_entry::GraphScheduleRequest; use crate::zoned::{ZonedArchitecture, validate_zone_constraints}; @@ -167,6 +173,26 @@ pub struct CompactionOptions { pub legality: Option, /// If true, run greedy compaction after ASAP; if false, ASAP-only baseline. pub greedy: bool, + /// When set, refuse an entangle merge that puts non-partner atoms within + /// this distance (µm) at the entangle cycle. Positions are replayed from + /// the declared start through earlier moves, not read from final occupancy. + /// `None` leaves the check off. Order-only compaction sets the target's + /// isolation spacing here without enabling static [`Self::legality`]. + pub entangle_isolation_um: Option, +} + +/// Greedy compaction with static final-layout R2/R3 left off. +/// +/// `min_rydberg_spacing_um` is checked at the entangle cycle after replaying +/// moves. A merge that would put non-partner atoms inside that spacing is +/// refused and the layers stay split. +pub fn order_only_compaction_options(min_rydberg_spacing_um: f64) -> CompactionOptions { + CompactionOptions { + arch: None, + legality: None, + greedy: true, + entangle_isolation_um: Some(min_rydberg_spacing_um), + } } /// Placeholder AOD ref emitted by #107 `schedule_zoned` (all zeros). @@ -856,6 +882,132 @@ fn distance_um(a: &Position, b: &Position) -> f64 { (dx * dx + dy * dy).sqrt() } +fn binding_site(binding: &crate::layout::AtomBinding) -> SiteId { + match binding.trap { + TrapBinding::Slm { site } | TrapBinding::Aod { site, .. } => site, + } +} + +fn site_position(layout: &NeutralAtomLayout, site: SiteId) -> Option { + layout + .sites + .iter() + .find(|s| s.id == site) + .map(|s| s.position) +} + +/// Positions after every move in a cycle strictly before `cycle`. +/// +/// Starts from `declared_initial_bindings` when the planner saved them. +/// Otherwise the layout's `initial_bindings` are the only positions available +/// (tests with no moves, and schedules that never rewrote occupancy). +fn positions_before_cycle( + layers: &[ScheduleLayer], + layout: &NeutralAtomLayout, + cycle: u32, +) -> BTreeMap { + let start = if layout.declared_initial_bindings.is_empty() { + layout.initial_bindings.as_slice() + } else { + layout.declared_initial_bindings.as_slice() + }; + let mut pos = BTreeMap::new(); + for binding in start { + if let Some(position) = site_position(layout, binding_site(binding)) { + pos.insert(binding.atom, position); + } + } + let mut earlier: Vec<&ScheduleLayer> = + layers.iter().filter(|layer| layer.cycle < cycle).collect(); + earlier.sort_by_key(|layer| layer.cycle); + for layer in earlier { + for action in &layer.actions { + let NeutralAtomAction::Move(group) = action else { + continue; + }; + for atom_move in &group.moves { + if let Some(position) = site_position(layout, atom_move.to) { + pos.insert(atom_move.atom, position); + } + } + } + } + pos +} + +fn entangle_partner_keys(layer: &ScheduleLayer) -> BTreeSet<(AtomId, AtomId)> { + let mut keys = BTreeSet::new(); + for action in &layer.actions { + match action { + NeutralAtomAction::Entangle2 { atoms, .. } => { + keys.insert(atom_pair_key(atoms[0], atoms[1])); + } + NeutralAtomAction::EntangleN { atoms, .. } => { + for i in 0..atoms.len() { + for j in (i + 1)..atoms.len() { + keys.insert(atom_pair_key(atoms[i], atoms[j])); + } + } + } + _ => {} + } + } + keys +} + +fn entangle_atoms(layer: &ScheduleLayer) -> Vec { + let mut atoms = Vec::new(); + for action in &layer.actions { + match action { + NeutralAtomAction::Entangle2 { atoms: pair, .. } => atoms.extend(pair), + NeutralAtomAction::EntangleN { atoms: group, .. } => { + atoms.extend(group.iter().copied()) + } + _ => {} + } + } + atoms +} + +/// True when the merged entangle layer puts non-partners inside `isolation_um`. +fn entangle_cycle_breaks_isolation( + sim_layers: &[ScheduleLayer], + union: &ScheduleLayer, + layout: &NeutralAtomLayout, + isolation_um: f64, +) -> bool { + let wanted = entangle_partner_keys(union); + if wanted.is_empty() { + return false; + } + let Some(layer) = sim_layers.iter().find(|layer| { + let keys = entangle_partner_keys(layer); + wanted.iter().all(|key| keys.contains(key)) + }) else { + return true; + }; + let positions = positions_before_cycle(sim_layers, layout, layer.cycle); + let partners = entangle_partner_keys(layer); + let atoms = entangle_atoms(layer); + for i in 0..atoms.len() { + for j in (i + 1)..atoms.len() { + let left = atoms[i]; + let right = atoms[j]; + if left == right || partners.contains(&atom_pair_key(left, right)) { + continue; + } + let (Some(left_pos), Some(right_pos)) = (positions.get(&left), positions.get(&right)) + else { + return true; + }; + if distance_um(left_pos, right_pos) <= isolation_um { + return true; + } + } + } + false +} + fn atom_pair_key(a: AtomId, b: AtomId) -> (AtomId, AtomId) { if a <= b { (a, b) } else { (b, a) } } @@ -1121,6 +1273,18 @@ fn try_merge_pair( return Ok(MergeAttempt::HardFail(CompactionError::DependencyViolation)); } + if let Some(isolation_um) = opts.entangle_isolation_um { + if isolation_um.is_finite() && isolation_um > 0.0 { + if let Some(layout) = layout.as_ref() { + if layer_has_entangle(&union) + && entangle_cycle_breaks_isolation(&sim_layers, &union, layout, isolation_um) + { + return Ok(MergeAttempt::Skip); + } + } + } + } + Ok(MergeAttempt::Ok) } diff --git a/quon_na/src/lib.rs b/quon_na/src/lib.rs index a58f4b93..e3c13b78 100644 --- a/quon_na/src/lib.rs +++ b/quon_na/src/lib.rs @@ -72,6 +72,7 @@ pub use compaction::{ CompactionError, CompactionOptions, CompactionResult, CriticalPathReport, LegalityLimits, ScheduleDependency, ScheduleDependencyKind, asap_schedule_layers, compact_schedule, feed_forward_dependencies, force_merge_layers, infer_atom_dependencies, + order_only_compaction_options, }; pub use entangling_schedule::{ CapacityLayerError, EntanglingScheduleError, EntanglingScheduleResult, LayerUtilization, diff --git a/quon_na/src/pipeline.rs b/quon_na/src/pipeline.rs index efcece00..746d886e 100644 --- a/quon_na/src/pipeline.rs +++ b/quon_na/src/pipeline.rs @@ -19,6 +19,7 @@ use thiserror::Error; use crate::compaction::{ CompactionError, CompactionOptions, LegalityLimits, compact_schedule, infer_atom_dependencies, + order_only_compaction_options, }; use crate::entangling_schedule::schedule_entangling_layers; use crate::graph::{InteractionGraph, LogicalQubitId}; @@ -300,6 +301,7 @@ pub fn compaction_options(na: &NeutralAtomTarget, greedy: bool) -> CompactionOpt aod_min_separation_um: na.movement.min_row_col_separation_um, }), greedy, + entangle_isolation_um: Some(na.interaction.min_rydberg_spacing_um), } } @@ -703,14 +705,11 @@ fn finish_pipeline( let mut compaction_us = None; if opts.compact && !req.layers.is_empty() { let deps = infer_atom_dependencies(&req.layers); - // After movement, layout bindings are final occupancy — static R2/R3 - // against that layout is not meaningful for earlier layers. Order-only - // compaction still fail-closes on software/dependency errors. - let compact_opts = CompactionOptions { - arch: None, - legality: None, - greedy: true, - }; + // Final occupancy is not the position at an earlier entangle cycle, so + // static R2/R3 stays off. Isolation is checked by replaying moves up + // to that cycle; a merge that would put non-partners inside it is + // refused and the stage split stays. + let compact_opts = order_only_compaction_options(na.interaction.min_rydberg_spacing_um); let stage_started = Instant::now(); let compacted = compact_schedule(req.clone(), &deps, &compact_opts)?; compaction_us = Some(elapsed_us(stage_started)); diff --git a/quon_na/src/qec_schedule.rs b/quon_na/src/qec_schedule.rs index a972a19f..e10adb9a 100644 --- a/quon_na/src/qec_schedule.rs +++ b/quon_na/src/qec_schedule.rs @@ -17,8 +17,8 @@ use quon_qec::{ }; use crate::compaction::{ - CompactionError, CompactionOptions, ScheduleDependency, ScheduleDependencyKind, - compact_schedule, feed_forward_dependencies, infer_atom_dependencies, + CompactionError, ScheduleDependency, ScheduleDependencyKind, compact_schedule, + feed_forward_dependencies, infer_atom_dependencies, }; use crate::entangling_schedule::schedule_entangling_layers; #[cfg(feature = "solver")] @@ -199,11 +199,8 @@ fn schedule_expanded( kind: ScheduleDependencyKind::Barrier, }); } - let compact_opts = CompactionOptions { - arch: None, - legality: None, - greedy: true, - }; + let compact_opts = + crate::compaction::order_only_compaction_options(na.interaction.min_rydberg_spacing_um); let stage_started = Instant::now(); let compacted = compact_schedule(req.clone(), &deps, &compact_opts)?; compaction_us = Some(elapsed_us(stage_started)); @@ -745,6 +742,7 @@ fn all_physical_atoms(expanded: &ExpandedWorkload) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::compaction::CompactionOptions; use crate::schedule::LocalGateKind; use quon_qec::{LogicalBasis, SourceFamily, WorkloadBuilder}; @@ -1171,6 +1169,7 @@ mod tests { arch: None, legality: None, greedy: true, + entangle_isolation_um: None, }; let without = compact_schedule(synthetic.clone(), &[], &opts).expect("without cuts"); let mut with_deps = Vec::new(); diff --git a/quon_na/tests/compaction.rs b/quon_na/tests/compaction.rs index 604f6e84..a3345e9b 100644 --- a/quon_na/tests/compaction.rs +++ b/quon_na/tests/compaction.rs @@ -9,8 +9,8 @@ use quon_na::{ LogicalQubitId, MeasurementBasis, MovementGroup, NeutralAtomAction, NeutralAtomLayout, Position, ScheduleDependency, ScheduleDependencyKind, ScheduleLayer, SiteId, TransferDirection, TrapBinding, TrapTransfer, ZoneKind, asap_schedule_layers, compact_schedule, - feed_forward_dependencies, force_merge_layers, infer_atom_dependencies, schedule_from_graph, - toy_zoned_architecture, + feed_forward_dependencies, force_merge_layers, infer_atom_dependencies, + order_only_compaction_options, schedule_from_graph, toy_zoned_architecture, }; /// Placeholder AOD ref emitted by #107 `schedule_zoned` (all zeros). @@ -632,6 +632,7 @@ fn zoned_entangle_only_passthrough() { greedy: true, arch: Some(arch), legality: Some(default_legality()), + entangle_isolation_um: None, }; let result = compact_schedule(req, &[], &opts).expect("compact"); assert_eq!(result.compacted_makespan_cycles, 1); @@ -991,3 +992,116 @@ fn classify_merge_still_forbids_global_ry_layers() { (got {result:?})" ); } + +/// Two gates already sit on entanglement pairs 15.62 µm apart, inside the +/// 18.75 µm isolation spacing, so a weighted stage split keeps them in +/// separate entangle cycles. Order-only compaction must not merge those +/// cycles. Pairs 200 µm apart still merge. +#[test] +fn order_only_compaction_keeps_isolated_stage_split() { + let isolation_um = 18.75; + let diagonal_um = (12.0_f64 * 12.0 + 10.0 * 10.0).sqrt(); + assert!(diagonal_um <= isolation_um); + let opts = order_only_compaction_options(isolation_um); + + let close = parked_pair_schedule(0.0, 310.0, 12.0, 320.0); + let close_deps = infer_atom_dependencies(&close.layers); + let close_result = compact_schedule(close, &close_deps, &opts).expect("compact close"); + let close_cycles = entangle_cycles(&close_result.request.layers); + assert_eq!( + close_cycles.len(), + 2, + "stage split inside isolation must stay two entangle cycles, got {close_cycles:?}" + ); + assert_ne!(close_cycles[0], close_cycles[1]); + assert_verified(&close_result.request); + + let far = parked_pair_schedule(0.0, 310.0, 200.0, 310.0); + let far_deps = infer_atom_dependencies(&far.layers); + let far_result = compact_schedule(far, &far_deps, &opts).expect("compact far"); + let far_cycles = entangle_cycles(&far_result.request.layers); + assert_eq!( + far_cycles.len(), + 1, + "pairs outside isolation may share an entangle cycle, got {far_cycles:?}" + ); + assert_verified(&far_result.request); + + fn parked_pair_schedule(x0: f64, y0: f64, x1: f64, y1: f64) -> GraphScheduleRequest { + let sites = vec![ + site(0, x0, y0), + site(1, x0 + 2.0, y0), + site(2, x1, y1), + site(3, x1 + 2.0, y1), + ]; + let bindings = (0..4) + .map(|atom| AtomBinding { + atom: AtomId(atom), + trap: TrapBinding::Slm { site: SiteId(atom) }, + }) + .collect(); + let mut req = empty_req(4); + req.layers = vec![ + layer(0, vec![entangle(0, 1)]), + layer(1, vec![entangle(2, 3)]), + ]; + req.layout = Some(NeutralAtomLayout { + sites, + initial_bindings: bindings, + declared_initial_bindings: Vec::new(), + }); + req + } + + fn site(id: u32, x_um: f64, y_um: f64) -> AtomSite { + AtomSite { + id: SiteId(id), + position: Position { x_um, y_um }, + } + } + + fn entangle_cycles(layers: &[ScheduleLayer]) -> Vec { + layers + .iter() + .filter(|layer| { + layer + .actions + .iter() + .any(|action| matches!(action, NeutralAtomAction::Entangle2 { .. })) + }) + .map(|layer| layer.cycle) + .collect() + } +} + +#[cfg(feature = "mlir")] +fn assert_verified(request: &GraphScheduleRequest) { + use quon_na::{ + DeclaredArchitecture, ScheduleLowerParams, lower_schedule, verify_emitted_schedule, + }; + let layout = request.layout.as_ref().expect("layout"); + let spec = lower_schedule( + request, + &ScheduleLowerParams { + target_id: "stage-split-isolation".into(), + rydberg_range_um: 7.5, + min_rydberg_spacing_um: 18.75, + aod_min_separation_um: 2.0, + }, + ) + .expect("lower"); + let declared = DeclaredArchitecture { + sites: layout.sites.clone(), + initial_bindings: layout.initial_bindings.clone(), + zones: Vec::new(), + check_zones: false, + require_readout_zone: false, + rydberg_range_um: 7.5, + min_rydberg_spacing_um: 18.75, + aod_min_separation_um: 2.0, + }; + verify_emitted_schedule(&spec, &declared).expect("verified schedule"); +} + +#[cfg(not(feature = "mlir"))] +fn assert_verified(_request: &GraphScheduleRequest) {}