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"];
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#[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#[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 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}