use serde::{Deserialize, Serialize};
use crate::error::{check_positive_finite, Result, SdkError};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum GateDecision {
HardPass,
AcceptedByDescent { delta_v: f64 },
RejectedNonDescending { delta_v: f64 },
StoppedAtDeclaredFloor,
ExhaustedWithCertificate { certificate_id: String },
}
impl GateDecision {
pub fn is_accepted(&self) -> bool {
matches!(
self,
GateDecision::HardPass | GateDecision::AcceptedByDescent { .. }
)
}
}
pub fn evaluate_gate(
hard_pass: bool,
candidate_v: f64,
best_accepted_v: f64,
rho_gate: f64,
) -> Result<GateDecision> {
check_positive_finite(rho_gate, "rho_gate")?;
crate::error::check_non_negative_finite(candidate_v, "candidate energy")?;
crate::error::check_non_negative_finite(best_accepted_v, "best accepted energy")?;
if hard_pass {
return Ok(GateDecision::HardPass);
}
let delta_v = best_accepted_v - candidate_v;
if candidate_v <= best_accepted_v - rho_gate {
Ok(GateDecision::AcceptedByDescent { delta_v })
} else {
Ok(GateDecision::RejectedNonDescending { delta_v })
}
}
pub fn finite_decision_bound(
baseline_energy: f64,
rho_gate: f64,
rejection_budget: u32,
) -> Result<u64> {
check_positive_finite(rho_gate, "rho_gate")?;
crate::error::check_non_negative_finite(baseline_energy, "baseline energy")?;
let descents = (baseline_energy / rho_gate).floor();
if !descents.is_finite() {
return Err(SdkError::InvalidGate(
"finite-decision bound overflow".into(),
));
}
Ok(descents as u64 + rejection_budget as u64 + 1)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GateDecisionRef {
pub decision: GateDecision,
pub observed_energy: f64,
pub best_accepted_before: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AcceptedTrajectory {
pub node_id: String,
pub generation: u32,
pub baseline_energy: f64,
pub best_accepted_energy: f64,
pub rho_gate: f64,
pub rejection_budget: u32,
pub rejections_used: u32,
pub gate_decisions: Vec<GateDecisionRef>,
}
impl AcceptedTrajectory {
pub fn new(
node_id: impl Into<String>,
generation: u32,
baseline_energy: f64,
rho_gate: f64,
rejection_budget: u32,
) -> Result<Self> {
check_positive_finite(rho_gate, "rho_gate")?;
crate::error::check_non_negative_finite(baseline_energy, "baseline energy")?;
Ok(Self {
node_id: node_id.into(),
generation,
baseline_energy,
best_accepted_energy: baseline_energy,
rho_gate,
rejection_budget,
rejections_used: 0,
gate_decisions: Vec::new(),
})
}
pub fn decision_bound(&self) -> Result<u64> {
finite_decision_bound(self.baseline_energy, self.rho_gate, self.rejection_budget)
}
pub fn submit(&mut self, hard_pass: bool, candidate_v: f64) -> Result<GateDecision> {
let decision = evaluate_gate(
hard_pass,
candidate_v,
self.best_accepted_energy,
self.rho_gate,
)?;
self.gate_decisions.push(GateDecisionRef {
decision: decision.clone(),
observed_energy: candidate_v,
best_accepted_before: self.best_accepted_energy,
});
if decision.is_accepted() {
if candidate_v < self.best_accepted_energy {
self.best_accepted_energy = candidate_v;
}
} else if matches!(decision, GateDecision::RejectedNonDescending { .. }) {
self.rejections_used += 1;
}
Ok(decision)
}
pub fn budget_exhausted(&self) -> bool {
self.rejections_used >= self.rejection_budget
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hard_pass_is_accepted() {
let d = evaluate_gate(true, 100.0, 0.0, 0.5).unwrap();
assert_eq!(d, GateDecision::HardPass);
assert!(d.is_accepted());
}
#[test]
fn descent_below_best_minus_rho_accepts() {
let d = evaluate_gate(false, 9.4, 10.0, 0.5).unwrap();
assert!(matches!(d, GateDecision::AcceptedByDescent { .. }));
assert!(d.is_accepted());
}
#[test]
fn insufficient_descent_rejected() {
let d = evaluate_gate(false, 9.6, 10.0, 0.5).unwrap();
assert!(matches!(d, GateDecision::RejectedNonDescending { .. }));
assert!(!d.is_accepted());
}
#[test]
fn descent_measured_against_best_not_latest() {
let mut traj = AcceptedTrajectory::new("n1", 0, 10.0, 0.5, 8).unwrap();
assert!(traj.submit(false, 5.0).unwrap().is_accepted());
assert_eq!(traj.best_accepted_energy, 5.0);
assert!(!traj.submit(false, 5.2).unwrap().is_accepted());
assert_eq!(traj.best_accepted_energy, 5.0);
}
#[test]
fn finite_decision_bound_formula() {
assert_eq!(finite_decision_bound(10.0, 0.5, 3).unwrap(), 24);
}
#[test]
fn rho_gate_must_be_positive() {
assert!(evaluate_gate(false, 1.0, 2.0, 0.0).is_err());
assert!(finite_decision_bound(10.0, 0.0, 1).is_err());
}
#[test]
fn trajectory_terminates_within_bound() {
let mut traj = AcceptedTrajectory::new("n1", 0, 5.0, 1.0, 3).unwrap();
let bound = traj.decision_bound().unwrap(); let mut decisions = 0u64;
let mut v: f64 = 5.0;
while v > 0.0 {
v = (v - 1.0).max(0.0);
traj.submit(false, v).unwrap();
decisions += 1;
}
assert!(decisions <= bound);
}
}