Skip to main content

virtuoso_cli/commands/
process.rs

1use crate::client::bridge::VirtuosoClient;
2use crate::error::{Result, VirtuosoError};
3use serde_json::{json, Value};
4
5#[allow(clippy::too_many_arguments)]
6pub fn char(
7    lib: &str,
8    cell: &str,
9    view: &str,
10    inst: &str,
11    device_type: &str,
12    l_values: &[f64],
13    vgs_start: f64,
14    vgs_stop: f64,
15    vgs_step: f64,
16    output: &str,
17    timeout: u64,
18) -> Result<Value> {
19    let client = VirtuosoClient::from_env()?;
20
21    client.execute_skill("simulator('spectre)", None)?;
22    let design_result =
23        client.execute_skill(&format!("design(\"{lib}\" \"{cell}\" \"{view}\")"), None)?;
24    if !design_result.skill_ok() {
25        return Err(VirtuosoError::NotFound(format!(
26            "design {lib}/{cell}/{view} not found"
27        )));
28    }
29
30    let mut all_data: Vec<Value> = Vec::new();
31    let mut total_points = 0;
32
33    for &l in l_values {
34        client.execute_skill(&format!("desVar(\"L\" {l:e})"), None)?;
35
36        let mut points: Vec<Value> = Vec::new();
37        let mut vgs = vgs_start;
38
39        while vgs <= vgs_stop + vgs_step * 0.01 {
40            client.execute_skill(&format!("desVar(\"VGS\" {vgs})"), None)?;
41            let rdir = format!("/tmp/char_{device_type}_{l:e}_{vgs}");
42            client.execute_skill(&format!("resultsDir(\"{rdir}\")"), None)?;
43            client.execute_skill("analysis('dc ?saveOppoint t)", None)?;
44            client.execute_skill("save('all)", None)?;
45            client.execute_skill("run()", Some(timeout))?;
46
47            // Fetch all 5 oppoint params in one round-trip via SKILL list().
48            let params = ["gm", "ids", "gds", "vth", "cgs"];
49            let fetch_expr = {
50                let gets: Vec<String> = params
51                    .iter()
52                    .map(|p| format!("value(getData(\"{inst}:{p}\" ?result \"dcOpInfo\"))"))
53                    .collect();
54                format!("list({})", gets.join(" "))
55            };
56            let r = client.execute_skill(&fetch_expr, None)?;
57            let cleaned = r.output.trim().trim_matches(|c| c == '(' || c == ')');
58            let vals: Vec<&str> = cleaned.split_whitespace().collect();
59
60            let mut opvals = OpVals::default();
61            let mut ok = vals.len() == params.len();
62
63            if ok {
64                for (p, v) in params.iter().zip(vals.iter()) {
65                    let v = v.trim_matches('"');
66                    if v == "nil" || v.is_empty() {
67                        ok = false;
68                        break;
69                    }
70                    match v.parse::<f64>() {
71                        Ok(f) => opvals.set(p, f),
72                        Err(_) => {
73                            ok = false;
74                            break;
75                        }
76                    }
77                }
78            }
79
80            if ok {
81                if let Some(pt) = opvals.build_point(vgs, false) {
82                    points.push(pt);
83                    total_points += 1;
84                }
85            }
86
87            vgs += vgs_step;
88        }
89
90        if !points.is_empty() {
91            all_data.push(json!({ "l": l, "points": points }));
92        }
93    }
94
95    let output_path = write_lookup_json(output, device_type, all_data)?;
96
97    Ok(json!({
98        "status": "success",
99        "device": device_type,
100        "l_values": l_values.len(),
101        "total_points": total_points,
102        "output": output_path,
103    }))
104}
105
106/// Characterize via direct Spectre netlist — no Virtuoso session required.
107/// Generates a netlist for each L, runs spectre, parses PSF ASCII oppoint results.
108#[allow(clippy::too_many_arguments)]
109pub fn char_netlist(
110    device_type: &str,
111    l_values: &[f64],
112    vgs_start: f64,
113    vgs_stop: f64,
114    vgs_step: f64,
115    output: &str,
116    model_file: &str,
117    model_section: &str,
118    vdd: f64,
119    device_model: &str,
120    inst_name: &str,
121    vds: f64,
122) -> Result<Value> {
123    let is_pmos = device_type == "pmos";
124    let spectre_cmd = crate::config::Config::from_env()
125        .map(|c| c.spectre_cmd)
126        .unwrap_or_else(|_| "spectre".into());
127
128    let mut all_data: Vec<Value> = Vec::new();
129    let mut total_points = 0;
130
131    let work_dir = std::path::PathBuf::from(format!("/tmp/vcli_char_{device_type}"));
132    std::fs::create_dir_all(&work_dir).map_err(VirtuosoError::Io)?;
133
134    for &l in l_values {
135        let netlist_path = work_dir.join(format!("char_{l:e}.scs"));
136        let raw_dir = work_dir.join(format!("raw_{l:e}"));
137        std::fs::create_dir_all(&raw_dir).map_err(VirtuosoError::Io)?;
138
139        let netlist = if is_pmos {
140            format!(
141                r#"simulator lang=spectre
142include "{model_file}" section={model_section}
143parameters VDD={vdd} VSD={vds} W=1u L={l:e} vgs_val={vgs_start}
144Vvdd (vdd 0) vsource dc=VDD
145Vsg  (vdd g) vsource dc=vgs_val
146Vsd  (vdd d) vsource dc=VSD
147{inst_name} (d g vdd vdd) {device_model} w=W l=L
148vgs_sweep dc param=vgs_val start={vgs_start} stop={vgs_stop} step={vgs_step} oppoint=rawfile
149save {inst_name}:oppoint
150"#
151            )
152        } else {
153            format!(
154                r#"simulator lang=spectre
155include "{model_file}" section={model_section}
156parameters VDS={vds} W=1u L={l:e} vgs_val={vgs_start}
157Vvgs (g 0) vsource dc=vgs_val
158Vvds (d 0) vsource dc=VDS
159{inst_name} (d g 0 0) {device_model} w=W l=L
160vgs_sweep dc param=vgs_val start={vgs_start} stop={vgs_stop} step={vgs_step} oppoint=rawfile
161save {inst_name}:oppoint
162"#
163            )
164        };
165
166        std::fs::write(&netlist_path, &netlist).map_err(VirtuosoError::Io)?;
167
168        let raw_str = raw_dir.to_str().expect("raw_dir path is UTF-8");
169        let output_run = std::process::Command::new(&spectre_cmd)
170            .args([
171                netlist_path.to_str().expect("netlist path is UTF-8"),
172                "+aps",
173                "-format",
174                "psfascii",
175                "-raw",
176                raw_str,
177            ])
178            .output()
179            .map_err(|e| VirtuosoError::Execution(format!("spectre failed: {e}")))?;
180
181        if !output_run.status.success() {
182            let stderr = String::from_utf8_lossy(&output_run.stderr);
183            return Err(VirtuosoError::Execution(format!(
184                "spectre error at L={l:e}: {stderr}"
185            )));
186        }
187
188        let psf_path = raw_dir.join("vgs_sweep.dc");
189        let psf = std::fs::read_to_string(&psf_path).map_err(VirtuosoError::Io)?;
190        let points = parse_psf_oppoint(&psf, inst_name, is_pmos)?;
191
192        eprintln!("L={l:e}: {} points", points.len());
193        total_points += points.len();
194
195        if !points.is_empty() {
196            all_data.push(json!({ "l": l, "points": points }));
197        }
198    }
199
200    let output_path = write_lookup_json(output, device_type, all_data)?;
201
202    Ok(json!({
203        "status": "success",
204        "device": device_type,
205        "l_values": l_values.len(),
206        "total_points": total_points,
207        "output": output_path,
208    }))
209}
210
211fn write_lookup_json(output: &str, device_type: &str, data: Vec<Value>) -> Result<String> {
212    let lookup = json!({
213        "process": output.split('/').nth_back(1).unwrap_or("unknown"),
214        "device": device_type,
215        "w_testbench": 1e-6,
216        "w_unit": "meters",
217        "id_unit": "amperes (absolute current at w_testbench)",
218        "gain_unit": "V/V (gm/gds)",
219        "ft_unit": "Hz",
220        "sizing_formula": "W_design(um) = Id_needed(A) / id(A) * 1",
221        "characterized": chrono::Local::now().format("%Y-%m-%d").to_string(),
222        "data": data,
223    });
224    let output_path = format!("{output}/{device_type}_lookup.json");
225    std::fs::create_dir_all(output).map_err(VirtuosoError::Io)?;
226    std::fs::write(
227        &output_path,
228        serde_json::to_string_pretty(&lookup).map_err(VirtuosoError::Json)?,
229    )
230    .map_err(VirtuosoError::Io)?;
231    Ok(output_path)
232}
233
234/// Accumulated oppoint values from one PSF block or one SKILL query set.
235#[derive(Default)]
236struct OpVals {
237    gm: Option<f64>,
238    ids: Option<f64>,
239    gds: Option<f64>,
240    vth: Option<f64>,
241    cgs: Option<f64>,
242}
243
244impl OpVals {
245    fn set(&mut self, key: &str, v: f64) {
246        match key {
247            "gm" => self.gm = Some(v),
248            "ids" => self.ids = Some(v),
249            "gds" => self.gds = Some(v),
250            "vth" => self.vth = Some(v),
251            "cgs" => self.cgs = Some(v),
252            _ => {}
253        }
254    }
255
256    fn build_point(&self, vgs: f64, is_pmos: bool) -> Option<Value> {
257        let gm = self.gm?;
258        let id = self.ids?.abs();
259        let gds = self.gds?.abs();
260        let vth = self.vth?;
261        let cgs = self.cgs?.abs();
262
263        if id < 1e-15 || gm < 1e-15 {
264            return None;
265        }
266
267        let gmid = gm / id;
268        let gain = gm / gds;
269        // vgs represents VSG for PMOS — overdrive is VSG - |Vtp|
270        let vov = if is_pmos { vgs - vth.abs() } else { vgs - vth };
271        let ft = gm / (2.0 * std::f64::consts::PI * cgs);
272
273        Some(json!({
274            "vgs":  (vgs * 1000.0).round() / 1000.0,
275            "gmid": (gmid * 100.0).round() / 100.0,
276            "gain": (gain * 10.0).round() / 10.0,
277            "id":   id,
278            "vov":  (vov * 1000.0).round() / 1000.0,
279            "ft":   ft,
280            "vth":  (vth * 10000.0).round() / 10000.0,
281            "gds":  gds,
282        }))
283    }
284}
285
286fn parse_psf_oppoint(psf: &str, inst: &str, is_pmos: bool) -> Result<Vec<Value>> {
287    let gm_key = format!("\"{}:gm\"", inst);
288    let ids_key = format!("\"{}:ids\"", inst);
289    let gds_key = format!("\"{}:gds\"", inst);
290    let vth_key = format!("\"{}:vth\"", inst);
291    let cgs_key = format!("\"{}:cgs\"", inst);
292
293    let mut points: Vec<Value> = Vec::new();
294    let mut current_vgs: Option<f64> = None;
295    let mut vals = OpVals::default();
296
297    for line in psf.lines() {
298        let line = line.trim();
299
300        if line.starts_with("\"vgs_val\"") && !line.contains("sweep") {
301            if let Some(vgs) = current_vgs {
302                if let Some(pt) = vals.build_point(vgs, is_pmos) {
303                    points.push(pt);
304                }
305                vals = OpVals::default();
306            }
307            let v: f64 = line
308                .split_whitespace()
309                .nth(1)
310                .and_then(|s| s.parse().ok())
311                .unwrap_or(0.0);
312            current_vgs = Some(v);
313            continue;
314        }
315
316        let parse_val = |key: &str| -> Option<f64> {
317            if line.starts_with(key) {
318                line.split_whitespace().nth(1).and_then(|s| s.parse().ok())
319            } else {
320                None
321            }
322        };
323
324        if let Some(v) = parse_val(&gm_key) {
325            vals.gm = Some(v);
326        } else if let Some(v) = parse_val(&ids_key) {
327            vals.ids = Some(v);
328        } else if let Some(v) = parse_val(&gds_key) {
329            vals.gds = Some(v);
330        } else if let Some(v) = parse_val(&vth_key) {
331            vals.vth = Some(v);
332        } else if let Some(v) = parse_val(&cgs_key) {
333            vals.cgs = Some(v);
334        }
335    }
336
337    if let Some(vgs) = current_vgs {
338        if let Some(pt) = vals.build_point(vgs, is_pmos) {
339            points.push(pt);
340        }
341    }
342
343    Ok(points)
344}