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