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,
});
}
#[must_use]
pub fn is_pc_reseed_free(&self) -> bool {
self.pc_reseed_events == 0
}
pub fn assert_no_pc_reseed(&self) -> Result<(), String> {
if self.is_pc_reseed_free() {
Ok(())
} else {
Err(format!(
"SaeMigrationLedger invariant breached: {} principal-component \
reseed event(s) recorded (births must draw from the \
residual-factor pool only)",
self.pc_reseed_events
))
}
}
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;
}
}
}
}
}
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 record_updates_tallies_and_holds_pc_invariant() {
let mut ledger = SaeMigrationLedger::new();
ledger.birth(
MoveStage::Curved,
BirthSeed::ResidualFactor,
2,
Some(0),
MoveEvidence::from_dl_bits(4.0),
-1.0,
);
ledger.death(
MoveStage::Linear,
MoveReason::DeadRouting,
1,
None,
MoveEvidence::none(),
f64::NAN,
);
ledger.refuse(
MoveStage::Curved,
MoveReason::EvidenceInsufficient,
1,
Some(1),
MoveEvidence::from_dl_bits(0.0),
-0.5,
);
assert_eq!(ledger.n_births, 2);
assert_eq!(ledger.n_deaths, 1);
assert_eq!(ledger.n_refusals, 1);
assert_eq!(ledger.moves.len(), 3);
assert!(ledger.is_pc_reseed_free());
assert!(ledger.assert_no_pc_reseed().is_ok());
}
#[test]
fn principal_component_birth_trips_the_invariant() {
let mut ledger = SaeMigrationLedger::new();
ledger.birth(
MoveStage::Curved,
BirthSeed::PrincipalComponent,
1,
Some(0),
MoveEvidence::none(),
f64::NAN,
);
assert_eq!(ledger.pc_reseed_events, 1);
assert!(!ledger.is_pc_reseed_free());
assert!(ledger.assert_no_pc_reseed().is_err());
}
}