diff --git a/.gitignore b/.gitignore index ce04b90be..7a5515adb 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,4 @@ tools/upload-www trash/ unit-test/test.py +PySpicePro.egg-info/ diff --git a/PySpice/Config/ConfigInstall.py b/PySpice/Config/ConfigInstall.py index 7b4e9d3e3..998d30974 100644 --- a/PySpice/Config/ConfigInstall.py +++ b/PySpice/Config/ConfigInstall.py @@ -5,7 +5,7 @@ #################################################################################################### -import PySpice.Tools.Path as PathTools # Fixme: why ? +from ..Tools import Path as PathTools #################################################################################################### diff --git a/PySpice/Logging/Logging.py b/PySpice/Logging/Logging.py index eab4a4f1e..abd81622e 100644 --- a/PySpice/Logging/Logging.py +++ b/PySpice/Logging/Logging.py @@ -44,7 +44,7 @@ def setup_logging(application_name='PySpice', """ logging_config_file_name = ConfigInstall.Logging.find(config_file) - logging_config = yaml.load(open(logging_config_file_name, 'r')) + logging_config = yaml.load(open(logging_config_file_name, 'r'), Loader=yaml.FullLoader) if ConfigInstall.OS.on_linux: # Fixme: \033 is not interpreted in YAML diff --git a/PySpice/Spice/NgSpice/Shared.py b/PySpice/Spice/NgSpice/Shared.py index 6885bc997..6c88fc8d7 100644 --- a/PySpice/Spice/NgSpice/Shared.py +++ b/PySpice/Spice/NgSpice/Shared.py @@ -47,9 +47,14 @@ #################################################################################################### import logging -import os +import os, shutil +import tempfile import platform import re +from weakref import WeakValueDictionary +import warnings +import gc + # import time import numpy as np @@ -57,7 +62,6 @@ #################################################################################################### from cffi import FFI -ffi = FFI() #################################################################################################### @@ -79,7 +83,15 @@ #################################################################################################### def ffi_string_utf8(x): - return ffi.string(x).decode('utf8') # Fixme: ascii ? + bytelist = [] + i = 0 + while True: + c=x[i] + if c == b'\0': + break + bytelist.append(c) + i+=1 + return b''.join(bytelist).decode('utf-8') #################################################################################################### @@ -306,7 +318,7 @@ def _to_transient_analysis(self): #################################################################################################### -class NgSpiceShared: +class NgSpiceShared(object): _logger = _module_logger.getChild('NgSpiceShared') @@ -317,43 +329,93 @@ class NgSpiceShared: ############################################## - _instances = {} - - @classmethod - def new_instance(cls, ngspice_id=0, send_data=False): + _instances=None + _ffis = {} + _ngspice_shared_dict = {} + _temp_dlls = {} + + def __new__(cls, ngspice_id=None, send_data=False): + """ + Only one instance exists per ngspice_id. All instances can be called in parallel but only one + callback function exists per instance (the fist one defined). The ngspice library has a limit + on the number of open libraries, do not open too many instances (50 as of writing these lines). + If a duplicate of an instance is required, use the ngspice_id of the previously created instance. + Use ngspice_id=None for auto-numbering of instances (recycling previously closed libraries). + Use get_id() to get the instance id. + """ + assert type(ngspice_id) is int if ngspice_id is not None else True, "ngspice_id must be an int" # Fixme: send_data - - if ngspice_id in cls._instances: - return cls._instances[ngspice_id] + if __class__._instances is None: + __class__._instances = WeakValueDictionary() + + if ngspice_id in __class__._instances: + warnings.warn("""This use case as not been tested as I had no use for it yet. +Creating multiple instances that points to the same library (dll) would be used to have +multiple circuit simulated in lockstep one to the other. The lockstep functionnality is +provided by the get_isrc_data and get_vsrc_data callback function.""") + return __class__._instances[ngspice_id] else: + if ngspice_id is None: # find an empty slot + gc.collect() # Force garbage collection to make sure all instances have been released + slot=0 # starting point + while True: + if slot not in __class__._instances: + break # found + slot+=1 + ngspice_id = slot # keep this id cls._logger.info("New instance for id {}".format(ngspice_id)) - instance = cls(ngspice_id=ngspice_id, send_data=send_data) - cls._instances[ngspice_id] = instance + instance = object.__new__(cls) + __class__._instances[ngspice_id] = instance + instance._ffi = FFI() + __class__._ffis[ngspice_id] = instance._ffi + instance._ngspice_id = ngspice_id + instance._load_library() return instance ############################################## - def __init__(self, ngspice_id=0, send_data=False): - - """ Set the *send_data* flag if you want to enable the output callback. - - Set the *ngspice_id* to an integer value if you want to run NgSpice in parallel. + def __init__(self, ngspice_id=None, send_data=False): """ - - self._ngspice_id = ngspice_id + Set the *send_data* flag if you want to enable the output callback. + """ + assert ngspice_id == self._ngspice_id if ngspice_id is not None else True, "__new__ must have been called first" + ### __init__ is not called for an instance copy self._stdout = [] self._stderr = [] - - self._load_library() + self._init_ngspice(send_data) self._is_running = False ############################################## - + + def __del__(self): + try: + self.quit() # this function generates a NameError + except NameError: + pass + self._ffi.dlclose(self._ngspice_shared) + del __class__._ngspice_shared_dict[self._ngspice_id] + del __class__._ffis[self._ngspice_id] + try: + del __class__._instances[self._ngspice_id] + except KeyError: + pass + try: + os.unlink(self._temp_dlls[self._ngspice_id]) + except: + "dlclose is not doing its job!" # do not know how to solve + pass + + def get_id(self): + return self._ngspice_id + def _load_library(self): + ############################################## + + ffi = self._ffi if ConfigInstall.OS.on_windows: # https://sourceforge.net/p/ngspice/discussion/133842/thread/1cece652/#4e32/5ab8/9027 @@ -366,20 +428,30 @@ def _load_library(self): with open(api_path) as f: ffi.cdef(f.read()) - if not self._ngspice_id: - library_prefix = '' - else: - library_prefix = '{}'.format(self._ngspice_id) - library_path = self.LIBRARY_PATH.format(library_prefix) - self._logger.debug('Load {}'.format(library_path)) - self._ngspice_shared = ffi.dlopen(library_path) - + library_path = self.LIBRARY_PATH.format('') + # create a new instance of the DLL as per ngspice docs about parallelization + temp_dll = os.path.join(tempfile.gettempdir(), "ngspice_"+ str(self._ngspice_id) +".dll") + try: + shutil.copy(library_path, temp_dll) + except: + pass # Already exist? + self._logger.debug('Load {}'.format(temp_dll)) + try: # TODO browse through dlls??? + __class__._ngspice_shared_dict[self._ngspice_id] = ffi.dlopen(temp_dll) # the file may be busy from other application + self._ngspice_shared = __class__._ngspice_shared_dict[self._ngspice_id] + except: # dll may be in use + try: + os.unlink(temp_dll) + except: + pass + raise + self.__class__._temp_dlls[self._ngspice_id] = temp_dll # Note: cannot yet execute command ############################################## def _init_ngspice(self, send_data): - + ffi = self._ffi self._send_char_c = ffi.callback('int (char *, int, void *)', self._send_char) self._send_stat_c = ffi.callback('int (char *, int, void *)', self._send_stat) self._exit_c = ffi.callback('int (int, bool, bool, int, void *)', self._exit) @@ -389,7 +461,7 @@ def _init_ngspice(self, send_data): if send_data: self._send_data_c = ffi.callback('int (pvecvaluesall, int, int, void *)', self._send_data) else: - self._send_data_c = ffi.NULL + self._send_data_c = FFI.NULL self._get_vsrc_data_c = ffi.callback('int (double *, double, char *, int, void *)', self._get_vsrc_data) self._get_isrc_data_c = ffi.callback('int (double *, double, char *, int, void *)', self._get_isrc_data) @@ -408,10 +480,10 @@ def _init_ngspice(self, send_data): raise NameError("Ngspice_Init returned {}".format(rc)) ngspice_id_c = ffi.new('int *', self._ngspice_id) - self._ngspice_id = ngspice_id_c # To prevent garbage collection + self._ngspice_id_c = ngspice_id_c # To prevent garbage collection rc = self._ngspice_shared.ngSpice_Init_Sync(self._get_vsrc_data_c, self._get_isrc_data_c, - ffi.NULL, # GetSyncData + FFI.NULL, # GetSyncData ngspice_id_c, self_c) if rc: @@ -441,8 +513,10 @@ def _init_ngspice(self, send_data): def _send_char(message_c, ngspice_id, user_data): """Callback for sending output from stdout, stderr to caller""" + if ngspice_id not in NgSpiceShared._ffis: + return 0 - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) message = ffi_string_utf8(message_c) prefix, _, content = message.partition(' ') @@ -460,7 +534,7 @@ def _send_char(message_c, ngspice_id, user_data): @staticmethod def _send_stat(message, ngspice_id, user_data): """Callback for simulation status to caller""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) return self.send_stat(ffi_string_utf8(message), ngspice_id) ############################################## @@ -468,7 +542,7 @@ def _send_stat(message, ngspice_id, user_data): @staticmethod def _exit(exit_status, immediate_unloding, quit_exit, ngspice_id, user_data): """Callback for asking for a reaction after controlled exit""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) self._logger.debug('ngspice_id-{} exit status={} immediate_unloding={} quit_exit={}'.format( ngspice_id, exit_status, @@ -481,7 +555,7 @@ def _exit(exit_status, immediate_unloding, quit_exit, ngspice_id, user_data): @staticmethod def _send_data(data, number_of_vectors, ngspice_id, user_data): """Callback to send back actual vector data""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) # self._logger.debug('ngspice_id-{} send_data [{}]'.format(ngspice_id, data.vecindex)) actual_vector_values = {} for i in range(int(number_of_vectors)): @@ -497,7 +571,7 @@ def _send_data(data, number_of_vectors, ngspice_id, user_data): @staticmethod def _send_init_data(data, ngspice_id, user_data): """Callback to send back initialization vector data""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) # if self._logger.isEnabledFor(logging.DEBUG): # self._logger.debug('ngspice_id-{} send_init_data'.format(ngspice_id)) # number_of_vectors = data.veccount @@ -510,7 +584,7 @@ def _send_init_data(data, ngspice_id, user_data): @staticmethod def _background_thread_running(is_running, ngspice_id, user_data): """Callback to indicate if background thread is runnin""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) self._logger.debug('ngspice_id-{} background_thread_running {}'.format(ngspice_id, is_running)) self._is_running = is_running @@ -519,7 +593,7 @@ def _background_thread_running(is_running, ngspice_id, user_data): @staticmethod def _get_vsrc_data(voltage, time, node, ngspice_id, user_data): """FFI Callback""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) return self.get_vsrc_data(voltage, time, ffi_string_utf8(node), ngspice_id) ############################################## @@ -527,7 +601,7 @@ def _get_vsrc_data(voltage, time, node, ngspice_id, user_data): @staticmethod def _get_isrc_data(current, time, node, ngspice_id, user_data): """FFI Callback""" - self = ffi.from_handle(user_data) + self = NgSpiceShared._ffis[ngspice_id].from_handle(user_data) return self.get_isrc_data(current, time, ffi_string_utf8(node), ngspice_id) ############################################## @@ -578,7 +652,7 @@ def _convert_string_array(array): strings = [] i = 0 while (True): - if array[i] == ffi.NULL: + if array[i] == FFI.NULL: break else: strings.append(ffi_string_utf8(array[i])) @@ -717,10 +791,11 @@ def _alter(self, command, device, kwargs): ############################################## - def alter_device(self, device, **kwargs): + def alter_device(self, device=None, **kwargs): """Alter device parameters""" - + if device is None: + device = '' self._alter('alter', device, kwargs) ############################################## @@ -961,11 +1036,12 @@ def where(self): def load_circuit(self, circuit): """Load the given circuit string.""" - + + ffi = NgSpiceShared._ffis[self._ngspice_id] circuit_lines = [line for line in str(circuit).split(os.linesep) if line] circuit_lines_keepalive = [ffi.new("char[]", line.encode('utf8')) for line in circuit_lines] - circuit_lines_keepalive += [ffi.NULL] + circuit_lines_keepalive += [FFI.NULL] circuit_array = ffi.new("char *[]", circuit_lines_keepalive) rc = self._ngspice_shared.ngSpice_Circ(circuit_array) if rc: @@ -1011,9 +1087,9 @@ def halt(self): ############################################## - def resume(self, background=True): + def resume(self, background=False): - """ Halt the simulation in the background thread. """ + """ Resume the simulation """ command = b'bg_resume' if background else b'resume' rc = self._ngspice_shared.ngSpice_Command(command) @@ -1081,11 +1157,12 @@ def plot(self, simulation, plot_name): # plot_name is for example dc with an integer suffix which is increment for each run + ffi = self._ffi plot = Plot(simulation, plot_name) all_vectors_c = self._ngspice_shared.ngSpice_AllVecs(plot_name.encode('utf8')) i = 0 while (True): - if all_vectors_c[i] == ffi.NULL: + if all_vectors_c[i] == FFI.NULL: break else: vector_name = ffi_string_utf8(all_vectors_c[i]) @@ -1098,7 +1175,7 @@ def plot(self, simulation, plot_name): # vector_type, # self._flags_to_str(vector_info.v_flags), # length)) - if vector_info.v_compdata == ffi.NULL: + if vector_info.v_compdata == FFI.NULL: # for k in xrange(length): # print(" [{}] {}".format(k, vector_info.v_realdata[k])) tmp_array = np.frombuffer(ffi.buffer(vector_info.v_realdata, length*8), dtype=np.float64) diff --git a/PySpice/Spice/Simulation.py b/PySpice/Spice/Simulation.py index 15ab2d0e7..47074cbc2 100644 --- a/PySpice/Spice/Simulation.py +++ b/PySpice/Spice/Simulation.py @@ -297,6 +297,37 @@ def to_list(self): #################################################################################################### +class MeasureParameters(AnalysisParameters): + + """ + This class defines measurements on analysis. + """ + + __analysis_name__ = 'meas' + + ############################################## + + def __init__(self, analysis_type, name, *args): + + if (str(analysis_type).upper() not in ('AC', 'DC', 'OP', 'TRAN', 'TF', 'NOISE')): + raise ValueError("Incorrect analysis type") + + self._parameters = [analysis_type, name, *args] + + ############################################## + + @property + def parameters(self): + return self._parameters + + ############################################## + + def to_list(self): + + return self._parameters + +#################################################################################################### + class CircuitSimulation: """Define and generate the spice instruction to perform a circuit simulation. @@ -315,6 +346,7 @@ def __init__(self, circuit, **kwargs): self._circuit = circuit self._options = {} # .options + self._measures = [] # .measure self._initial_condition = {} # .ic self._saved_nodes = set() self._analyses = {} @@ -441,6 +473,12 @@ def _add_analysis(self, analysis_parameters): ############################################## + def _add_measure(self, measure_parameters): + + self._measures.append(measure_parameters) + + ############################################## + def operating_point(self): """Compute the operating point of the circuit with capacitors open and inductors shorted.""" @@ -586,6 +624,28 @@ def ac(self, variation, number_of_points, start_frequency, stop_frequency): ############################################## + def measure(self, analysis_type, name, *args): + + """Add a measure in the circuit. + + Examples of usage:: + + simulator.measure('TRAN', 'tdiff', 'TRIG AT=10m', 'TARG v(n1) VAL=75.0 CROSS=1') + simulator.measure('tran', 'tdiff', 'TRIG AT=0m', f"TARG par('v(n1)-v(derate)') VAL=0 CROSS=1") + + Note: can be used with the .options AUTOSTOP to stop the simulation at Trigger. + Spice syntax: + + .. code:: spice + + .meas tran tdiff TRIG AT=0m TARG v(n1) VAL=75.0 CROSS=1 + + """ + + self._add_measure(MeasureParameters(analysis_type, name, *args)) + + ############################################## + def transient(self, step_time, end_time, start_time=0, max_time=None, use_initial_condition=False): @@ -646,6 +706,8 @@ def __str__(self): else: all_str = '' netlist += '.save ' + all_str + join_list(saved_nodes) + os.linesep + for measure_parameters in self._measures: + netlist += str(measure_parameters) + os.linesep for analysis_parameters in self._analyses.values(): netlist += str(analysis_parameters) + os.linesep netlist += '.end' + os.linesep diff --git a/README.html b/README.html deleted file mode 100644 index 501738ba1..000000000 --- a/README.html +++ /dev/null @@ -1,559 +0,0 @@ - - - -
- - -PySpice is a Python module which interface Python to the Ngspice and Xyce circuit -simulators.
-The documentation is available on the PySpice Home Page.
-Look at the installation section in the documentation.
-Authors: Fabrice Salvaire
----
-- support Ngspice 30 and Xyce 6.10
-- fixed NgSpice and Xyce support on Windows 10
-- bug fixes
-
-- --
-- Initial support of the Xyce simulator. Xyce is an open source, SPICE-compatible, -high-performance analog circuit simulator, capable of solving extremely large circuit problems -developed at Sandia National Laboratories. Xyce will make PySpice suitable for industry and -research use.
-- Fixed OSX support
-- Splitted G device
-- Implemented partially A XSPICE device
-- Implemented missing transmission line devices
-- Implemented high level current sources -Notice: Some classes were renamed !
-- Implemented node kwarg e.g.
-circuit.Q(1, base=1, collector=2, emitter=3, model='npn')- Implemented raw spice pass through (see User FAQ)
-- Implemented access to internal parameters (cf.
-save @device[parameter])- Implemented check for missing ground node
-- Implemented a way to disable an element and clone netlist
-- Improved SPICE parser
-- Improved unit support:
--
-- Implemented unit prefix cast U_μV(U_mV(1)) to easily convert values
-- Added U_mV, ... shortcuts
-- Added Numpy array support to unit, see UnitValues Notice: this new feature could be buggy !!!
-- Rebased WaveForm to UnitValues
-- Fixed node order so as to not confuse users Now PySpice matches SPICE order for two ports elements !
-- Fixed device shortcuts in Netlist class
-- Fixed model kwarg for BJT Notice: it must be passed exclusively as kwarg !
-- Fixed subcircuit nesting
-- Outsourced documentation generator to Pyterate
-- Updated setup.py for wheel
-
---
-- Enhanced shared mode
-- Shared mode is now set as default on Linux
-
---
-- Bump version to v1.0.0 since it just works!
-- Support Windows platform using Ngspice shared mode
-- Fixed shared mode
-- Fixed and completed Spice parser : tested on example's libraries
-
---
-- Fixed Spice parser for lower case device prefix.
-
---
-- Git repository cleanup: filtered generated doc and useless files so as to shrink the repository size.
-- Improved documentation generator: Implemented
-formatfor RST content and Tikz figure.- Improved unit support: It implements now the International System of Units. -And we can now use unit helper like
-u_mVor compute the value of1.2@u_kΩ / 2@u_mA. -The relevant documentation is on this page.- Added the Simulation instance to the Analysis class.
-- Refactored simulation parameters as classes.
-
---
-- fixed CCCS and CCVS
-
---
-- fixed ngspice shared
-
---
-- Added an example to show how to use the NgSpice Shared Simulation Mode.
-- Completed the Spice netlist parser and added examples, we could now use a schematic editor -to define the circuit. The program cir2py translates a circuit file to Python.
-
Started project
- - -