Source code for denspp.offline.analog.pyspice_handler

import numpy as np
import matplotlib.pyplot as plt
import PySpice.Logging.Logging as Logging
from PySpice.Spice.Netlist import Circuit, Netlist
from PySpice.Spice.NgSpice.Shared import NgSpiceShared
from denspp.offline.plot_helper import scale_auto_value, save_figure


[docs] class PySpiceModels: def __init__(self): self.vcm = 0.0
[docs] def resistor(self, value: float) -> Circuit: """PySpice model for handling a resistor in simulation :param value: The resistance of the circuit. :return: The circuit model including voltage sources (input, common mode) """ circuit0 = Circuit("Resistive Load") circuit0.R(1, 'input', 'output', value) circuit0.V('cm', 'output', circuit0.gnd, self.vcm) return circuit0
[docs] def diode_1n4148(self) -> Circuit: """PySpice model for using a 1N4148 diode in simulation :return: The circuit model including voltage sources (input, common mode) """ circuit0 = Circuit("Diode_1N4148") circuit0.model('1N4148', 'D', IS=4.352e-9, N=1.906, BV=110, IBV=1e-5, RS=0.6458, CJO=7.048e-13, V=0.869, M=0.03, FC=0.5, TT=3.48E-9) circuit0.Diode(0, 'input', 'middle', model='1N4148') circuit0.R(0, 'middle', 'output', 100) circuit0.V('cm', 'output', circuit0.gnd, self.vcm) return circuit0
[docs] def resistive_diode(self, r0: float=1e3, Uth: float=0.7, IS0: float=4e-12, N: float=2.0) -> Circuit: """PySpice model for handling a custom-made diode (series) in simulation :param r0: The resistance of the diode. :param Uth: Threshold voltage of the diode. :param IS0: Saturation current of the diode. :param N: Nonlinear factor of the diode. :return: The circuit model including voltage sources (input, common mode) """ circuit0 = Circuit("Resistive Diode") circuit0.model('myDiode', 'D', IS=IS0, RS=0, N=N, VJ=Uth, BV=10, IBV=1e-12, ) circuit0.R(1, 'input', 'middle', r0) circuit0.Diode(0, 'middle', 'output', model='myDiode') circuit0.V('cm', 'output', circuit0.gnd, self.vcm) return circuit0
[docs] def resistive_diode_antiparallel(self, r0: float=1e3, Uth: float=0.7, IS0: float=4e-12, N: float=2.0) -> Circuit: """PySpice model for handling a custom-made diode (antiparallel) in simulation :param r0: The resistance of the diode. :param Uth: Threshold voltage of the diode. :param IS0: Saturation current of the diode. :param N: Nonlinear factor of the diode. :return: The circuit model including voltage sources (input, common mode) """ circuit0 = Circuit("Resistive Diode (Antiparallel)") circuit0.model('myDiode', 'D', IS=IS0, RS=0, N=N, VJ=Uth, BV=10, IBV=1e-12) circuit0.R(1, 'input', 'middle', r0) circuit0.Diode(0, 'middle', 'output', model='myDiode') circuit0.Diode(1, 'output', 'middle', model='myDiode') circuit0.V('cm', 'output', circuit0.gnd, self.vcm) return circuit0
[docs] def simple_randles_model(self, r_tis: float=10e3, r_far: float=100e6, c_dl: float=10e-9) -> Circuit: """PySpice model for handling a Randles model in simulation :param r_tis: Tissue resistance. :param r_far: Faraday resistance. :param c_dl: Double layer capacity. :return: The circuit model including voltage sources (input, common mode) """ circuit0 = Circuit("Simple Randles Model") circuit0.R(1, 'input', 'middle', r_tis) circuit0.R(2, 'middle', 'output', r_far) circuit0.C(1, 'middle', 'output', c_dl) circuit0.V('cm', 'output', circuit0.gnd, self.vcm) return circuit0
[docs] def voltage_divider(self, r_0: float=10e3, r_1: float=10e3, r_load: float=10e12, c_load: float=0.0) -> Circuit: """PySpice model for handling a custom-made diode (series) in simulation :param r0: The resistance of the diode. :param Uth: Threshold voltage of the diode. :param IS0: Saturation current of the diode. :param N: Nonlinear factor of the diode. :return: The circuit model including voltage sources (input, common mode) """ circuit0 = Circuit("Voltage Divider with Load") circuit0.R(1, 'input', 'output', r_0) circuit0.R(2, 'output', 'ref', r_1) circuit0.R(3, 'output', 'ref', r_load) if not c_load == 0.0: circuit0.C(0, 'output', 'ref', c_load) circuit0.V(0, 'ref', circuit0.gnd, self.vcm) return circuit0
############################################################################ def _add_method(o: object, method, name) -> None: """Changing the functionality of an attribute""" setattr(o, name, method.__get__(o, type(o))) def _create_arbfwg(spice_instance: NgSpiceShared, waveform: np.ndarray, f_samp: float) -> None: """Bugfixing for replacement a PySPICE function""" def get_vsrc_data(self, voltage, time, node, ngspice_id) -> int: """Internal NgSpice function for simulation""" self.__logger.debug('ngspice_id-{} get_vsrc_data @{} node {}'.format(ngspice_id, time, node)) index = int(time * f_samp) % len(waveform) voltage[0] = waveform[index] return 0 _add_method(spice_instance, get_vsrc_data, "get_vsrc_data") def _clear_arbfwg(spice_instance: NgSpiceShared) -> None: """Function for getting the voltage source value for making transient analysis""" def get_vsrc_data(self, voltage, time, node, ngspice_id) -> int: """Internal NgSpice function for simulation""" self.__logger.debug('ngspice_id-{} get_vsrc_data @{} node {}'.format(ngspice_id, time, node)) return 0 _add_method(spice_instance, get_vsrc_data, "get_vsrc_data")
[docs] def do_bugfix_clone_circuit(circuits_netlist: Netlist) -> None: """Bugfix function for cloning circuits successful""" def copy_to(self, netlist: Netlist) -> Netlist: for subcircuit in self.subcircuits: netlist.subcircuit(subcircuit) for element in self.elements: element.copy_to(netlist) for name, model in self._models.items(): netlist._models[name] = model netlist.raw_spice = str(self.raw_spice) return netlist _add_method(circuits_netlist, copy_to, "copy_to")
############################################################################
[docs] class PySpiceHandler: _circuit: Circuit _is_input_voltage: bool __results: dict def __init__(self, temperature: float=300.0, input_voltage: bool=True) -> None: """Rewritten API for using PySPICE in simulation (Git Tutorial: https://github.com/benedictjones/engineeringthings-pyspice, YouTube: https://www.youtube.com/watch?v=62BOYx1UCfs&list=PL97KTNA1aBe1QXCcVIbZZ76B2f0Sx2Snh) Args: temperature: Given temperature for simulation in [K] [Default: 300.0 K] input_voltage: Defining if input is a voltage (True) or current (False) Returns: None """ from PySpice import __version__ from PySpice.Spice.NgSpice import NGSPICE_SUPPORTED_VERSION self.__version_pyspice = __version__ self.__version_ngspice = NGSPICE_SUPPORTED_VERSION self.__results = dict() self._is_input_voltage = input_voltage self._used_temp = temperature self._circuit = Circuit("Test") self._run_ite = 0 self._arbitrary_signal_ng_spice_instance = NgSpiceShared.new_instance() self.logger = Logging.setup_logging() @property def __calc_temp_in_celsius(self) -> float: """Translating the temperature value from Kelvin [K] to Grad Celsius [°C]""" return self._used_temp - 273.15
[docs] def get_ngspice_version(self) -> str: """Getting the version of used NGspice in PySPICE""" text = f"PySpice v{self.__version_pyspice} with NGSpice v{self.__version_ngspice}" return text
[docs] def set_src_mode(self, do_voltage: bool) -> None: """Setting the Source Mode of Input Source [0: Current, 1: Voltage]""" self._is_input_voltage = do_voltage
[docs] def load_circuit_model(self, circuit_used: Circuit) -> None: """Loading an external circuit SPICE model""" self._circuit = Circuit(circuit_used.title) do_bugfix_clone_circuit(circuit_used) circuit_used.copy_to(self._circuit)
[docs] def print_spice_circuit(self) -> None: """Printing the circuit in SPICE format""" print("\n======================================================") print("\t CIRCUIT SPICE IMPLEMENTATION") print("======================================================") print(self._circuit)
############################################################################
[docs] def do_dc_simulation(self, value: float, initial_value: float=0.0) -> dict: """Performing the DC or Operating Point Simulation Args: value: Specified value of the input voltage or current source initial_value: Applied initial value [Default: 0.0] Returns: Dictionary with node voltages """ if self._is_input_voltage: self._circuit.V('input', 'input', self._circuit.gnd, value) self._circuit.Vinput.minus.add_current_probe(self._circuit) else: self._circuit.I('input', self._circuit.gnd, 'input', value) self._circuit.Iinput.plus.add_current_probe(self._circuit) simulator = self._circuit.simulator(temperature=self.__calc_temp_in_celsius, nominal_temperature=self.__calc_temp_in_celsius) if initial_value: simulator.initial_condition(point=initial_value) analysis = simulator.operating_point() results = dict() for node in analysis.nodes.values(): results.update({str(node): float(node)}) return self.__get_results(0, analysis)
############################################################################
[docs] def do_dc_sweep_simulation(self, start_dc: float, stop_dc: float, step_dc: float, initial_value: float=0.0) -> dict: """Performing the DC or Operating Point Simulation Args: start_dc: Starting point of DC Sweep stop_dc: End point of DC Sweep step_dc: Step size of DC Sweep initial_value: Applied initial value [Default: 0.0] Returns: NGSpice dictionary with simulation results [optional] """ if self._is_input_voltage: self._circuit.V('input', 'input', self._circuit.gnd, 0) self._circuit.Vinput.minus.add_current_probe(self._circuit) else: self._circuit.I('input', self._circuit.gnd, 'input', 0) self._circuit.Iinput.plus.add_current_probe(self._circuit) simulator = self._circuit.simulator(temperature=self.__calc_temp_in_celsius, nominal_temperature=self.__calc_temp_in_celsius) if initial_value: simulator.initial_condition(point=initial_value) if self._is_input_voltage: results = simulator.dc(Vinput=slice(start_dc, stop_dc, step_dc)) else: results = simulator.dc(Iinput=slice(start_dc, stop_dc, step_dc)) del self.__results self.__results = self.__get_results(1, results) return self.__results
############################################################################
[docs] def do_ac_simulation(self, start_freq: float, stop_freq: float, num_points: int, amplitude: float=1.0, initial_value: float=0.0) -> dict: """Performing the DC or Operating Point Simulation Args: start_freq: Frequency value for starting point stop_freq: Frequency value for end point num_points: Number of repetitions amplitude: Amplitude of input signal [Default: 1.0] initial_value: Applied initial value [Default: 0.0] Returns: NGSpice dictionary with simulation results [optional] """ if self._is_input_voltage: self._circuit.SinusoidalVoltageSource('input', 'input', self._circuit.gnd, amplitude) self._circuit.Vinput.minus.add_current_probe(self._circuit) else: self._circuit.SinusoidalCurrentSource('input', self._circuit.gnd, 'input', amplitude) self._circuit.Iinput.plus.add_current_probe(self._circuit) simulator = self._circuit.simulator(temperature=self.__calc_temp_in_celsius, nominal_temperature=self.__calc_temp_in_celsius) if initial_value: simulator.initial_condition(point=initial_value) results = simulator.ac(start_frequency=start_freq, stop_frequency=stop_freq, number_of_points=num_points, variation='dec') del self.__results self.__results = self.__get_results(2, results) return self.__results
############################################################################
[docs] def do_transient_pulse_simulation(self, neg_value: float, pos_value: float, pulse_width: float, pulse_period: float, t_sim: float, f_samp: float, initial_value: float=0.0) -> dict: """Performing the Transient Simulation with Pulse Signal Args: neg_value: Pos. peak value of the pulse signal [V or A] pos_value: Neg. peak value of the pulse signal [V or A] pulse_width: Pulse width [s] pulse_period: Period of the pulses [s] t_sim: Total simulation time [s] f_samp: Sampling frequency [Hz] initial_value: Applied initial value [Default: 0.0] Returns: NGSpice dictionary with simulation results [optional] """ if self._is_input_voltage: self._circuit.PulseVoltageSource('input', 'input', self._circuit.gnd, initial_value=neg_value, pulsed_value=pos_value, pulse_width=pulse_width, period=pulse_period, rise_time=1 / f_samp, fall_time=1 / f_samp) self._circuit.Vinput.minus.add_current_probe(self._circuit) else: self._circuit.PulseCurrentSource('input', self._circuit.gnd, 'input', initial_value=neg_value, pulsed_value=pos_value, pulse_width=pulse_width, period=pulse_period, rise_time=1 / f_samp, fall_time=1 / f_samp) self._circuit.Iinput.plus.add_current_probe(self._circuit) simulator = self._circuit.simulator(temperature=self.__calc_temp_in_celsius, nominal_temperature=self.__calc_temp_in_celsius) if initial_value: simulator.initial_condition(point=initial_value) results = simulator.transient(step_time=1 / f_samp, end_time=t_sim) del self.__results self.__results = self.__get_results(3, results) return self.__results
############################################################################
[docs] def do_transient_sinusoidal_simulation(self, amp: float, freq: float, t_sim: float, f_samp: float, t_dly: float=0.0, offset: float=0.0, initial_value: float=0.0) -> dict: """Performing the Transient Simulation with Sinusoidal Signal Args: amp: Amplitude of sinusoidal waveform [V or A] freq: Frequency of sinusoidal waveform [Hz] t_sim: Total simulation time [s] f_samp: Sampling frequency [Hz] t_dly: Applied time delay to signal [s] [Default: 0.0] offset: Applied offset on signal [V or A] [Default: 0.0] initial_value: Applied initial value [Default: 0.0] Returns: NGSpice dictionary with simulation results [optional] """ if self._is_input_voltage: self._circuit.SinusoidalVoltageSource( 'input', 'input', self._circuit.gnd, amplitude=amp, delay=t_dly, offset=offset, frequency=freq ) self._circuit.Vinput.minus.add_current_probe(self._circuit) else: self._circuit.SinusoidalCurrentSource( 'input', self._circuit.gnd, 'input', amplitude=amp, delay=t_dly, offset=offset, frequency=freq ) self._circuit.Iinput.plus.add_current_probe(self._circuit) simulator = self._circuit.simulator(temperature=self.__calc_temp_in_celsius, nominal_temperature=self.__calc_temp_in_celsius) if initial_value: simulator.initial_condition(point=initial_value) results = simulator.transient(step_time=1 / f_samp, end_time=t_sim) del self.__results self.__results = self.__get_results(3, results) return self.__results
############################################################################
[docs] def do_transient_arbitrary_simulation(self, signal: np.ndarray, t_end: float, f_samp: float, initial_value: float=0.0, trans_value: float=1.0) -> dict: """Performing the Transient Simulation with Arbitrary Signal Waveform Args: signal: Numpy array with transient custom-made signal t_end: Total simulation time [s] f_samp: Sampling frequency [Hz] initial_value: Applied initial value [Default: 0.0] trans_value: Transcondunctance value [Default: 1 A/V] Returns: NGSpice dictionary with simulation results [optional] """ # --- Definition of energy source if self._is_input_voltage: self._circuit.V('input', 'input', self._circuit.gnd, 'dc 0 external') self._circuit.Vinput.minus.add_current_probe(self._circuit) else: self._circuit.V('data', 'src', self._circuit.gnd, 'dc 0 external') self._circuit.VCCS('input', self._circuit.gnd, 'input0', 'src', self._circuit.gnd, trans_value) self._circuit.R('sens', 'input0', 'input', 0.0) self._circuit.Rsens.plus.add_current_probe(self._circuit) # --- Generating instance for using arbitrary waveforms in SPICE transient simulation _create_arbfwg(self._arbitrary_signal_ng_spice_instance, signal, f_samp) data = self._arbitrary_signal_ng_spice_instance # --- Prepare and Run simulation simulator = self._circuit.simulator(temperature=self.__calc_temp_in_celsius, nominal_temperature=self.__calc_temp_in_celsius, simulator='ngspice-shared', ngspice_shared=data) if initial_value: simulator.initial_condition(point=initial_value) results = simulator.transient(step_time=1 / f_samp, end_time=t_end) # --- Process results del self.__results self.__results = self.__get_results(4, results) _clear_arbfwg(self._arbitrary_signal_ng_spice_instance) return self.__results
############################################################################
[docs] def create_dummy_signal(self, t_sim: float, f_samp: float, offset: float=0.0) -> [np.ndarray, np.ndarray]: """Creating a dummy function for transient simulation Args: t_sim: Simulation time [s] f_samp: Sampling frequency [Hz] offset: Offset on signal [Default: 0.0] Returns: Two numpy arrays with time vector and signal vector """ freq_used = [100, 300, 500] amp0_used = [1.0, 0.25, 0.66] time0 = np.linspace(0, t_sim, int(t_sim * f_samp), endpoint=True) sig_out = np.zeros(time0.shape) + offset for freq, amp in zip(freq_used, amp0_used): sig_out += amp * np.sin(2 * np.pi * freq * time0) return time0, sig_out
############################################################################ def __get_results(self, mode: int, data) -> dict: """Getting the results from already runned SPICE analysis Args: mode: Selection mode for getting results (0 = Operating Point, 1 = DC Sweep, 2 = AC Sweep, 3 = Transient) Returns: Dictionary with entries "v_in", "v_out", "i_in", "time", "freq" """ output_dict = dict() cur_in_key0 = 'vvinput_minus' if self._is_input_voltage else 'viinput_plus' cur_in_key1 = 'vvinput_minus' if self._is_input_voltage else 'vrsens_plus' match mode: case 0: # DC Operating Point Analysis output_dict.update({"v_in": np.array(data.input)}) output_dict.update({"i_in": np.array(data.branches[cur_in_key0])}) case 1: # DC Sweep Analysis output_dict.update({"v_in": np.array(data.input)}) output_dict.update({"i_in": np.array(data.branches[cur_in_key0])}) case 2: # AC Sweep Analysis output_dict.update({"freq": np.array(data.frequency)}) output_dict.update({"v_in": np.array(data.input)}) output_dict.update({"i_in": np.array(data.branches[cur_in_key0])}) output_dict.update({"v_out": np.array(data.output)}) case 3: # Transient Simulation (Pulse, Sinusoidal) output_dict.update({"time": np.array(data.time)}) output_dict.update({"v_in": np.array(data.input)}) output_dict.update({"i_in": np.array(data.branches[cur_in_key0])}) output_dict.update({"v_out": np.array(data.output)}) case 4: # Transient Simulation (Pulse, Sinusoidal) output_dict.update({"time": np.array(data.time)}) output_dict.update({"v_in": np.array(data.input)}) output_dict.update({"i_in": np.array(data.branches[cur_in_key1])}) output_dict.update({"v_out": np.array(data.output)}) return output_dict ############################################################################
[docs] def plot_iv_curve(self, do_log: bool=False, path2save: str='', show_plot: bool=False) -> None: """Plotting the I-V relationship/curve of investigated circuit (taking v_in and i_in) Args: do_log: Do a logarithmic plotting on y-axis path2save: Optional string for plotting [Default: '' for non-plotting] show_plot: Blocking plots for showing [Default: False] Returns: None """ # --- Getting data results = self.__results u_in = results["v_in"] scale_u, units_u = scale_auto_value(u_in) i_out = results["i_in"] scale_i, units_i = scale_auto_value(i_out) # --- Plotting plt.figure() if not do_log: plt.plot(scale_u * u_in, scale_i * i_out, 'k', linewidth=1, marker='.') else: plt.semilogy(scale_u * u_in, scale_i * np.abs(i_out), 'k', linewidth=1, marker='.') plt.xlabel(fr'Voltage $U$ / {units_u}V') plt.ylabel(fr'Current $I$ / {units_i}A') plt.xlim([scale_u * u_in[0], scale_u * u_in[-1]]) plt.tight_layout() plt.grid() if path2save: save_figure(plt, path2save, 'pyspice_dc_result', formats=['svg']) if show_plot: plt.show(block=True)
[docs] def plot_bodeplot(self, mode: int=0, path2save: str='', show_plot: str=False) -> None: """Plotting the Bode Diagram (mode == 0) or Impedance Plot (mode == 1) of investigated circuit Args: mode: Mode selection (0 = Bode diagram, 1 = Impedance plot) path2save: Optional string for plotting [Default: '' for non-plotting] show_plot: Blocking plots for showing [Default: False] Returns: None """ # --- Getting data results = self.__results freq = results["freq"] transfer_function = results["v_out"] / results["v_in"] if mode == 0 else results["v_in"] / results["i_in"] # --- Plotting fig, axs = plt.subplots(2, 1, sharex="all") axs[0].semilogx(freq, 20 * np.log10(np.abs(transfer_function)), 'k', linewidth=1, marker='.', label="Gain") axs[0].set_ylabel(r"Gain $v_U$ / dB") axs[0].set_xlim([freq[0], freq[-1]]) axs[1].semilogx(freq, np.angle(transfer_function, deg=True), 'r', linewidth=1, marker='.', label="Phase") axs[1].set_ylabel(r"Phase $\alpha$ / °") axs[1].set_xlabel(r'Frequency $f$ / Hz') axs[1].set_xlim([freq[0], freq[-1]]) for ax in axs: ax.set_xlim([freq[0], freq[-1]]) ax.grid(which='both', linestyle='--') plt.tight_layout() plt.subplots_adjust(hspace=0.05) if path2save: save_figure(plt, path2save, 'pyspice_ac_result', ['svg']) if show_plot: plt.show(block=True)
[docs] def plot_transient(self, path2save: str='', show_plot: bool=False) -> None: """Plotting the results of Transient Simulation of investigated circuit Args: path2save: Optional string for plotting [Default: '' for non-plotting] show_plot: Blocking plots for showing [Default: False] Returns: None """ # --- Getting data results = self.__results time = results["time"] scale_t, units_t = scale_auto_value(time) v_sig = [results["v_in"], results["v_out"]] scale_u, units_u = scale_auto_value(np.concatenate((v_sig[0], v_sig[1]), axis=0)) v_label = ["Input", "Output"] i_in = results["i_in"] scale_i, units_i = scale_auto_value(i_in) # --- Plotting fig, ax1 = plt.subplots() idx = 0 for sig, label in zip(v_sig, v_label): if idx == 0: lns = ax1.plot(scale_t * time, scale_u * sig, linewidth=1, label=label) else: lns += ax1.plot(scale_t * time, scale_u * sig, linewidth=1, label=label) idx += 1 ax1.set_ylabel(fr"Voltage $U_x$ / {units_u}V") ax1.set_xlabel(fr'Time $t$ / {units_t}s') ax2 = ax1.twinx() lns += ax2.plot(scale_t * time, scale_i * i_in, 'r--', linewidth=1, label='Current') ax2.set_ylabel(fr"Current $I$ / {units_i}A") ax2.set_xlim([scale_t * time[0], scale_t * time[-1]]) # added these three lines labs = [l.get_label() for l in lns] ax2.legend(lns, labs, loc=0) plt.tight_layout() plt.grid() if path2save: save_figure(plt, path2save, 'pyspice_transient_result', ['svg']) if show_plot: plt.show(block=True)