use std::collections::HashMap;
use gam_solve::structure_search::{MoveVerdict, SearchLedger, StructureMove};
const LN_2: f64 = std::f64::consts::LN_2;
#[inline]
#[must_use]
pub fn bits_from_nats(nats: f64) -> f64 {
nats / LN_2
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MoveStage {
Residual,
Linear,
Curved,
}
impl MoveStage {
#[must_use]
pub fn code(self) -> u64 {
match self {
MoveStage::Residual => 0,
MoveStage::Linear => 1,
MoveStage::Curved => 2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BirthSeed {
ResidualFactor,
LinearAtom,
CurvedChart,
PrincipalComponent,
}
impl BirthSeed {
#[must_use]
pub fn is_pc_reseed(self) -> bool {
matches!(self, BirthSeed::PrincipalComponent)
}
#[must_use]
pub fn code(self) -> u64 {
match self {
BirthSeed::ResidualFactor => 0,
BirthSeed::LinearAtom => 1,
BirthSeed::CurvedChart => 2,
BirthSeed::PrincipalComponent => 3,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MoveEvidence {
pub reml_delta: f64,
pub rank_charge: f64,
pub dl_bits: f64,
}
impl MoveEvidence {
#[must_use]
pub fn from_dl_bits(dl_bits: f64) -> Self {
Self {
reml_delta: f64::NAN,
rank_charge: 0.0,
dl_bits,
}
}
#[must_use]
pub fn from_log_e(log_e: f64) -> Self {
Self {
reml_delta: f64::NAN,
rank_charge: 0.0,
dl_bits: bits_from_nats(log_e),
}
}
#[must_use]
pub fn none() -> Self {
Self {
reml_delta: f64::NAN,
rank_charge: 0.0,
dl_bits: 0.0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SaeMove {
Birth { stage: MoveStage, seed: BirthSeed },
Death {
stage: MoveStage,
reason: MoveReason,
},
Refuse {
stage: MoveStage,
reason: MoveReason,
},
}
impl SaeMove {
#[must_use]
pub fn kind_code(&self) -> u64 {
match self {
SaeMove::Birth { .. } => 0,
SaeMove::Death { .. } => 1,
SaeMove::Refuse { .. } => 2,
}
}
#[must_use]
pub fn stage(&self) -> MoveStage {
match self {
SaeMove::Birth { stage, .. }
| SaeMove::Death { stage, .. }
| SaeMove::Refuse { stage, .. } => *stage,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MoveReason {
DeadRouting,
EvidenceInsufficient,
CertifiedVeto,
BudgetDeferred,
StaleOrDuplicate,
Custom(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct MigrationMove {
pub kind: SaeMove,
pub round: Option<usize>,
pub count: usize,
pub evidence: MoveEvidence,
pub objective: f64,
pub predicted_dl_bits: Option<f64>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SaeMigrationLedger {
pub moves: Vec<MigrationMove>,
pub pc_reseed_events: usize,
pub n_births: usize,
pub n_deaths: usize,
pub n_refusals: usize,
}
impl SaeMigrationLedger {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, mv: MigrationMove) {
match &mv.kind {
SaeMove::Birth { seed, .. } => {
self.n_births += mv.count;
if seed.is_pc_reseed() {
self.pc_reseed_events += mv.count;
}
}
SaeMove::Death { .. } => self.n_deaths += mv.count,
SaeMove::Refuse { .. } => self.n_refusals += mv.count,
}
self.moves.push(mv);
}
pub fn birth(
&mut self,
stage: MoveStage,
seed: BirthSeed,
count: usize,
round: Option<usize>,
evidence: MoveEvidence,
objective: f64,
) {
self.record(MigrationMove {
kind: SaeMove::Birth { stage, seed },
round,
count,
evidence,
objective,
predicted_dl_bits: None,
});
}
pub fn death(
&mut self,
stage: MoveStage,
reason: MoveReason,
count: usize,
round: Option<usize>,
evidence: MoveEvidence,
objective: f64,
) {
self.record(MigrationMove {
kind: SaeMove::Death { stage, reason },
round,
count,
evidence,
objective,
predicted_dl_bits: None,
});
}
pub fn refuse(
&mut self,
stage: MoveStage,
reason: MoveReason,
count: usize,
round: Option<usize>,
evidence: MoveEvidence,
objective: f64,
) {
self.record(MigrationMove {
kind: SaeMove::Refuse { stage, reason },
round,
count,
evidence,
objective,
predicted_dl_bits: None,
});
}
pub fn record_search_round(
&mut self,
round: usize,
ledger: &SearchLedger,
birth_predictions: &HashMap<usize, f64>,
) {
for record in &ledger.moves {
let stage = structure_move_stage(&record.mv);
let predicted = match &record.mv {
StructureMove::Birth { candidate } => birth_predictions.get(candidate).copied(),
_ => None,
};
match &record.verdict {
MoveVerdict::Accepted { log_e } => match &record.mv {
StructureMove::Death { .. } => self.death(
stage,
MoveReason::DeadRouting,
1,
Some(round),
MoveEvidence::from_log_e(*log_e),
f64::NAN,
),
_ => self.birth(
stage,
BirthSeed::ResidualFactor,
1,
Some(round),
MoveEvidence::from_log_e(*log_e),
f64::NAN,
),
},
MoveVerdict::Demoted { log_e } => self.death(
stage,
MoveReason::DeadRouting,
1,
Some(round),
MoveEvidence::from_log_e(*log_e),
f64::NAN,
),
MoveVerdict::Contested { log_e } => self.refuse(
stage,
MoveReason::EvidenceInsufficient,
1,
Some(round),
MoveEvidence::from_log_e(*log_e),
f64::NAN,
),
MoveVerdict::Vetoed { log_e } => self.refuse(
stage,
MoveReason::CertifiedVeto,
1,
Some(round),
MoveEvidence::from_log_e(*log_e),
f64::NAN,
),
MoveVerdict::Deduplicated | MoveVerdict::Stale => self.refuse(
stage,
MoveReason::StaleOrDuplicate,
1,
Some(round),
MoveEvidence::none(),
f64::NAN,
),
MoveVerdict::Deferred => self.refuse(
stage,
MoveReason::BudgetDeferred,
1,
Some(round),
MoveEvidence::none(),
f64::NAN,
),
}
if predicted.is_some() {
if let Some(last) = self.moves.last_mut() {
last.predicted_dl_bits = predicted;
}
}
}
}
pub fn assert_no_pc_reseed(&self) -> Result<(), String> {
if self.pc_reseed_events == 0 {
return Ok(());
}
let offenders: Vec<usize> = self
.moves
.iter()
.enumerate()
.filter(|(_, mv)| {
matches!(&mv.kind, SaeMove::Birth { seed, .. } if seed.is_pc_reseed())
})
.map(|(index, _)| index)
.collect();
Err(format!(
"#2023: {} principal-component reseed event(s) recorded; births must draw \
from the residual-factor pool (offending move indices {:?})",
self.pc_reseed_events, offenders
))
}
}
fn structure_move_stage(mv: &StructureMove) -> MoveStage {
match mv {
StructureMove::Birth { .. }
| StructureMove::Fusion { .. }
| StructureMove::Fission { .. }
| StructureMove::Glue { .. } => MoveStage::Curved,
StructureMove::Death { .. } => MoveStage::Curved,
}
}
#[cfg(test)]
mod ledger_tests {
use super::*;
#[test]
fn nats_to_bits_is_log2_scaling() {
assert!((bits_from_nats(LN_2) - 1.0).abs() < 1e-12);
assert!((bits_from_nats(2.0 * LN_2) - 2.0).abs() < 1e-12);
}
#[test]
fn pc_reseed_bar_fires_on_the_forbidden_seed_and_passes_the_sanctioned_one_2023() {
let mut sanctioned = SaeMigrationLedger::new();
sanctioned.birth(
MoveStage::Curved,
BirthSeed::ResidualFactor,
3,
Some(0),
MoveEvidence::none(),
f64::NAN,
);
assert_eq!(
sanctioned.n_births, 3,
"#2023: a residual-factor birth must still be counted as a birth"
);
assert_eq!(
sanctioned.pc_reseed_events, 0,
"#2023: the sanctioned seed must not trip the PC-reseed counter"
);
assert!(
sanctioned.assert_no_pc_reseed().is_ok(),
"#2023: a residual-factor-only ledger must clear the acceptance bar"
);
let mut forbidden = SaeMigrationLedger::new();
forbidden.birth(
MoveStage::Curved,
BirthSeed::PrincipalComponent,
2,
Some(0),
MoveEvidence::none(),
f64::NAN,
);
assert_eq!(
forbidden.pc_reseed_events, 2,
"#2023: the counter must count the MULTIPLICITY of a forbidden birth, \
not merely that one occurred"
);
let refusal = forbidden
.assert_no_pc_reseed()
.expect_err("#2023: a recorded PC reseed must fail the acceptance bar");
assert!(
refusal.contains("[0]"),
"#2023: the refusal must name the offending move INDEX LIST so a failure \
is actionable, got {refusal:?}"
);
forbidden.birth(
MoveStage::Curved,
BirthSeed::ResidualFactor,
1,
Some(1),
MoveEvidence::none(),
f64::NAN,
);
assert_eq!(forbidden.pc_reseed_events, 2);
assert!(
forbidden.assert_no_pc_reseed().is_err(),
"#2023: a later sanctioned birth cannot clear an earlier forbidden one"
);
}
}