Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 30 additions & 18 deletions backend/src/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,24 +465,7 @@ fn neutral_atom_from_descriptor(
.atom_loss_model
.map(loss_model_from_descriptor)
.transpose()?,
cost_model: NeutralAtomCostModel {
rydberg_stage_weight: non_negative_f64(
"cost_model.rydberg_stage_weight",
d.cost_model.rydberg_stage_weight,
)?,
movement_time_weight: non_negative_f64(
"cost_model.movement_time_weight",
d.cost_model.movement_time_weight,
)?,
trap_transfer_weight: non_negative_f64(
"cost_model.trap_transfer_weight",
d.cost_model.trap_transfer_weight,
)?,
idle_time_weight: non_negative_f64(
"cost_model.idle_time_weight",
d.cost_model.idle_time_weight,
)?,
},
cost_model: cost_model_from_descriptor(d.cost_model)?,
};

let entanglement_capacity = target.zone_capacity(ZoneKind::Entanglement);
Expand All @@ -495,6 +478,35 @@ fn neutral_atom_from_descriptor(
Ok(BackendTarget::neutral_atom_reconfigurable(d.id, target))
}

fn cost_model_from_descriptor(
d: NeutralAtomCostModelDescriptor,
) -> Result<NeutralAtomCostModel, BackendError> {
let rydberg_stage_weight =
non_negative_f64("cost_model.rydberg_stage_weight", d.rydberg_stage_weight)?;
let movement_time_weight =
non_negative_f64("cost_model.movement_time_weight", d.movement_time_weight)?;
let trap_transfer_weight =
non_negative_f64("cost_model.trap_transfer_weight", d.trap_transfer_weight)?;
let idle_time_weight = non_negative_f64("cost_model.idle_time_weight", d.idle_time_weight)?;
// Finite and non-negative is not enough: an all-zero vector scores every
// schedule the same, so placement ignores it.
if rydberg_stage_weight == 0.0
&& movement_time_weight == 0.0
&& trap_transfer_weight == 0.0
&& idle_time_weight == 0.0
{
return invalid_config(
"cost_model weights are all zero (rydberg_stage_weight, movement_time_weight, trap_transfer_weight, idle_time_weight); at least one must be positive so the objective can select a schedule",
);
}
Ok(NeutralAtomCostModel {
rydberg_stage_weight,
movement_time_weight,
trap_transfer_weight,
idle_time_weight,
})
}

fn zone_from_descriptor(d: NeutralAtomZoneDescriptor) -> Result<NeutralAtomZone, BackendError> {
let kind = d.kind;
let pair_gap_um = match (kind, d.pair_gap_um) {
Expand Down
38 changes: 38 additions & 0 deletions backend/tests/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,44 @@ fn neutral_atom_error_model_rejects_out_of_range_probability() {
assert!(err.to_string().contains("error_model.rydberg"));
}

fn with_cost_model(stage: f64, movement: f64, transfer: f64, idle: f64) -> String {
let mut value = neutral_sample_value();
let model = value
.get_mut("cost_model")
.and_then(|v| v.as_object_mut())
.expect("cost_model object");
model.insert("rydberg_stage_weight".into(), serde_json::json!(stage));
model.insert("movement_time_weight".into(), serde_json::json!(movement));
model.insert("trap_transfer_weight".into(), serde_json::json!(transfer));
model.insert("idle_time_weight".into(), serde_json::json!(idle));
value.to_string()
}

#[test]
fn neutral_atom_all_zero_cost_weights_are_rejected() {
let err = json::from_str(&with_cost_model(0.0, 0.0, 0.0, 0.0)).unwrap_err();
assert!(
matches!(err, BackendError::InvalidTargetConfig(_)),
"got {err:?}"
);
let msg = err.to_string();
assert!(msg.contains("all zero"), "{msg}");
assert!(msg.contains("rydberg_stage_weight"), "{msg}");
assert!(msg.contains("movement_time_weight"), "{msg}");
assert!(msg.contains("trap_transfer_weight"), "{msg}");
assert!(msg.contains("idle_time_weight"), "{msg}");
assert!(msg.contains("at least one must be positive"), "{msg}");

let loaded = json::from_str(&with_cost_model(1.0, 0.0, 0.0, 0.0))
.expect("one positive weight still loads");
let na = loaded
.neutral_atom_target()
.expect("neutral atom")
.cost_model;
assert_eq!(na.rydberg_stage_weight, 1.0);
assert_eq!(na.movement_time_weight, 0.0);
}

#[test]
fn neutral_atom_error_model_rejects_negative_probability() {
let src = with_error_model_mutated(|m| {
Expand Down
36 changes: 36 additions & 0 deletions quon_na/src/objective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ pub enum ObjectiveError {
"cost_model weights must be finite and non-negative (rydberg_stage_weight, movement_time_weight, trap_transfer_weight, idle_time_weight)"
)]
InvalidWeights,
/// Every schedule scores 0, so strict cost comparisons never move and the
/// vector does not select a schedule.
#[error(
"cost_model weights are all zero (rydberg_stage_weight, movement_time_weight, trap_transfer_weight, idle_time_weight); at least one must be positive so the objective can select a schedule"
)]
AllZeroWeights,
}

/// Movement, transfer, and idle cost of one gate orientation.
Expand Down Expand Up @@ -132,6 +138,9 @@ pub fn weighted_total(
if !weights_usable(weights) {
return Err(ObjectiveError::InvalidWeights);
}
if cost_weights_are_all_zero(weights) {
return Err(ObjectiveError::AllZeroWeights);
}
Ok(weights.rydberg_stage_weight * rydberg_stages as f64
+ weights.movement_time_weight * movement_time_us as f64
+ weights.trap_transfer_weight * trap_transfers as f64
Expand All @@ -149,6 +158,18 @@ fn weights_usable(weights: &NeutralAtomCostModel) -> bool {
.all(|weight| weight.is_finite() && weight >= 0.0)
}

/// True when every §9 weight is exactly zero.
///
/// Non-finite and negative values are a separate check ([`weights_usable`]).
/// An all-zero vector is finite and non-negative, but every schedule has the
/// same cost, so placement comparisons ignore it.
pub(crate) fn cost_weights_are_all_zero(weights: &NeutralAtomCostModel) -> bool {
weights.rydberg_stage_weight == 0.0
&& weights.movement_time_weight == 0.0
&& weights.trap_transfer_weight == 0.0
&& weights.idle_time_weight == 0.0
}

/// Objective of a schedule that has already passed verification.
///
/// Counts come from `spec` alone: Rydberg stages are entangle layers, movement
Expand Down Expand Up @@ -342,6 +363,21 @@ mod tests {
assert_eq!(err, ObjectiveError::InvalidWeights);
}

#[test]
fn all_zero_weights_are_rejected() {
let err = weighted_total(1, 1, 1, 1, &weights(0.0, 0.0, 0.0, 0.0)).expect_err("zero");
assert_eq!(err, ObjectiveError::AllZeroWeights);
let msg = err.to_string();
assert!(msg.contains("all zero"), "{msg}");
assert!(msg.contains("rydberg_stage_weight"), "{msg}");
assert!(msg.contains("movement_time_weight"), "{msg}");
assert!(msg.contains("trap_transfer_weight"), "{msg}");
assert!(msg.contains("idle_time_weight"), "{msg}");
assert!(msg.contains("at least one must be positive"), "{msg}");
let total = weighted_total(2, 10, 4, 100, &weights(0.0, 0.0, 0.0, 1.0)).expect("idle");
assert_eq!(total, 100.0);
}

#[cfg(feature = "mlir")]
#[test]
fn verified_spec_objective_counts_each_component() {
Expand Down
67 changes: 67 additions & 0 deletions quon_na/src/zoned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ pub enum ZonedScheduleError {
EmptySchedule,
#[error("schedule layer conflict: {0}")]
Conflict(String),
/// Every schedule scores 0, so the placer's strict comparisons never move.
#[error(
"cost_model weights are all zero (rydberg_stage_weight, movement_time_weight, trap_transfer_weight, idle_time_weight); at least one must be positive so the objective can select a schedule"
)]
AllZeroCostWeights,
}

/// √(d_max / a) duration contribution for one movement group (\[RAP\] Eq. (1)).
Expand Down Expand Up @@ -978,6 +983,11 @@ pub fn schedule_zoned_with_aware_params<V: VertexId>(
aware_search: AwareSearchParams,
cost_model: PlacementCostModel,
) -> Result<ZonedScheduleResult<V>, ZonedScheduleError> {
if let PlacementCostModel::Weighted { weights, .. } = cost_model {
if crate::objective::cost_weights_are_all_zero(&weights) {
return Err(ZonedScheduleError::AllZeroCostWeights);
}
}
arch.validate()?;
if req.layers.is_empty() {
// Synthesize one layer per commutation/ ASAP is caller's job; allow
Expand Down Expand Up @@ -3445,6 +3455,63 @@ mod tests {
toy_zoned_architecture().validate().expect("ok");
}

fn weighted_cost(stage: f64, movement: f64, transfer: f64, idle: f64) -> PlacementCostModel {
let arch = toy_zoned_architecture();
PlacementCostModel::Weighted {
weights: backend::NeutralAtomCostModel {
rydberg_stage_weight: stage,
movement_time_weight: movement,
trap_transfer_weight: transfer,
idle_time_weight: idle,
},
speed_model: arch.speed_model,
trap_transfer_us: arch.trap_transfer_us,
}
}

fn entangling_request() -> crate::schedule_entry::GraphScheduleRequest<LogicalQubitId> {
let graph = matching_graph(2);
let req = schedule_from_graph(graph).expect("stub");
schedule_entangling_layers(req, 340)
.expect("layers")
.request
}

#[test]
fn all_zero_cost_weights_fail_and_a_positive_weight_still_schedules() {
let arch = toy_zoned_architecture();
let err = schedule_zoned_with_aware_params(
entangling_request(),
&arch,
PlacerMode::RoutingAgnostic,
AwareSearchParams::default(),
weighted_cost(0.0, 0.0, 0.0, 0.0),
)
.expect_err("all-zero weights");
let msg = err.to_string();
assert!(msg.contains("all zero"), "{msg}");
assert!(msg.contains("rydberg_stage_weight"), "{msg}");
assert!(msg.contains("movement_time_weight"), "{msg}");
assert!(msg.contains("trap_transfer_weight"), "{msg}");
assert!(msg.contains("idle_time_weight"), "{msg}");
assert!(msg.contains("at least one must be positive"), "{msg}");

let result = schedule_zoned_with_aware_params(
entangling_request(),
&arch,
PlacerMode::RoutingAgnostic,
AwareSearchParams::default(),
weighted_cost(1.0, 0.0, 0.0, 0.0),
)
.expect("a positive stage weight still selects a schedule");
assert!(result.request.layers.iter().any(|layer| {
layer
.actions
.iter()
.any(|action| matches!(action, NeutralAtomAction::Entangle2 { .. }))
}));
}

#[test]
fn schedule_zoned_entangles_in_entanglement_zone() {
let graph = matching_graph(2);
Expand Down
Loading