Skip to main content

virtuoso_cli/spec/
bandgap.rs

1use crate::error::{Result, VirtuosoError};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct BandgapSpec {
7    pub ip_type: String,
8    pub target: TargetSpec,
9    pub params: HashMap<String, ParamRange>,
10    #[serde(default = "default_corner")]
11    pub corner: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TargetSpec {
16    #[serde(rename = "Vbg")]
17    pub vbg: f64,
18    #[serde(rename = "PSRR")]
19    pub psrr: Option<f64>,
20    #[serde(rename = "TC")]
21    pub tc: Option<f64>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ParamRange {
26    pub min: f64,
27    pub max: f64,
28    pub step: f64,
29}
30
31fn default_corner() -> String {
32    "tt".to_string()
33}
34
35impl BandgapSpec {
36    pub fn from_file(path: &str) -> Result<Self> {
37        let content = std::fs::read_to_string(path)
38            .map_err(|e| VirtuosoError::Config(format!("cannot read spec file '{path}': {e}")))?;
39        let spec: Self = serde_yaml::from_str(&content)
40            .map_err(|e| VirtuosoError::Config(format!("invalid YAML in '{path}': {e}")))?;
41        spec.validate()?;
42        Ok(spec)
43    }
44
45    pub fn validate(&self) -> Result<()> {
46        for (name, range) in &self.params {
47            if range.min > range.max {
48                return Err(VirtuosoError::Config(format!(
49                    "param '{name}': min ({}) > max ({})",
50                    range.min, range.max
51                )));
52            }
53            if range.min <= 0.0 || range.max <= 0.0 {
54                return Err(VirtuosoError::Config(format!(
55                    "param '{name}': W/L values must be positive"
56                )));
57            }
58        }
59        Ok(())
60    }
61
62    pub fn param_combos(&self) -> Vec<HashMap<String, f64>> {
63        let mut combos: Vec<HashMap<String, f64>> = vec![HashMap::new()];
64        for (name, range) in &self.params {
65            let steps = ((range.max - range.min) / range.step).round() as usize;
66            let mut next = Vec::new();
67            for i in 0..=steps {
68                let v = range.min + range.step * i as f64;
69                for base in &combos {
70                    let mut c = base.clone();
71                    c.insert(name.clone(), v);
72                    next.push(c);
73                }
74            }
75            combos = next;
76        }
77        combos
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn parse_yaml(yaml: &str) -> BandgapSpec {
86        let spec: BandgapSpec = serde_yaml::from_str(yaml).expect("valid yaml");
87        spec
88    }
89
90    const VALID_YAML: &str = r#"
91ip_type: bandgap
92target:
93  Vbg: 1.20
94  PSRR: 80
95  TC: 20
96params:
97  W:
98    min: 1.0e-6
99    max: 5.0e-6
100    step: 1.0e-6
101  L:
102    min: 0.18e-6
103    max: 1.0e-6
104    step: 0.18e-6
105corner: tt
106"#;
107
108    #[test]
109    fn test_valid_bandgap_yaml() {
110        let spec = parse_yaml(VALID_YAML);
111        assert_eq!(spec.ip_type, "bandgap");
112        assert!((spec.target.vbg - 1.20).abs() < 1e-9);
113        assert_eq!(spec.corner, "tt");
114        assert!(spec.params.contains_key("W"));
115        assert!(spec.params.contains_key("L"));
116        spec.validate().expect("should be valid");
117    }
118
119    #[test]
120    fn test_missing_vbg_target() {
121        let yaml = r#"
122ip_type: bandgap
123target:
124  PSRR: 80
125params:
126  W:
127    min: 1.0e-6
128    max: 5.0e-6
129    step: 1.0e-6
130"#;
131        let result: std::result::Result<BandgapSpec, _> = serde_yaml::from_str(yaml);
132        assert!(result.is_err(), "missing Vbg should fail parse");
133    }
134
135    #[test]
136    fn test_negative_w_or_l() {
137        let yaml = r#"
138ip_type: bandgap
139target:
140  Vbg: 1.20
141params:
142  W:
143    min: -1.0e-6
144    max: 5.0e-6
145    step: 1.0e-6
146"#;
147        let spec: BandgapSpec = serde_yaml::from_str(yaml).expect("parses");
148        assert!(
149            spec.validate().is_err(),
150            "negative min should fail validation"
151        );
152    }
153
154    #[test]
155    fn test_default_corner() {
156        let yaml = r#"
157ip_type: bandgap
158target:
159  Vbg: 1.20
160params:
161  W:
162    min: 1.0e-6
163    max: 5.0e-6
164    step: 1.0e-6
165"#;
166        let spec: BandgapSpec = serde_yaml::from_str(yaml).expect("parses");
167        assert_eq!(spec.corner, "tt");
168    }
169
170    #[test]
171    fn test_param_range_order() {
172        let yaml = r#"
173ip_type: bandgap
174target:
175  Vbg: 1.20
176params:
177  W:
178    min: 5.0e-6
179    max: 1.0e-6
180    step: 1.0e-6
181"#;
182        let spec: BandgapSpec = serde_yaml::from_str(yaml).expect("parses");
183        assert!(spec.validate().is_err(), "min > max should fail");
184    }
185
186    #[test]
187    fn test_single_param_combo() {
188        let yaml = r#"
189ip_type: bandgap
190target:
191  Vbg: 1.20
192params:
193  W:
194    min: 2.0e-6
195    max: 2.0e-6
196    step: 1.0e-6
197"#;
198        let spec: BandgapSpec = serde_yaml::from_str(yaml).expect("parses");
199        let combos = spec.param_combos();
200        assert_eq!(combos.len(), 1);
201        assert!((combos[0]["W"] - 2.0e-6).abs() < 1e-15);
202    }
203}