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 ¶ms {
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#[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#[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 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}