virtuoso_cli/spec/
bandgap.rs1use 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}