import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from inspect import getfullargspec
from logging import getLogger, Logger
from PySpice.Logging import 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
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]
@dataclass
class SettingsPySpice:
"""Individual data class to configure the electrical device
Attributes:
type: Type of electrical device ['R': resistor, 'C': capacitor, 'L': inductor, 'RDs': Resistive diode]
fs_ana: Sampling frequency of input [Hz]
noise_en: Enable noise on output [True / False]
params_use: Dictionary with used parameters of the circuit/model
temp_kelvin:Temperature [K]
input_volt: Boolean if input is voltage (True) or current (False)
"""
type: str
fs_ana: float
noise_en: bool
params_use: dict
temp_kelvin: float
input_volt: bool
@property
def temp_celsius(self) -> float:
"""Translating the temperature value from Kelvin [K] to Grad Celsius [°C]"""
return self.temp_kelvin - 273.15
RecommendedSettingsDEV = SettingsPySpice(
type='R',
fs_ana=50e3,
noise_en=False,
params_use={'r': 100e3},
temp_kelvin=300,
input_volt=True
)
############################################################################
[docs]
class PySpiceHandler:
_logger: Logger
_type_device: dict = dict()
_circuit: Circuit = Circuit("Test")
_results: dict = dict()
_run_ite: int = 0
_sim_time: float = 1.0
vcm: float = 0.0
_settings: SettingsPySpice
def __init__(self, settings: SettingsPySpice) -> 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:
settings: Dataclass SettingsPySpice with settings for simulation
Returns:
None
"""
self._logger = Logging.setup_logging()
self._settings = settings
def _register_device(self, short_label: str, description: str, func_circ) -> None:
"""Function for registering an electrical device to library
:param short_label: String with short label of the device (e.g. 'R')
:param description: Short description of device type (e.g. 'Resistor')
:param func_circ: Function to the implemented SPICE circuit for operation
"""
param = getfullargspec(func_circ)[0][1:]
self._logger.debug(f"Registering device: {short_label} ({description}) with params: {param}")
self._type_device.update({short_label: {'desp': description, 'param': param, 'circ': func_circ}})
def _load_circuit_model(self) -> None:
"""Loading an external circuit SPICE model"""
if self._settings.type in self._type_device.keys():
if [key for key in self._settings.params_use.keys()] == self._type_device[self._settings.type]['param']:
circuit_loaded = self._type_device[self._settings.type]['circ'](**self._settings.params_use)
else:
raise NotImplementedError(f"Model Params are unequal: Need {self._type_device[self._settings.type]['param']} is not supported")
else:
raise NotImplementedError(f"Type {self._settings.type} is not supported (only {self._type_device.keys()})")
self._circuit = Circuit(circuit_loaded.title)
do_bugfix_clone_circuit(circuit_loaded)
circuit_loaded.copy_to(self._circuit)
[docs]
@staticmethod
def get_ngspice_version() -> str:
"""Getting the version of used NGspice in PySPICE"""
from PySpice import __version__
from PySpice.Spice.NgSpice import NGSPICE_SUPPORTED_VERSION
return f"PySpice v{__version__} with NGSpice v{NGSPICE_SUPPORTED_VERSION}"
[docs]
def set_simulation_duration(self, sim_time: float) -> None:
"""Defining the simulation duration for SPICE simulation"""
self._sim_time = sim_time
[docs]
def set_src_mode(self, do_voltage: bool) -> None:
"""Setting the Source Mode of Input Source [0: Current, 1: Voltage]"""
self._settings.input_volt = do_voltage
[docs]
def print_spice_circuit(self) -> str:
"""Printing the circuit in SPICE format"""
self._logger.info("======================================================")
self._logger.info("\tCIRCUIT SPICE IMPLEMENTATION")
self._logger.info("======================================================")
self._logger.info(self._circuit)
return str(self._circuit)
[docs]
def print_types(self) -> list:
"""Print electrical types in terminal"""
self._logger.info("===========================================")
self._logger.info("\tAvailable types of electrical devices")
self._logger.info("===========================================")
methods = list()
for idx, type in enumerate(self._type_device.keys()):
self._logger.info(f"\t#{idx:03d}: {type} = {self._type_device[type]['desp']}")
methods.append(f"{type} = {self._type_device[type]['desp']} (params = {self._type_device[type]['param']})")
return methods
############################################################################
[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
"""
self._load_circuit_model()
if self._settings.input_volt:
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._settings.temp_celsius, nominal_temperature=self._settings.temp_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 get_current(self, u_top: np.ndarray | float, u_bot: np.ndarray | float) -> np.ndarray:
"""Getting the current response from electrical device
Args:
u_top: Applied voltage on top electrode [V]
u_bot: Applied voltage on bottom electrode [V]
Returns:
Corresponding current response
"""
self._load_circuit_model()
du = u_top - u_bot
self.set_src_mode(True)
if isinstance(du, float) or isinstance(du, int):
results = self.do_dc_simulation(du, initial_value=0.0)
i_out0 = results['i_in']
else:
self.set_simulation_duration(du.size / self._settings.fs_ana)
results = self.do_transient_arbitrary_simulation(du, self._sim_time, self._settings.fs_ana)
i_out0 = results['i_in']
num_dly = i_out0.size-du.size-1
i_out0 = i_out0[num_dly:-1]
return np.array(i_out0)
[docs]
def get_voltage(self, i_in: np.ndarray, u_inn: np.ndarray | float) -> np.ndarray:
"""Getting the voltage response from electrical device
Args:
i_in: Applied current input [A]
u_inn: Negative input | bottom electrode | reference voltage [V]
Returns:
Corresponding voltage response
"""
self._load_circuit_model()
if isinstance(i_in, float) or isinstance(u_inn, int):
vout = np.zeros((1,), dtype=float)
else:
vout = np.zeros(i_in.shape, dtype=float)
self.set_src_mode(False)
self.set_simulation_duration(i_in.size / self._settings.fs_ana)
results = self.do_transient_arbitrary_simulation(i_in, self._sim_time, self._settings.fs_ana)
vout = results['v_in'] + u_inn
num_dly = vout.size - i_in.size - 1
vout = vout[num_dly:]
return vout
############################################################################
[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]
"""
self._load_circuit_model()
if self._settings.input_volt:
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._settings.temp_celsius, nominal_temperature=self._settings.temp_celsius)
if initial_value:
simulator.initial_condition(point=initial_value)
if self._settings.input_volt:
results = simulator.dc(Vinput=slice(start_dc, stop_dc, step_dc))
else:
results = simulator.dc(Iinput=slice(start_dc, stop_dc, step_dc))
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]
"""
self._load_circuit_model()
if self._settings.input_volt:
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._settings.temp_celsius, nominal_temperature=self._settings.temp_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')
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, 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]
initial_value: Applied initial value [Default: 0.0]
Returns:
NGSpice dictionary with simulation results [optional]
"""
self._load_circuit_model()
if self._settings.input_volt:
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 / self._settings.fs_ana, fall_time=1 / self._settings.fs_ana)
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 / self._settings.fs_ana, fall_time=1 / self._settings.fs_ana)
self._circuit.Iinput.plus.add_current_probe(self._circuit)
simulator = self._circuit.simulator(temperature=self._settings.temp_celsius, nominal_temperature=self._settings.temp_celsius)
if initial_value:
simulator.initial_condition(point=initial_value)
results = simulator.transient(step_time=1 / self._settings.fs_ana, end_time=t_sim)
self._results = self.__get_results(3, results)
return self._results
############################################################################
[docs]
def do_transient_sinusoidal_simulation(self, amp: float, freq: float, t_sim: 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]
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]
"""
self._load_circuit_model()
if self._settings.input_volt:
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._settings.temp_celsius, nominal_temperature=self._settings.temp_celsius)
if initial_value:
simulator.initial_condition(point=initial_value)
results = simulator.transient(step_time=1 / self._settings.fs_ana, end_time=t_sim)
self._results = self.__get_results(3, results)
return self._results
############################################################################
[docs]
def do_transient_arbitrary_simulation(self, signal: np.ndarray, t_end: 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]
initial_value: Applied initial value [Default: 0.0]
trans_value: Transcondunctance value [Default: 1 A/V]
Returns:
NGSpice dictionary with simulation results [optional]
"""
arbitrary_signal = NgSpiceShared.new_instance()
self._load_circuit_model()
# --- Definition of energy source
if self._settings.input_volt:
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(arbitrary_signal, signal, self._settings.fs_ana)
data = arbitrary_signal
# --- Prepare and Run simulation
simulator = self._circuit.simulator(temperature=self._settings.temp_celsius,
nominal_temperature=self._settings.temp_celsius,
simulator='ngspice-shared', ngspice_shared=data)
if initial_value:
simulator.initial_condition(point=initial_value)
results = simulator.transient(step_time=1 / self._settings.fs_ana, end_time=t_end)
# --- Process results
_clear_arbfwg(arbitrary_signal)
self._results = self.__get_results(4, results)
return self._results
############################################################################
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._settings.input_volt else 'viinput_plus'
cur_in_key1 = 'vvinput_minus' if self._settings.input_volt 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)
[docs]
def plot_fit_curve(self, start_value: float=-5.0, stop_value: float=+5.0, step_size: float=0.1,
do_logy: bool=False, path2save: str='', show_plot: bool=False) -> None:
"""Plotting the output of the polynom fit function
Args:
start_value: Starting point of DC Sweep
stop_value: End point of DC Sweep
step_size: Step size of DC Sweep
do_logy: Do logarithmic plotting on y-scale
path2save: Path for saving the plot
show_plot: Showing and blocking the plot
Returns:
None
"""
self.set_src_mode(True)
self._load_circuit_model(self._type_device[self._settings.type]())
self.do_dc_sweep_simulation(start_value, stop_value, step_size)
self.plot_iv_curve(do_logy, path2save, show_plot)
[docs]
def create_dummy_signal(t_sim: float, f_samp: float, offset: float=0.0, freq_used: list=[100, 300, 500], freq_amp: list=[1.0, 0.25, 0.66]) -> [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
"""
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, freq_amp):
sig_out += amp * np.sin(2 * np.pi * freq * time0)
return time0, sig_out