Skip to main content

reda_spice/simulate/ngspice/shared/
mod.rs

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    /// Run the simulation
216    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
332/// Property
333impl 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.destroy_all()?;
503        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        // 测试一个节点电压(如 "out")在扫到 V1 = 2.0V 时的值
669        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        // 检查输出节点的电压值
718        let vout = analysis.nodes.get("out").unwrap();
719        println!("{}", vout);
720    }
721    
722}