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
34 changes: 34 additions & 0 deletions backend/src/descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NeutralAtomLossModelDescriptor>,
/// Linear §9 weights. Omitted fields use [`NeutralAtomCostModel::PLACEHOLDER`].
#[serde(default)]
pub cost_model: NeutralAtomCostModelDescriptor,
}

Expand Down Expand Up @@ -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<TargetDescriptor> for BackendTarget {
type Error = BackendError;

Expand Down
13 changes: 13 additions & 0 deletions backend/src/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions backend/tests/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand Down
7 changes: 1 addition & 6 deletions quon_na/src/objective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Loading