Skip to main content

oxihuman_core/
capacity_planner.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Capacity planning calculator stub.
6
7/// A capacity planning resource specification.
8#[derive(Debug, Clone)]
9pub struct CapacitySpec {
10    pub resource_name: String,
11    pub current_capacity: f64,
12    pub current_usage: f64,
13    /// Expected growth rate per period (fraction, e.g. 0.1 = 10%).
14    pub growth_rate: f64,
15    /// Safety headroom factor (e.g. 1.2 = 20% buffer).
16    pub headroom_factor: f64,
17}
18
19impl CapacitySpec {
20    pub fn new(
21        resource_name: &str,
22        current_capacity: f64,
23        current_usage: f64,
24        growth_rate: f64,
25        headroom_factor: f64,
26    ) -> Self {
27        Self {
28            resource_name: resource_name.to_string(),
29            current_capacity,
30            current_usage,
31            growth_rate,
32            headroom_factor,
33        }
34    }
35
36    pub fn utilization(&self) -> f64 {
37        if self.current_capacity <= 0.0 {
38            1.0
39        } else {
40            (self.current_usage / self.current_capacity).clamp(0.0, 1.0)
41        }
42    }
43
44    /// Predicted usage after `periods` time periods with compound growth.
45    pub fn predicted_usage(&self, periods: u32) -> f64 {
46        self.current_usage * (1.0 + self.growth_rate).powi(periods as i32)
47    }
48
49    /// Required capacity to handle predicted usage with headroom.
50    pub fn required_capacity(&self, periods: u32) -> f64 {
51        self.predicted_usage(periods) * self.headroom_factor
52    }
53
54    /// Periods until capacity is breached (without headroom).
55    pub fn periods_until_full(&self) -> Option<u32> {
56        if self.growth_rate <= 0.0 || self.current_usage <= 0.0 {
57            return None;
58        }
59        let mut usage = self.current_usage;
60        for p in 0..10_000 {
61            if usage >= self.current_capacity {
62                return Some(p);
63            }
64            usage *= 1.0 + self.growth_rate;
65        }
66        None
67    }
68}
69
70/// Registry of capacity specs.
71#[derive(Debug, Default)]
72pub struct CapacityPlanner {
73    specs: Vec<CapacitySpec>,
74}
75
76impl CapacityPlanner {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    pub fn add(&mut self, spec: CapacitySpec) {
82        self.specs.push(spec);
83    }
84
85    pub fn spec_count(&self) -> usize {
86        self.specs.len()
87    }
88
89    pub fn most_utilized(&self) -> Option<&CapacitySpec> {
90        self.specs.iter().max_by(|a, b| {
91            a.utilization()
92                .partial_cmp(&b.utilization())
93                .unwrap_or(std::cmp::Ordering::Equal)
94        })
95    }
96
97    pub fn over_threshold(&self, threshold: f64) -> Vec<&CapacitySpec> {
98        self.specs
99            .iter()
100            .filter(|s| s.utilization() > threshold)
101            .collect()
102    }
103
104    pub fn specs(&self) -> &[CapacitySpec] {
105        &self.specs
106    }
107}
108
109pub fn new_capacity_planner() -> CapacityPlanner {
110    CapacityPlanner::new()
111}
112
113pub fn cp_add(planner: &mut CapacityPlanner, spec: CapacitySpec) {
114    planner.add(spec);
115}
116
117pub fn cp_spec_count(planner: &CapacityPlanner) -> usize {
118    planner.spec_count()
119}
120
121pub fn cp_most_utilized(planner: &CapacityPlanner) -> Option<&CapacitySpec> {
122    planner.most_utilized()
123}
124
125pub fn cp_over_threshold(planner: &CapacityPlanner, threshold: f64) -> Vec<&CapacitySpec> {
126    planner.over_threshold(threshold)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn make_spec(name: &str, cap: f64, usage: f64, growth: f64) -> CapacitySpec {
134        CapacitySpec::new(name, cap, usage, growth, 1.2)
135    }
136
137    #[test]
138    fn test_utilization() {
139        let spec = make_spec("cpu", 100.0, 80.0, 0.05);
140        assert!((spec.utilization() - 0.8).abs() < 1e-10);
141    }
142
143    #[test]
144    fn test_predicted_usage_grows() {
145        let spec = make_spec("mem", 1000.0, 500.0, 0.1);
146        let pred = spec.predicted_usage(1);
147        assert!(pred > 500.0);
148    }
149
150    #[test]
151    fn test_required_capacity_includes_headroom() {
152        let spec = CapacitySpec::new("disk", 100.0, 50.0, 0.0, 1.2);
153        let req = spec.required_capacity(0);
154        assert!((req - 60.0).abs() < 1e-5);
155    }
156
157    #[test]
158    fn test_periods_until_full() {
159        let spec = make_spec("net", 100.0, 50.0, 0.2);
160        let p = spec.periods_until_full();
161        assert!(p.is_some());
162        assert!(p.expect("should succeed") > 0);
163    }
164
165    #[test]
166    fn test_no_growth_no_full() {
167        let spec = make_spec("x", 100.0, 50.0, 0.0);
168        assert_eq!(spec.periods_until_full(), None);
169    }
170
171    #[test]
172    fn test_add_and_count() {
173        let mut planner = new_capacity_planner();
174        cp_add(&mut planner, make_spec("cpu", 100.0, 40.0, 0.05));
175        assert_eq!(cp_spec_count(&planner), 1);
176    }
177
178    #[test]
179    fn test_most_utilized() {
180        let mut planner = new_capacity_planner();
181        cp_add(&mut planner, make_spec("cpu", 100.0, 80.0, 0.05));
182        cp_add(&mut planner, make_spec("mem", 100.0, 30.0, 0.05));
183        let top = cp_most_utilized(&planner).expect("should succeed");
184        assert_eq!(top.resource_name, "cpu");
185    }
186
187    #[test]
188    fn test_over_threshold() {
189        let mut planner = new_capacity_planner();
190        cp_add(&mut planner, make_spec("a", 100.0, 90.0, 0.0));
191        cp_add(&mut planner, make_spec("b", 100.0, 50.0, 0.0));
192        let over = cp_over_threshold(&planner, 0.8);
193        assert_eq!(over.len(), 1);
194    }
195
196    #[test]
197    fn test_compound_growth_5_periods() {
198        let spec = make_spec("svc", 1000.0, 100.0, 0.1);
199        let pred5 = spec.predicted_usage(5);
200        /* 100 * 1.1^5 ≈ 161.05 */
201        assert!(pred5 > 160.0 && pred5 < 162.0);
202    }
203}