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 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 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 let _ = client.execute_skill("save('all)", None);
53
54 let result = client.execute_skill("run()", Some(timeout))?;
56 if !result.ok() {
57 return Err(VirtuosoError::Execution(result.errors.join("; ")));
58 }
59
60 let rdir = client.execute_skill("resultsDir()", None)?;
62 let results_dir = rdir.output.trim().trim_matches('"').to_string();
63
64 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 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 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 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 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 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 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 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
325pub 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}