Skip to main content

ruvector_cognitive_container/
epoch.rs

1use serde::{Deserialize, Serialize};
2
3/// Per-phase tick budgets for a single container epoch.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct ContainerEpochBudget {
6    /// Maximum total ticks for the entire epoch.
7    pub total: u64,
8    /// Ticks allocated to the ingest phase.
9    pub ingest: u64,
10    /// Ticks allocated to the min-cut phase.
11    pub mincut: u64,
12    /// Ticks allocated to the spectral analysis phase.
13    pub spectral: u64,
14    /// Ticks allocated to the evidence accumulation phase.
15    pub evidence: u64,
16    /// Ticks allocated to the witness receipt phase.
17    pub witness: u64,
18}
19
20impl Default for ContainerEpochBudget {
21    fn default() -> Self {
22        Self {
23            total: 10_000,
24            ingest: 2_000,
25            mincut: 3_000,
26            spectral: 2_000,
27            evidence: 2_000,
28            witness: 1_000,
29        }
30    }
31}
32
33/// Processing phases within a single epoch.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum Phase {
36    Ingest,
37    MinCut,
38    Spectral,
39    Evidence,
40    Witness,
41}
42
43/// Controls compute-tick budgeting across phases within an epoch.
44pub struct EpochController {
45    budget: ContainerEpochBudget,
46    ticks_used: u64,
47    phase_used: [u64; 5],
48    current_phase: Phase,
49}
50
51impl EpochController {
52    /// Create a new controller with the given budget.
53    pub fn new(budget: ContainerEpochBudget) -> Self {
54        Self {
55            budget,
56            ticks_used: 0,
57            phase_used: [0; 5],
58            current_phase: Phase::Ingest,
59        }
60    }
61
62    /// Check whether `phase` still has budget remaining.
63    /// If yes, sets the current phase and returns `true`.
64    pub fn try_budget(&mut self, phase: Phase) -> bool {
65        let idx = Self::phase_index(phase);
66        let limit = self.phase_budget(phase);
67        if self.phase_used[idx] < limit && self.ticks_used < self.budget.total {
68            self.current_phase = phase;
69            true
70        } else {
71            false
72        }
73    }
74
75    /// Consume `ticks` from both the total budget and the current phase budget.
76    pub fn consume(&mut self, ticks: u64) {
77        let idx = Self::phase_index(self.current_phase);
78        self.ticks_used += ticks;
79        self.phase_used[idx] += ticks;
80    }
81
82    /// Ticks remaining in the total epoch budget.
83    pub fn remaining(&self) -> u64 {
84        self.budget.total.saturating_sub(self.ticks_used)
85    }
86
87    /// Reset the controller for a new epoch.
88    pub fn reset(&mut self) {
89        self.ticks_used = 0;
90        self.phase_used = [0; 5];
91        self.current_phase = Phase::Ingest;
92    }
93
94    /// Total tick budget allocated to `phase`.
95    pub fn phase_budget(&self, phase: Phase) -> u64 {
96        match phase {
97            Phase::Ingest => self.budget.ingest,
98            Phase::MinCut => self.budget.mincut,
99            Phase::Spectral => self.budget.spectral,
100            Phase::Evidence => self.budget.evidence,
101            Phase::Witness => self.budget.witness,
102        }
103    }
104
105    /// Ticks consumed so far by `phase`.
106    pub fn phase_used(&self, phase: Phase) -> u64 {
107        self.phase_used[Self::phase_index(phase)]
108    }
109
110    fn phase_index(phase: Phase) -> usize {
111        match phase {
112            Phase::Ingest => 0,
113            Phase::MinCut => 1,
114            Phase::Spectral => 2,
115            Phase::Evidence => 3,
116            Phase::Witness => 4,
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_epoch_budgeting() {
127        let budget = ContainerEpochBudget {
128            total: 100,
129            ingest: 30,
130            mincut: 25,
131            spectral: 20,
132            evidence: 15,
133            witness: 10,
134        };
135        let mut ctl = EpochController::new(budget);
136
137        assert!(ctl.try_budget(Phase::Ingest));
138        ctl.consume(30);
139        assert_eq!(ctl.phase_used(Phase::Ingest), 30);
140        // Phase is now exhausted.
141        assert!(!ctl.try_budget(Phase::Ingest));
142        assert_eq!(ctl.remaining(), 70);
143
144        assert!(ctl.try_budget(Phase::MinCut));
145        ctl.consume(25);
146        assert!(!ctl.try_budget(Phase::MinCut));
147        assert_eq!(ctl.remaining(), 45);
148
149        assert!(ctl.try_budget(Phase::Spectral));
150        ctl.consume(20);
151        assert!(ctl.try_budget(Phase::Evidence));
152        ctl.consume(15);
153        assert!(ctl.try_budget(Phase::Witness));
154        ctl.consume(10);
155
156        assert_eq!(ctl.remaining(), 0);
157    }
158
159    #[test]
160    fn test_epoch_reset() {
161        let mut ctl = EpochController::new(ContainerEpochBudget::default());
162        assert!(ctl.try_budget(Phase::Ingest));
163        ctl.consume(500);
164        assert_eq!(ctl.phase_used(Phase::Ingest), 500);
165
166        ctl.reset();
167        assert_eq!(ctl.phase_used(Phase::Ingest), 0);
168        assert_eq!(ctl.remaining(), 10_000);
169    }
170
171    #[test]
172    fn test_total_budget_caps_phase() {
173        let budget = ContainerEpochBudget {
174            total: 10,
175            ingest: 100,
176            mincut: 100,
177            spectral: 100,
178            evidence: 100,
179            witness: 100,
180        };
181        let mut ctl = EpochController::new(budget);
182        assert!(ctl.try_budget(Phase::Ingest));
183        ctl.consume(10);
184        // Total is exhausted even though phase still has room.
185        assert!(!ctl.try_budget(Phase::MinCut));
186    }
187}