Skip to main content

virtuoso_cli/commands/
sim.rs

1use crate::client::bridge::VirtuosoClient;
2use crate::error::{Result, VirtuosoError};
3use crate::ocean;
4use crate::ocean::corner::CornerConfig;
5use crate::spectre::jobs::Job;
6use crate::spectre::runner::SpectreSimulator;
7use serde_json::{json, Value};
8use std::collections::HashMap;
9
10pub fn setup(lib: &str, cell: &str, view: &str, simulator: &str) -> Result<Value> {
11    let client = VirtuosoClient::from_env()?;
12    let skill = ocean::setup_skill(lib, cell, view, simulator);
13    let result = client.execute_skill(&skill, None)?;
14
15    if !result.ok() {
16        return Err(VirtuosoError::Execution(result.errors.join("; ")));
17    }
18
19    Ok(json!({
20        "status": "success",
21        "simulator": simulator,
22        "design": { "lib": lib, "cell": cell, "view": view },
23        "results_dir": result.output.trim().trim_matches('"'),
24    }))
25}
26
27pub fn run(analysis: &str, params: &HashMap<String, String>, timeout: u64) -> Result<Value> {
28    let client = VirtuosoClient::from_env()?;
29
30    // Check if resultsDir is set — do NOT override if it is, as changing
31    // resultsDir while an ADE session is active causes run() to silently
32    // return nil (ADE binds the session to a specific results path).
33    let rdir = client.execute_skill("resultsDir()", None)?;
34    let rdir_val = rdir.output.trim().trim_matches('"');
35    if rdir_val == "nil" || rdir_val.is_empty() {
36        return Err(VirtuosoError::Execution(
37            "resultsDir is not set. Run `virtuoso sim setup` first, or open \
38             ADE L for your testbench and run at least one simulation to \
39             establish the session path."
40                .into(),
41        ));
42    }
43
44    // Send analysis setup
45    let analysis_skill = ocean::analysis_skill_simple(analysis, params);
46    let analysis_result = client.execute_skill(&analysis_skill, None)?;
47    if !analysis_result.ok() {
48        return Err(VirtuosoError::Execution(analysis_result.errors.join("; ")));
49    }
50
51    // Send save
52    let _ = client.execute_skill("save('all)", None);
53
54    // Execute run
55    let result = client.execute_skill("run()", Some(timeout))?;
56    if !result.ok() {
57        return Err(VirtuosoError::Execution(result.errors.join("; ")));
58    }
59
60    // Get actual results dir
61    let rdir = client.execute_skill("resultsDir()", None)?;
62    let results_dir = rdir.output.trim().trim_matches('"').to_string();
63
64    // Validate: run() returning nil usually means simulation didn't execute
65    let run_output = result.output.trim().trim_matches('"');
66    if run_output == "nil" {
67        let check =
68            client.execute_skill(&format!(r#"isFile("{results_dir}/psf/spectre.out")"#), None)?;
69        let has_spectre_out = check.output.trim().trim_matches('"');
70        if has_spectre_out == "nil" || has_spectre_out == "0" {
71            return Err(VirtuosoError::Execution(
72                "Simulation failed: run() returned nil and no spectre.out found. \
73                 The netlist may be missing or stale — regenerate via ADE \
74                 (Simulation → Netlist and Run) or `virtuoso sim netlist`."
75                    .into(),
76            ));
77        }
78    }
79
80    Ok(json!({
81        "status": "success",
82        "analysis": analysis,
83        "params": params,
84        "results_dir": results_dir,
85        "execution_time": result.execution_time,
86    }))
87}
88
89pub fn measure(analysis: &str, exprs: &[String]) -> Result<Value> {
90    let client = VirtuosoClient::from_env()?;
91
92    // Open results from resultsDir PSF and select result type
93    let rdir = client.execute_skill("resultsDir()", None)?;
94    let rdir_val = rdir.output.trim().trim_matches('"');
95    if rdir_val != "nil" && !rdir_val.is_empty() {
96        let open_skill = format!("openResults(\"{rdir_val}/psf\")");
97        let _ = client.execute_skill(&open_skill, None);
98    }
99    let select_skill = format!("selectResult('{analysis})");
100    let _ = client.execute_skill(&select_skill, None);
101
102    // Execute each measure expression individually for reliability
103    let mut measures = Vec::new();
104    for expr in exprs {
105        let result = client.execute_skill(expr, None)?;
106        let value = if result.ok() {
107            result.output.trim().trim_matches('"').to_string()
108        } else {
109            format!("ERROR: {}", result.errors.join("; "))
110        };
111        measures.push(json!({
112            "expr": expr,
113            "value": value,
114        }));
115    }
116
117    // Detect all-nil results and provide diagnostics
118    let all_nil = !measures.is_empty()
119        && measures.iter().all(|m| {
120            m.get("value")
121                .and_then(|v| v.as_str())
122                .map(|s| s == "nil")
123                .unwrap_or(false)
124        });
125
126    let mut warnings: Vec<String> = Vec::new();
127    if all_nil {
128        let rdir_for_check = rdir_val.to_string();
129        let spectre_exists = client
130            .execute_skill(
131                &format!(r#"isFile("{rdir_for_check}/psf/spectre.out")"#),
132                None,
133            )
134            .map(|r| {
135                let v = r.output.trim().trim_matches('"');
136                v != "nil" && v != "0"
137            })
138            .unwrap_or(false);
139
140        if !spectre_exists {
141            warnings.push(
142                "All measurements returned nil. No spectre.out found — simulation \
143                 may not have run. Check netlist with `virtuoso sim netlist`."
144                    .into(),
145            );
146        } else {
147            warnings.push(
148                "All measurements returned nil. Spectre ran but produced no matching \
149                 data — verify signal names match your schematic and that the correct \
150                 analysis type is selected."
151                    .into(),
152            );
153        }
154    }
155
156    Ok(json!({
157        "status": "success",
158        "measures": measures,
159        "warnings": warnings,
160    }))
161}
162
163pub fn sweep(
164    var: &str,
165    from: f64,
166    to: f64,
167    step: f64,
168    analysis: &str,
169    measure_exprs: &[String],
170    timeout: u64,
171) -> Result<Value> {
172    let client = VirtuosoClient::from_env()?;
173
174    // Generate value list
175    let mut values = Vec::new();
176    let mut v = from;
177    while v <= to + step * 0.01 {
178        values.push(v);
179        v += step;
180    }
181
182    let skill = ocean::sweep_skill(var, &values, analysis, measure_exprs);
183    let result = client.execute_skill(&skill, Some(timeout))?;
184
185    if !result.ok() {
186        return Err(VirtuosoError::Execution(result.errors.join("; ")));
187    }
188
189    let parsed = ocean::parse_skill_list(result.output.trim());
190
191    let mut headers = vec![var.to_string()];
192    headers.extend(measure_exprs.iter().cloned());
193
194    let rows: Vec<Value> = parsed
195        .iter()
196        .map(|row| {
197            let mut obj = serde_json::Map::new();
198            for (i, h) in headers.iter().enumerate() {
199                if let Some(val) = row.get(i) {
200                    obj.insert(h.clone(), json!(val));
201                }
202            }
203            Value::Object(obj)
204        })
205        .collect();
206
207    Ok(json!({
208        "status": "success",
209        "variable": var,
210        "points": values.len(),
211        "headers": headers,
212        "data": rows,
213        "execution_time": result.execution_time,
214    }))
215}
216
217pub fn corner(file: &str, timeout: u64) -> Result<Value> {
218    let content = std::fs::read_to_string(file)
219        .map_err(|e| VirtuosoError::NotFound(format!("corner config not found: {file}: {e}")))?;
220
221    let config: CornerConfig = serde_json::from_str(&content)
222        .map_err(|e| VirtuosoError::Config(format!("invalid corner config: {e}")))?;
223
224    let client = VirtuosoClient::from_env()?;
225    let skill = ocean::corner_skill(&config);
226    let result = client.execute_skill(&skill, Some(timeout))?;
227
228    if !result.ok() {
229        return Err(VirtuosoError::Execution(result.errors.join("; ")));
230    }
231
232    let parsed = ocean::parse_skill_list(result.output.trim());
233
234    let mut headers = vec!["corner".to_string(), "temp".to_string()];
235    headers.extend(config.measures.iter().map(|m| m.name.clone()));
236
237    let rows: Vec<Value> = parsed
238        .iter()
239        .map(|row| {
240            let mut obj = serde_json::Map::new();
241            for (i, h) in headers.iter().enumerate() {
242                if let Some(val) = row.get(i) {
243                    obj.insert(h.clone(), json!(val));
244                }
245            }
246            Value::Object(obj)
247        })
248        .collect();
249
250    Ok(json!({
251        "status": "success",
252        "corners": config.corners.len(),
253        "measures": config.measures.len(),
254        "headers": headers,
255        "data": rows,
256        "execution_time": result.execution_time,
257    }))
258}
259
260pub fn results() -> Result<Value> {
261    let client = VirtuosoClient::from_env()?;
262    let result = client.execute_skill("resultsDir()", None)?;
263
264    if !result.ok() {
265        return Err(VirtuosoError::Execution(result.errors.join("; ")));
266    }
267
268    let dir = result.output.trim().trim_matches('"').to_string();
269
270    // Query available result types
271    let types_result = client.execute_skill(
272        &format!(r#"let((dir files) dir="{dir}" when(isDir(dir) files=getDirFiles(dir)) files)"#),
273        None,
274    )?;
275
276    Ok(json!({
277        "status": "success",
278        "results_dir": dir,
279        "contents": types_result.output.trim(),
280    }))
281}
282
283pub fn netlist(recreate: bool) -> Result<Value> {
284    let client = VirtuosoClient::from_env()?;
285
286    // Method 1: Ocean createNetlist
287    let r1 = client.execute_skill(
288        if recreate {
289            "createNetlist(?recreateAll t ?display nil)"
290        } else {
291            "createNetlist(?display nil)"
292        },
293        Some(60),
294    )?;
295    let r1_out = r1.output.trim().trim_matches('"');
296    if r1.ok() && r1_out != "nil" {
297        return Ok(json!({
298            "status": "success",
299            "method": "createNetlist",
300            "output": r1_out,
301        }));
302    }
303
304    // Method 2: ASI session-based netlisting
305    let r2 = client.execute_skill(
306        "asiCreateNetlist(asiGetSession(hiGetCurrentWindow()))",
307        Some(60),
308    )?;
309    let r2_out = r2.output.trim().trim_matches('"');
310    if r2.ok() && r2_out != "nil" {
311        return Ok(json!({
312            "status": "success",
313            "method": "asiCreateNetlist",
314            "output": r2_out,
315        }));
316    }
317
318    Err(VirtuosoError::Execution(
319        "Cannot create netlist programmatically. \
320         Open ADE L for this cell and run Simulation → Netlist and Run."
321            .into(),
322    ))
323}
324
325// ── Async job commands ──────────────────────────────────────────────
326
327pub fn run_async(netlist_path: &str) -> Result<Value> {
328    let content = std::fs::read_to_string(netlist_path)
329        .map_err(|e| VirtuosoError::Config(format!("Cannot read netlist '{netlist_path}': {e}")))?;
330    let sim = SpectreSimulator::from_env()?;
331    let job = sim.run_async(&content)?;
332    Ok(json!({
333        "status": "launched",
334        "job_id": job.id,
335        "pid": job.pid,
336        "netlist": netlist_path,
337    }))
338}
339
340pub fn job_status(id: &str) -> Result<Value> {
341    let mut job = Job::load(id)?;
342    job.refresh()?;
343    serde_json::to_value(&job).map_err(|e| VirtuosoError::Execution(e.to_string()))
344}
345
346pub fn job_list() -> Result<Value> {
347    let mut jobs = Job::list_all()?;
348    for job in &mut jobs {
349        let _ = job.refresh();
350    }
351    Ok(json!({
352        "count": jobs.len(),
353        "jobs": serde_json::to_value(&jobs).unwrap_or_default(),
354    }))
355}
356
357pub fn job_cancel(id: &str) -> Result<Value> {
358    let mut job = Job::load(id)?;
359    job.cancel()?;
360    Ok(json!({
361        "status": "cancelled",
362        "job_id": id,
363    }))
364}