1mod api;
2mod plot;
3mod callback;
4
5use std::sync::atomic::{AtomicI32, Ordering};
6use std::sync::LazyLock;
7use std::collections::HashMap;
8use std::env;
9use std::path::{Path, PathBuf};
10use std::ffi::{CStr, CString, c_char, c_double, c_int, c_void};
11use api::{NgSpiceAPI, VecData, VecInfoAll, VecValuesAll};
12use libloading::Library;
13use num_complex::Complex64;
14use plot::Plot;
15use regex::Regex;
16use callback::{DefaultNgSpiceSharedCallback, NgSpiceSharedCallback};
17use crate::probe::{AcAnalysis, DcVoltageAnalysis, OpAnalysis, ToAnalysis, TranAnalysis};
18use crate::simulate::Simulate;
19use crate::Value;
20
21use super::error::*;
22
23#[cfg(unix)]
24use libc::setlocale;
25#[cfg(unix)]
26use libc::LC_NUMERIC;
27
28static NGSPICE_ID: LazyLock<AtomicI32> = LazyLock::new(|| AtomicI32::new(0));
29fn next_count() -> i32 {
30 NGSPICE_ID.fetch_add(1, Ordering::SeqCst)
31}
32
33pub struct NgSpiceShared {
34 ngspice_id: i32,
35 pub api: NgSpiceAPI,
36 callback: Box<dyn NgSpiceSharedCallback>,
37 library_path: PathBuf,
38
39 stdout: Vec<String>,
40 stderr: Vec<String>,
41 error_in_stdout: bool,
42 error_in_stderr: bool,
43 spinit_not_found: bool,
44 is_running: bool,
45
46 ngspice_version: Option<u32>,
47 has_xspice: bool,
48 has_cider: bool,
49 extensions: Vec<String>,
50}
51
52
53impl NgSpiceShared {
54 pub fn default() -> NgSpiceResult<Self> {
55 Self::new_with_callback(None, DefaultNgSpiceSharedCallback)
56 }
57
58 pub fn new(library_path: PathBuf) -> NgSpiceResult<Self> {
59 Self::new_with_callback(Some(library_path), DefaultNgSpiceSharedCallback)
60 }
61
62 pub fn new_with_callback(
63 library_path: Option<PathBuf>,
64 callback: impl NgSpiceSharedCallback + 'static
65 ) -> NgSpiceResult<Self> {
66 let ngspice_id = next_count();
67
68 let library_path = match library_path {
69 Some(p) => p,
70 None => Self::setup_platform()?,
71 };
72
73 let lib = Self::load_library(&library_path)?;
74 let api = NgSpiceAPI::new(lib);
75
76 Ok(NgSpiceShared {
77 library_path,
78 callback: Box::new(callback),
79 api,
80 stdout: vec![],
81 stderr: vec![],
82 error_in_stdout: false,
83 error_in_stderr: false,
84 spinit_not_found: false,
85 is_running: false,
86 ngspice_id,
87 ngspice_version: None,
88 has_xspice: false,
89 has_cider: false,
90 extensions: Vec::new(),
91 })
92 }
93
94 fn setup_platform() -> NgSpiceResult<PathBuf> {
95 if let Ok(path) = env::var("NGSPICE_LIBRARY_PATH") {
96 return Ok(PathBuf::from(path))
97 }
98
99 #[cfg(target_os = "linux")] {
100 return Ok(PathBuf::from("libngspice.so"));
101 }
102
103 #[allow(unreachable_code)]
104 Err(NgSpiceError::Platform)
105 }
106
107 fn load_library<P: AsRef<Path>>(library_path: P) -> NgSpiceResult<Library> {
108 let library_path = library_path.as_ref();
109 #[cfg(target_os = "linux")] {
110 unsafe {
111 let c_locale = CString::new("C").unwrap();
112 setlocale(LC_NUMERIC, c_locale.as_ptr());
113 }
114 }
115
116 log::debug!("Loading ngspice library: {:?}", library_path);
117 let lib = unsafe { Library::new(library_path)? };
118
119 Ok(lib)
120 }
121}
122
123impl NgSpiceShared {
124 pub fn init(&mut self) -> NgSpiceResult<()> {
125 self.api.init(
126 Some(Self::send_char_callback),
127 Some(Self::send_stat_callback),
128 Some(Self::exit_callback),
129 Some(Self::send_data_callback),
130 Some(Self::send_init_data_callback),
131 Some(Self::background_thread_running_callback),
132 self as *const Self as *mut c_void
133 );
134
135 self.api.init_sync(
136 Some(Self::get_vsrc_data_callback),
137 Some(Self::get_isrc_data_callback),
138 None,
139 &self.ngspice_id as *const i32 as *mut i32,
140 self as *const Self as *mut c_void
141 );
142
143 self.get_infomation()
144 }
145
146 pub fn exec_command(&mut self, command: &str) -> NgSpiceResult<String> {
147 log::debug!("Execute command: {}", command);
148 self.clear_output();
149
150 let result = self.api.command(command).unwrap();
151 if result != 0 {
152 return Err(NgSpiceError::command(
153 command.into(),
154 format!("ngSpice_Command return '{}'", result)
155 ));
156 }
157
158 if self.error_in_stdout || self.error_in_stderr {
159 return Err(NgSpiceError::command(
160 command.into(),
161 "Error in stdout/stderr".into()
162 ));
163 }
164
165 Ok(self.stdout())
166 }
167
168 pub fn set(&mut self, key: &str) -> NgSpiceResult<()> {
169 self.exec_command(&format!("set {}", key))?;
170 Ok(())
171 }
172
173 pub fn reset(&mut self) -> NgSpiceResult<()> {
174 self.exec_command("reset")?;
175 Ok(())
176 }
177
178 pub fn status(&mut self) -> NgSpiceResult<String> {
179 self.exec_command("status")
180 }
181
182 pub fn step(&mut self, step: Option<usize>) -> NgSpiceResult<()> {
183 match step {
184 Some(step) => self.exec_command(&format!("step {}", step))?,
185 None => self.exec_command("step")?
186 };
187 Ok(())
188 }
189
190 pub fn listing(&mut self) -> NgSpiceResult<String> {
191 self.exec_command("listing")
192 }
193
194 pub fn load_circuit(&mut self, circuit: &str) -> NgSpiceResult<()> {
195 let circuit_lines: Vec<_> = circuit
196 .lines()
197 .collect();
198
199 self.clear_output();
200 let result = self.api.circ(&circuit_lines).unwrap();
201 if result != 0 {
202 return Err(NgSpiceError::circuit(circuit.into(), format!("ngSpice_Circ returned {}", result)));
203 }
204
205 if self.error_in_stdout || self.error_in_stderr {
206 return Err(NgSpiceError::circuit(
207 circuit.into(),
208 "Error in stdout/stderr".into()
209 ));
210 }
211
212 Ok(())
213 }
214
215 pub fn run(&mut self, background: bool) -> NgSpiceResult<()> {
217 let command = if background { "bg_run" } else { "run" };
218 self.exec_command(command)?;
219
220 if background {
221 self.is_running = true;
222 } else {
223 log::debug!("Simulation is done");
224 }
225
226 Ok(())
227 }
228
229 pub fn halt(&mut self) -> NgSpiceResult<()> {
230 self.exec_command("bg_halt")?;
231 Ok(())
232 }
233
234 pub fn resume(&mut self, background: bool) -> NgSpiceResult<()> {
235 let command = if background { "bg_resume" } else { "resume" };
236 self.exec_command(command)?;
237 Ok(())
238 }
239
240 pub fn get_vec(&self, name: &str) -> NgSpiceResult<Vec<f64>> {
241 match self.api.get_vec_real_data(name).unwrap() {
242 Some(data) => Ok(data),
243 None => Err(NgSpiceError::ResultNotFound(name.into())),
244 }
245 }
246
247 pub fn get_plot(&self, plot_name: &str) -> NgSpiceResult<Plot> {
248 let vec_names = self.api.all_vecs(plot_name)?;
249 let mut vectors = HashMap::new();
250
251 for name in vec_names {
252 let full_name = format!("{}.{}", plot_name, name);
253 match self.api.get_vec_data(&full_name) {
254 Ok(Some(data)) => {
255 match data {
256 VecData::Real(values) => {
257 let values = values.into_iter().map(|v| Value::real(v)).collect();
258 vectors.insert(name.clone(), values);
259 }
260 VecData::Complex(values) => {
261 let values = values.into_iter().map(|(re, im)| Value::complex(re, im)).collect();
262 vectors.insert(name.clone(), values);
263 }
264 }
265 }
266 _ => {
267 eprintln!("Warning: failed to load vector {}", full_name);
268 }
269 }
270 }
271
272 Ok(Plot {
273 name: plot_name.to_string(),
274 vectors,
275 })
276 }
277
278 pub fn destroy(&mut self, plot_name: &str) -> NgSpiceResult<()> {
279 self.exec_command(&format!("destroy {}", plot_name))?;
280 Ok(())
281 }
282
283 pub fn destroy_all(&mut self) -> NgSpiceResult<()> {
284 self.exec_command("destroy all")?;
285 Ok(())
286 }
287
288 pub fn get_infomation(&mut self) -> NgSpiceResult<()> {
289 self.ngspice_version = None;
290 self.has_xspice = false;
291 self.has_cider = false;
292 self.extensions.clear();
293
294 let output = self.exec_command("version -f")?;
295 let version_regex = Regex::new(r"\*\* ngspice-(\d+)").unwrap();
296
297 for line in output.lines() {
298 if let Some(caps) = version_regex.captures(line) {
299 if let Some(matched) = caps.get(1) {
300 self.ngspice_version = matched.as_str().parse::<u32>().ok();
301 }
302 }
303
304 if line.contains("** XSPICE") {
305 self.has_xspice = true;
306 self.extensions.push("XSPICE".to_string());
307 }
308
309 if line.contains("CIDER") {
310 self.has_cider = true;
311 self.extensions.push("CIDER".to_string());
312 }
313 }
314
315 log::debug!(
316 "Ngspice version {:?} with extensions: {}",
317 self.ngspice_version,
318 self.extensions.join(", ")
319 );
320
321 Ok(())
322 }
323
324 fn clear_output(&mut self) {
325 self.stdout.clear();
326 self.stderr.clear();
327 self.error_in_stdout = false;
328 self.error_in_stderr = false;
329 }
330}
331
332impl NgSpiceShared {
334 pub fn stdout(&self) -> String {
335 self.stdout.join(" ")
336 }
337
338 pub fn stderr(&self) -> String {
339 self.stderr.join(" ")
340 }
341
342 pub fn is_running(&self) -> bool {
343 self.is_running
344 }
345
346 pub fn library_path(&self) -> &Path {
347 self.library_path.as_path()
348 }
349
350 pub fn ngspice_version(&self) -> u32 {
351 self.ngspice_version.unwrap()
352 }
353
354 pub fn has_xspice(&self) -> bool {
355 self.has_xspice
356 }
357
358 pub fn has_cider(&self) -> bool {
359 self.has_cider
360 }
361
362 pub fn set_callback(&mut self, callback: impl NgSpiceSharedCallback + 'static) {
363 self.callback = Box::new(callback)
364 }
365}
366
367impl NgSpiceShared {
368 unsafe extern "C" fn send_char_callback(message_c: *mut c_char, id: c_int, user_data: *mut c_void) -> c_int {
369 let shared: &mut Self = unsafe { &mut *(user_data as *mut Self) };
370 let message = unsafe { match CStr::from_ptr(message_c).to_str() {
371 Ok(s) => s.to_string(),
372 Err(_) => return 1,
373 }};
374
375 log::debug!("[ngSpice raw] {message}");
376
377 let (prefix, content) = if let Some(pos) = message.find(' ') {
378 message.split_at(pos)
379 } else {
380 ("", &message[..])
381 };
382
383 let content = content.trim_start();
384
385 if prefix == "stderr" {
386 shared.stderr.push(content.to_string());
387 if content.starts_with("Warning:") {
388 eprintln!("[ngSpice warning] {content}");
389 } else {
390 shared.error_in_stderr = true;
391 if content == "Note: can't find init file." {
392 shared.spinit_not_found = true;
393 eprintln!("[ngSpice warning] spinit was not found");
394 } else {
395 eprintln!("[ngSpice error] {content}");
396 }
397 }
398 } else {
399 shared.stdout.push(content.to_string());
400 if content.to_lowercase().contains("error") {
401 shared.error_in_stdout = true;
402 eprintln!("[ngSpice error? stdout] {content}");
403 } else {
404 println!("[ngSpice] {content}");
405 }
406 }
407
408 shared.callback.send_char(&message, id)
409 }
410
411 unsafe extern "C" fn send_stat_callback(message_c: *mut c_char, id: c_int, user_data: *mut c_void) -> c_int {
412 let shared: &mut Self = unsafe { &mut *(user_data as *mut Self) };
413 let message = unsafe { match CStr::from_ptr(message_c).to_str() {
414 Ok(s) => s.to_string(),
415 Err(_) => return 1,
416 }};
417
418 shared.callback.send_stat(&message, id)
419 }
420
421 unsafe extern "C" fn exit_callback(exit_status: c_int, immediate_unloding: bool, quit_exit: bool, ngspice_id: c_int, _user_data: *mut c_void) -> c_int {
422 log::debug!(
423 "ngspice_id-{} exit status={} immediate_unloding={} quit_exit={}",
424 ngspice_id,
425 exit_status,
426 immediate_unloding,
427 quit_exit
428 );
429 exit_status
430 }
431
432 unsafe extern "C" fn send_data_callback(data: *mut VecValuesAll, number_of_vectors: c_int, ngspice_id: c_int, user_data: *mut c_void) -> c_int {
433 let handler = unsafe { &mut *(user_data as *mut Self) };
434 let data_ref = unsafe { &*data };
435 let vecsa_array = unsafe {
436 std::slice::from_raw_parts(data_ref.vecsa, number_of_vectors as usize)
437 };
438
439 let mut actual_vector_values = HashMap::new();
440 for &vec_ptr in vecsa_array {
441 if vec_ptr.is_null() {
442 continue;
443 }
444
445 let vec = unsafe { &*vec_ptr };
446
447 let name = if vec.name.is_null() {
448 "<null>".to_string()
449 } else {
450 unsafe { CStr::from_ptr(vec.name).to_string_lossy().into_owned() }
451 };
452
453 let value = Complex64::new(vec.creal, vec.cimag);
454 actual_vector_values.insert(name, value);
455 }
456
457 handler.callback.send_data(actual_vector_values, number_of_vectors, ngspice_id)
458 }
459
460 unsafe extern "C" fn send_init_data_callback(data: *mut VecInfoAll, ngspice_id: c_int, user_data: *mut c_void) -> c_int {
461 let handler = unsafe { &mut *(user_data as *mut Self) };
462 if data.is_null() {
463 return 0;
464 }
465 let data_ref = unsafe { &*data };
466 handler.callback.send_init_data(data_ref, ngspice_id)
467 }
468
469 unsafe extern "C" fn background_thread_running_callback(is_running: bool, ngspice_id: c_int, user_data: *mut c_void) -> c_int {
470 let handler = unsafe { &mut *(user_data as *mut Self) };
471 log::debug!("ngspice_id-{} background_thread_running {}", ngspice_id, is_running);
472 handler.is_running = is_running;
473 0
474 }
475
476 unsafe extern "C" fn get_vsrc_data_callback(voltage: *mut c_double, time: c_double, node: *mut c_char, ngspice_id: c_int, user_data: *mut c_void) -> c_int {
477 let handler = unsafe { &mut *(user_data as *mut Self) };
478 let node = unsafe { match CStr::from_ptr(node).to_str() {
479 Ok(s) => s.to_string(),
480 Err(_) => return 1,
481 }};
482 let voltage = unsafe { &mut *voltage };
483
484 handler.callback.get_vsrc_data(voltage, time, node, ngspice_id)
485 }
486
487 unsafe extern "C" fn get_isrc_data_callback(current: *mut c_double, time: c_double, node: *mut c_char, ngspice_id: c_int, user_data: *mut c_void) -> c_int {
488 let handler = unsafe { &mut *(user_data as *mut Self) };
489 let node = unsafe { match CStr::from_ptr(node).to_str() {
490 Ok(s) => s.to_string(),
491 Err(_) => return 1,
492 }};
493 let current = unsafe { &mut *current };
494
495 handler.callback.get_isrc_data(current, time, node, ngspice_id)
496 }
497}
498
499impl NgSpiceShared {
500 pub fn simulate(&mut self, circuit: &str) -> NgSpiceResult<Plot> {
501 self.init()?;
502 self.load_circuit(circuit)?;
504 self.run(false)?;
505 let plot_name = self.api.cur_plot().unwrap();
506 let plot = self.get_plot(&plot_name)?;
507 Ok(plot)
508 }
509}
510
511impl Simulate for NgSpiceShared {
512 type Err = NgSpiceError;
513
514 fn run_dc(&mut self, netlist: &str) -> Result<DcVoltageAnalysis, Self::Err> {
515 let plot = self.simulate(netlist)?;
516 plot.to_dc_voltage_analysis()
517 }
518
519 fn run_op(&mut self, netlist: &str) -> Result<OpAnalysis, Self::Err> {
520 let plot = self.simulate(netlist)?;
521 plot.to_op_analysis()
522 }
523
524 fn run_tran(&mut self, netlist: &str) -> Result<TranAnalysis, Self::Err> {
525 let plot = self.simulate(netlist)?;
526 plot.to_tran_analysis()
527 }
528
529 fn run_ac(&mut self, netlist: &str) -> Result<AcAnalysis, Self::Err> {
530 let plot = self.simulate(netlist)?;
531 plot.to_ac_analysis()
532 }
533}
534
535#[allow(unused)]
536#[cfg(test)]
537mod tests {
538 use reda_unit::{num, u};
539 use crate::simulate::ngspice::shared;
540
541 use super::*;
542 use std::ffi::CString;
543 use std::os::raw::{c_char, c_int};
544 use callback::DefaultNgSpiceSharedCallback;
545 use libloading::Symbol;
546
547 #[test]
548 fn test_load_lib() {
549 let mut ng = NgSpiceShared::default().expect("Failed to create NgSpiceShared");
550 ng.init().expect("Failed to init ngspice");
551 ng.exec_command("version");
552
553 }
554
555 #[test]
556 fn test_use_shared() {
557 let mut ng = NgSpiceShared::default().expect("Failed to create NgSpiceShared");
558 ng.init().expect("Failed to init ngspice");
559
560 let netlist = r#"
561* RC low-pass filter
562V1 in 0 DC 1
563R1 in out 1k
564C1 out 0 1u
565.IC V(out)=0
566.tran 1u 1m
567.end
568 "#;
569
570 ng.load_circuit(netlist).expect("Failed to load circuit");
571 ng.run(false).expect("Failed to run simulation");
572
573 let time = ng.api.get_vec_real_data("time")
574 .expect("Vec access failed")
575 .expect("Missing time vector");
576
577 let vout = ng.api.get_vec_real_data("v(out)")
578 .expect("Vec access failed")
579 .expect("Missing v(out) vector");
580
581 println!("v(out): {:?}", &vout);
582 println!("{}", time.len());
583 println!("{}", vout.len());
584 }
585
586 #[test]
587 fn test_tran_analysis() {
588 let mut ng = NgSpiceShared::default().expect("Failed to create NgSpiceShared");
589 ng.init().expect("Failed to init ngspice");
590
591 let netlist = r#"
592* RC low-pass filter
593V1 in 0 DC 1
594R1 in out 1k
595C1 out 0 1u
596.IC V(out)=0
597.tran 1u 1m
598.end
599 "#;
600
601 ng.load_circuit(netlist).expect("Failed to load circuit");
602 ng.run(false).expect("Failed to run simulation");
603
604 let plot_name = ng.api.cur_plot().expect("No current plot");
605 let plot = ng.get_plot(&plot_name).expect("plot");
606 let analysis = plot.to_tran_analysis().expect("ana");
607
608 println!("{}", analysis.time.len());
609
610 println!("node:");
611 for (name, values) in analysis.nodes.iter() {
612 println!("{}", name);
613 }
614
615 println!("branches:");
616 for (name, values) in analysis.branches.iter() {
617 println!("{}", name);
618 }
619
620 println!("internal_parameters:");
621 for (name, values) in analysis.internal_parameters.iter() {
622 println!("{}", name);
623 }
624
625 println!("{}", analysis.get_voltage_at("out", u!(200 us)).unwrap());
626 }
627
628 #[test]
629 fn test_dc_analysis() {
630 let mut ng = NgSpiceShared::default()
631 .expect("Failed to create NgSpiceShared");
632 ng.init().expect("Failed to init ngspice");
633
634 let netlist = r#"
635 * Simple DC Sweep
636 V1 in 0 DC 0
637 R1 in out 2k
638 R2 out 0 1k
639 .dc V1 0 5 0.1
640 .end
641 "#;
642
643 ng.load_circuit(netlist).expect("Failed to load circuit");
644 ng.run(false).expect("Failed to run simulation");
645
646 let plot_name = ng.api.cur_plot().expect("No current plot");
647 let plot = ng.get_plot(&plot_name).expect("Failed to get plot");
648
649 let analysis = plot.to_dc_voltage_analysis().expect("Failed to convert to DC analysis");
650
651 println!("Sweep points: {}", analysis.sweep.len());
652
653 println!("Nodes:");
654 for (name, _) in analysis.nodes.iter() {
655 println!(" {}", name);
656 }
657
658 println!("Branches:");
659 for (name, _) in analysis.branches.iter() {
660 println!(" {}", name);
661 }
662
663 println!("Internal parameters:");
664 for (name, _) in analysis.internal_parameters.iter() {
665 println!(" {}", name);
666 }
667
668 let vout = analysis.get_voltage_at("out", u!(2.0 V));
670 match vout {
671 Some(val) => println!("Voltage at out when V1=2.0V: {} V", val.to_f64()),
672 None => println!("No matching voltage value found for V1=2.0V"),
673 }
674 }
675
676 #[test]
677 fn test_operating_point_analysis() {
678 let mut ng =
679 NgSpiceShared::default()
680 .expect("Failed to create NgSpiceShared");
681 ng.init().expect("Failed to init ngspice");
682
683 let netlist = r#"
684 * Operating Point Test
685 V1 in 0 DC 5
686 R1 in out 1k
687 R2 out 0 2k
688 .op
689 .end
690 "#;
691
692 ng.load_circuit(netlist).expect("Failed to load circuit");
693 ng.run(false).expect("Failed to run simulation");
694
695 let plot_name = ng.api.cur_plot().expect("No current plot");
696 let plot = ng.get_plot(&plot_name).expect("Failed to get plot");
697
698 let analysis = plot
699 .to_op_analysis()
700 .expect("Failed to convert to operating point analysis");
701
702 println!("Nodes:");
703 for (name, values) in &analysis.nodes {
704 println!(" {}: {:?}", name, values);
705 }
706
707 println!("Branches:");
708 for (name, values) in &analysis.branches {
709 println!(" {}: {:?}", name, values);
710 }
711
712 println!("Internal parameters:");
713 for (name, values) in &analysis.internal_parameters {
714 println!(" {}: {:?}", name, values);
715 }
716
717 let vout = analysis.nodes.get("out").unwrap();
719 println!("{}", vout);
720 }
721
722}