From b308346bf55775e2f51254ee3e8e0c2f85f285e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 1 Oct 2026 18:51:13 +0000 Subject: [PATCH] [na] Default omitted cost_model weights An omitted cost_model weight loads as the placeholder from the checked-in neutral-atom target: stage, movement, and transfer at 1, idle at 1e-6. An explicit weight is left alone. The test locks that fill-in. Refs #484 Co-authored-by: Arnab Ghosh --- backend/src/descriptor.rs | 34 +++++++++++++++++++++++ backend/src/target.rs | 13 +++++++++ backend/tests/target.rs | 58 +++++++++++++++++++++++++++++++++++++++ quon_na/src/objective.rs | 7 +---- 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/backend/src/descriptor.rs b/backend/src/descriptor.rs index 43c2d2e..2f68625 100644 --- a/backend/src/descriptor.rs +++ b/backend/src/descriptor.rs @@ -133,6 +133,8 @@ pub struct NeutralAtomTargetDescriptor { /// still load and the report simply skips the `atom_loss_budget` section. #[serde(default, skip_serializing_if = "Option::is_none")] pub atom_loss_model: Option, + /// Linear §9 weights. Omitted fields use [`NeutralAtomCostModel::PLACEHOLDER`]. + #[serde(default)] pub cost_model: NeutralAtomCostModelDescriptor, } @@ -252,15 +254,47 @@ pub struct NeutralAtomLossModelDescriptor { pub loss_coeff: f64, } +fn default_rydberg_stage_weight() -> f64 { + NeutralAtomCostModel::PLACEHOLDER.rydberg_stage_weight +} + +fn default_movement_time_weight() -> f64 { + NeutralAtomCostModel::PLACEHOLDER.movement_time_weight +} + +fn default_trap_transfer_weight() -> f64 { + NeutralAtomCostModel::PLACEHOLDER.trap_transfer_weight +} + +fn default_idle_time_weight() -> f64 { + NeutralAtomCostModel::PLACEHOLDER.idle_time_weight +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NeutralAtomCostModelDescriptor { + #[serde(default = "default_rydberg_stage_weight")] pub rydberg_stage_weight: f64, + #[serde(default = "default_movement_time_weight")] pub movement_time_weight: f64, + #[serde(default = "default_trap_transfer_weight")] pub trap_transfer_weight: f64, + #[serde(default = "default_idle_time_weight")] pub idle_time_weight: f64, } +impl Default for NeutralAtomCostModelDescriptor { + fn default() -> Self { + let weights = NeutralAtomCostModel::PLACEHOLDER; + Self { + rydberg_stage_weight: weights.rydberg_stage_weight, + movement_time_weight: weights.movement_time_weight, + trap_transfer_weight: weights.trap_transfer_weight, + idle_time_weight: weights.idle_time_weight, + } + } +} + impl TryFrom for BackendTarget { type Error = BackendError; diff --git a/backend/src/target.rs b/backend/src/target.rs index de6a951..48decb1 100644 --- a/backend/src/target.rs +++ b/backend/src/target.rs @@ -670,6 +670,19 @@ pub struct NeutralAtomCostModel { pub idle_time_weight: f64, } +impl NeutralAtomCostModel { + /// Placeholder §9 weights from architecture_model.md §8.6 / §9. + /// + /// These match `targets/neutral_atom/generic_rna_v0.json`. An omitted + /// `cost_model` weight loads as the corresponding field here. + pub const PLACEHOLDER: Self = Self { + rydberg_stage_weight: 1.0, + movement_time_weight: 1.0, + trap_transfer_weight: 1.0, + idle_time_weight: 0.000001, + }; +} + /// True iff `q` is a valid qubit index for a device with `n` qubits. /// /// Refinement-typed: Flux proves the boolean result equals `q < n`, anchoring diff --git a/backend/tests/target.rs b/backend/tests/target.rs index c5d8501..eb20748 100644 --- a/backend/tests/target.rs +++ b/backend/tests/target.rs @@ -697,6 +697,64 @@ fn neutral_atom_all_zero_cost_weights_are_rejected() { assert_eq!(na.movement_time_weight, 0.0); } +#[test] +fn omitted_optional_cost_weights_keep_documented_defaults() { + let placeholder = backend::NeutralAtomCostModel::PLACEHOLDER; + let sample = json::from_str(&neutral_sample_json()).expect("checked-in target"); + let sample_cost = sample + .neutral_atom_target() + .expect("neutral atom") + .cost_model; + assert_eq!( + sample_cost, placeholder, + "generic_rna_v0.json is the documented placeholder vector" + ); + + let mut partial = neutral_sample_value(); + let model = partial + .get_mut("cost_model") + .and_then(|v| v.as_object_mut()) + .expect("cost_model object"); + model.insert("rydberg_stage_weight".into(), serde_json::json!(4.0)); + model.remove("movement_time_weight"); + model.remove("trap_transfer_weight"); + model.remove("idle_time_weight"); + let loaded = json::from_str(&partial.to_string()).expect("omitted weights load"); + let cost = loaded + .neutral_atom_target() + .expect("neutral atom") + .cost_model; + assert_eq!(cost.rydberg_stage_weight, 4.0); + assert_eq!(cost.movement_time_weight, placeholder.movement_time_weight); + assert_eq!(cost.trap_transfer_weight, placeholder.trap_transfer_weight); + assert_eq!(cost.idle_time_weight, placeholder.idle_time_weight); + + let mut empty = neutral_sample_value(); + empty["cost_model"] = serde_json::json!({}); + let loaded = json::from_str(&empty.to_string()).expect("empty cost_model loads"); + assert_eq!( + loaded + .neutral_atom_target() + .expect("neutral atom") + .cost_model, + placeholder + ); + + let mut missing = neutral_sample_value(); + missing + .as_object_mut() + .expect("object") + .remove("cost_model"); + let loaded = json::from_str(&missing.to_string()).expect("omitted cost_model loads"); + assert_eq!( + loaded + .neutral_atom_target() + .expect("neutral atom") + .cost_model, + placeholder + ); +} + #[test] fn neutral_atom_error_model_rejects_negative_probability() { let src = with_error_model_mutated(|m| { diff --git a/quon_na/src/objective.rs b/quon_na/src/objective.rs index 1acf797..873dbc0 100644 --- a/quon_na/src/objective.rs +++ b/quon_na/src/objective.rs @@ -29,12 +29,7 @@ use crate::geometry::{SpeedModel, movement_duration_for_model}; /// architecture_model.md §8.6 / §9: these are tuning knobs, not measurements. /// The time objective and error-budget mode both score with the target's /// `cost_model`, including when that vector is exactly this placeholder. -pub const PLACEHOLDER_COST_WEIGHTS: NeutralAtomCostModel = NeutralAtomCostModel { - rydberg_stage_weight: 1.0, - movement_time_weight: 1.0, - trap_transfer_weight: 1.0, - idle_time_weight: 0.000001, -}; +pub const PLACEHOLDER_COST_WEIGHTS: NeutralAtomCostModel = NeutralAtomCostModel::PLACEHOLDER; /// One weighted objective, with every term that went into the total. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]