diff --git a/.github/workflows/pyspice-test.yml b/.github/workflows/pyspice-test.yml index b4aaa7d3e..c8d327fb5 100644 --- a/.github/workflows/pyspice-test.yml +++ b/.github/workflows/pyspice-test.yml @@ -6,12 +6,9 @@ name: Pyspice Test # Trigger the workflow on on: push: - branches: - - master - - devel + branches: "*" pull_request: - branches: - - master + branches: [ "*" ] # page_build: # release: # types: # This configuration does not affect the page_build event above diff --git a/.gitignore b/.gitignore index d75b2e63f..cd4142876 100644 --- a/.gitignore +++ b/.gitignore @@ -177,3 +177,8 @@ examples/p.diff examples/spice-library-backup/ examples/spice-library/db.pickle test.cir + +.venv/* +*.vscode/* +examples/spice-library/**/**/*.yaml +examples/spice-library/**/*.yaml diff --git a/PySpice/Plot/BodeDiagram.py b/PySpice/Plot/BodeDiagram.py index d31a864f6..54e0e519e 100644 --- a/PySpice/Plot/BodeDiagram.py +++ b/PySpice/Plot/BodeDiagram.py @@ -33,7 +33,7 @@ def bode_diagram_gain(axe, frequency, gain, **kwargs): - axe.semilogx(frequency, gain, basex=10, **kwargs) + axe.semilogx(frequency, gain, base=10, **kwargs) axe.grid(True) axe.grid(True, which='minor') axe.set_xlabel("Frequency [Hz]") diff --git a/PySpice/Spice/Element.py b/PySpice/Spice/Element.py index 1007a9aa9..cd50ee567 100644 --- a/PySpice/Spice/Element.py +++ b/PySpice/Spice/Element.py @@ -211,7 +211,7 @@ def __iadd__(self, obj): ############################################## - def add_current_probe(self, circuit): + def add_current_probe(self, circuit, name=None): """Add a current probe between the node and the pin. The ammeter is named *ElementName_PinName*. @@ -221,8 +221,29 @@ def add_current_probe(self, circuit): # Fixme: add it to a list if self.connected: node = self._node - self._node = '_'.join((self._element.name, self._name)) - circuit.V(self._node, node, self._node, '0') + self._node = '_'.join((self._element.name, str(node), str(self.position) )) + if name is None: + name = self._node + circuit.V(name, node, self._node, '0') + else: + raise NameError("Dangling pin") + + def add_esr(self, circuit, name= None, value=1e-3): + + """Add a series resistance between the node and the pin. + + The ammeter is named *ElementName_PinName*. + + """ + + # Fixme: require a reference to circuit + # Fixme: add it to a list + if self.connected: + node = self._node + self._node = '_'.join((self._element.name, str(self._node), str(self.position))) + if name is None: + name = self._node + return circuit.R(name, node, self._node, value) else: raise NameError("Dangling pin") diff --git a/PySpice/Spice/Library/Library.py b/PySpice/Spice/Library/Library.py index ad46046f9..bd86d1edd 100644 --- a/PySpice/Spice/Library/Library.py +++ b/PySpice/Spice/Library/Library.py @@ -70,16 +70,27 @@ class SpiceLibrary: ############################################## - def __init__(self, root_path: str | Path, scan: bool = False) -> None: + def __init__(self, root_path: str | Path, scan: bool = False, recurse: bool = False, section: bool = False) -> None: + # recurse will be removed in the future maybe. it's here because skidl uses it + # if recurse: + # scan = recurse self._path = PathTools.expand_path(root_path) if not self._path.exists(): self._path.mkdir(parents=True) self._logger.info(f"Created {self._path}") + # elif self._path.is_file(): + # self._path = self._path.parent self._subcircuits = {} self._models = {} + self._recurse = recurse + self._section = section if not scan: if self.has_db_path: self.load() + '''Check if the library has the our path in the subcircuits.''' + paths = {Path(sub_path) for sub_path in self._subcircuits.values()} + if self._path not in paths: + scan = True else: self._logger.info("Initialize library...") scan = True @@ -91,6 +102,9 @@ def __init__(self, root_path: str | Path, scan: bool = False) -> None: @property def db_path(self) -> Path: + if self._path.is_file(): + # If the db path is for a file, use the file's parent directory + return self._path.parent.joinpath('db.pickle') return self._path.joinpath('db.pickle') @property @@ -165,19 +179,36 @@ def list_categories(self) -> str: ############################################## def scan(self) -> None: + self._logger.info(f"Scan {self._path}...") + + # Handle the case where self._path is a file, not a directory + if self._path.is_file(): + # Check if the file has a valid extension + _ = self._path.suffix.lower() + if _ in self.EXTENSIONS: + try: + self._handle_library(self._path) + except Exception as e: + self._logger.warning(f"Failed to parse {self._path}: {e}") + return + + # Handle the case where self._path is a directory for path in PathTools.walk(self._path): _ = path.suffix.lower() if _ in self.EXTENSIONS: - self._handle_library(path) + try: + self._handle_library(path) + except Exception as e: + self._logger.warning(f"Failed to parse {path}: {e}") ############################################## def _handle_library(self, path: Path) -> None: - spice_include = SpiceInclude(path) + spice_include = SpiceInclude(path, recurse=self._recurse, section=self._section) # Fixme: check overwrite - self._models.update({_.name: path for _ in spice_include.models}) - self._subcircuits.update({_.name: path for _ in spice_include.subcircuits}) + self._models.update({_.name: str(_.path) for _ in spice_include.models}) + self._subcircuits.update({_.name: str(_.path) for _ in spice_include.subcircuits}) ############################################## @@ -192,16 +223,63 @@ def delete_yaml(self) -> None: def __getitem__(self, name: str) -> Subcircuit | Model: if not self: self._logger.warning("Empty library") + + # First, check if the requested item exists directly + path = None if name in self._subcircuits: path = self._subcircuits[name] elif name in self._models: path = self._models[name] else: - # print('Library {} not found in {}'.format(name, self._path)) - # self._logger.warn('Library {} not found in {}'.format(name, self._path)) + # Item not found directly - warn and raise KeyError + available = list(self._subcircuits.keys()) + list(self._models.keys()) + available_str = ", ".join(available[:10]) + if len(available) > 10: + available_str += f", ... ({len(available)-10} more)" + self._logger.warning(f"Library item '{name}' not found in {self._path}. Available: {available_str}") raise KeyError(name) - # Fixme: lazy ??? - return SpiceInclude(path)[name] + + # Create SpiceInclude with recursion enabled if requested + spice_include = SpiceInclude(path, recurse=self._recurse) + + try: + # Try to get the item directly from this SpiceInclude + return spice_include[name] + except KeyError: + # Item exists in index but not in the file - search in parent directory + self._logger.info(f"Item '{name}' referenced in {path} but not found there. Searching in sibling files...") + + # Try to find the component in sibling library files + original_path = Path(path) + parent_dir = original_path.parent + + # Try looking for the item in other library files in the same directory + for lib_ext in self.EXTENSIONS: + for sibling_file in parent_dir.glob(f"*{lib_ext}"): + if sibling_file == original_path: + continue # Skip the original file + + try: + self._logger.info(f"Checking {sibling_file} for {name}") + sibling_include = SpiceInclude(sibling_file, recurse=self._recurse) + # Check if this file contains our item + for subckt in sibling_include.subcircuits: + if subckt.name == name: + return subckt + for model in sibling_include.models: + if model.name == name: + return model + except Exception as e: + self._logger.warning(f"Error checking {sibling_file}: {e}") + + # If we still can't find it, try one last method - force reparse everything + try: + self._logger.info(f"Attempting one last search with forced reparse for {name}") + reparse_include = SpiceInclude(path, rewrite_yaml=True, recurse=True) + return reparse_include[name] + except KeyError: + # Detailed error message when all recovery attempts fail + raise KeyError(f"'{name}' referenced in library index but not found in any source file. Check your include hierarchy.") ############################################## diff --git a/PySpice/Spice/Library/SpiceInclude.py b/PySpice/Spice/Library/SpiceInclude.py index ae6231c8c..b67ca0b5a 100644 --- a/PySpice/Spice/Library/SpiceInclude.py +++ b/PySpice/Spice/Library/SpiceInclude.py @@ -32,7 +32,7 @@ import hashlib import logging import os - +import re import yaml from PySpice.Spice.Parser import SpiceFile, ParseError @@ -183,8 +183,39 @@ def _parse_nodes(self, nodes: list[str]) -> None: for index, node_str in enumerate(nodes): internal_node = None name = None + if isinstance(node_str, dict): + # if we are hear we probably have read the yaml file + internal_node = node_str['internal_node'] + node_str = node_str['name'] + if isinstance(node_str, str): + node_str = node_str.strip() + # make sure the node is a valid spice node identifier + reg_ex = r''' + (?i: # case-insensitive + (?:[+\-\%](?=[a-z_]))? # optional +, - or % at the start, only if followed by letter/underscore + [a-z0-9_]+ # one or more letters/digits/underscores + (?:\.[a-z0-9_]+)? # optionally a dot, followed by one or more letters/digits/underscores + (?:(?<=[a-z0-9])[+\-] # optionally a plus or minus if it's right after a letter/digit + (?![a-z0-9.]) # and not followed by letter/digit/dot + )? + )(?!:) # negative lookahead: do not allow a colon immediately after + ''' + # Using VERBOSE to ignore whitespace/comments in the regex + pattern = re.compile(reg_ex, re.VERBOSE) + match = pattern.fullmatch(node_str) + if match: + name = match.group(0) + if not internal_node: + internal_node = index + 1 + # continue + else: + self._logger.warning(f"Invalid pin format {node_str} for {self.name}") + # self._valid = False + # return + continue if isinstance(node_str, int): internal_node = node_str + name = f'{internal_node}' else: node_str = node_str.strip() i = node_str.find(' ') @@ -232,7 +263,7 @@ def pin_names(self) -> list[str]: def to_yaml(self) -> dict: _ = super().to_yaml() - _.update({'nodes': self._nodes}) + _.update({'nodes': [{'index': _.index, 'name': _.name, 'internal_node': _.internal_node} for _ in self._nodes]}) return _ ############################################## @@ -258,7 +289,7 @@ class SpiceInclude: ############################################## - def __init__(self, path: str | Path, rewrite_yaml: bool = False) -> None: + def __init__(self, path: str | Path, rewrite_yaml: bool = False, recurse: bool = False, section: str | None = None) -> None: self._path = Path(path) # .resolve() self._extension = None @@ -269,6 +300,8 @@ def __init__(self, path: str | Path, rewrite_yaml: bool = False) -> None: self._subcircuits = {} self._digest = None self._recursive_digest = None + self._recurse = recurse + self._section = section # Fixme: check still valid ! if not rewrite_yaml and self.has_yaml: @@ -277,9 +310,69 @@ def __init__(self, path: str | Path, rewrite_yaml: bool = False) -> None: else: self.parse() self.write_yaml() + if self._recurse: + self._process_inner_includes() ############################################## + def _process_inner_includes(self) -> None: + """Process inner includes recursively and add their models and subcircuits. + + This method processes all inner includes detected during parsing and + adds their models and subcircuits to this SpiceInclude instance. + """ + processed_paths = set() # Track processed paths to avoid circular includes + processed_paths.add(str(self._path.resolve())) + + # Create a list to store inner include instances + includes = [] + + # Process each inner include path + for path_str in self._inner_includes: + try: + path = Path(path_str) + # Ensure path is absolute by resolving it relative to parent directory if needed + if not path.is_absolute(): + path = (self._path.parent / path).resolve() + else: + path = path.resolve() + + # Skip if we've already processed this path + if str(path) in processed_paths: + self._logger.info(f"Skipping already processed include: {path}") + continue + + # Check if file exists + if not path.exists(): + self._logger.warning(f"Include file not found: {path}") + continue + + self._logger.info(f"Processing inner include {path}") + include = SpiceInclude(path, recurse=self._recurse) + includes.append(include) + processed_paths.add(str(path)) + + # Add models from this include + for model in include.models: + if model.name in self._models: + self._logger.warning(f"Duplicate model {model.name} from {path}, keeping original") + else: + self._models[model.name] = model + + # Add subcircuits from this include + for subcircuit in include.subcircuits: + if subcircuit.name in self._subcircuits: + self._logger.warning(f"Duplicate subcircuit {subcircuit.name} from {path}, keeping original") + else: + self._subcircuits[subcircuit.name] = subcircuit + + except Exception as e: + self._logger.error(f"Failed to process inner include {path_str}: {str(e)}") + + # Replace the path strings with actual SpiceInclude instances + # for recursive digest computation + self._inner_includes = includes + # def dump(self) -> None: # print(self._path) # for _ in self._models: @@ -362,12 +455,29 @@ def add_line(item): def parse(self) -> None: self._logger.info(f"Parse {self._path}") try: - spice_file = SpiceFile(self._path) + spice_file = SpiceFile(self._path, self._section) except ParseError as exception: # Parse problem with this file, so skip it and keep going. self._logger.warn(f"Parse error in Spice library {self._path}{NEWLINE}{exception}") - self._inner_includes = [Path(str(_)) for _ in spice_file.includes] - self._inner_libraries = [Path(str(_)) for _ in spice_file.libraries] + + # Convert include paths to absolute paths if they are relative + self._inner_includes = [] + for include_path in spice_file.includes: + path = Path(str(include_path)) + # If path is not absolute, make it absolute using self.path's parent directory + if not path.is_absolute(): + path = self._path.parent / path + self._inner_includes.append(path) + + # Process library files separately + # self._inner_libraries = [] + # for lib in spice_file.libraries: + # path = Path(str(lib.path)) + # # If path is not absolute, make it absolute using self.path's parent directory + # if not path.is_absolute(): + # path = self._path.parent / path + # self._inner_libraries.append(path) + for subcircuit in spice_file.subcircuits: # name = self._suffix_name(subcircuit.name) _ = Subcircuit(self, subcircuit.name, subcircuit.nodes) @@ -391,7 +501,7 @@ def write_yaml(self): with open(self.yaml_path, 'w', encoding='utf8') as fh: data = { # 'path': str(self._path), - 'path': self._path.name, + 'path': str(self.path), 'date': self.mtime.isoformat(), 'digest': self.digest, 'description': self._description, @@ -401,7 +511,7 @@ def write_yaml(self): if self._subcircuits: data['subcircuits'] = [_.to_yaml() for _ in self.subcircuits] if self._inner_includes: - data['inner_includes'] = self._inner_includes + data['inner_includes'] = [str(_) for _ in self._inner_includes] if self._inner_libraries: data['inner_libraries'] = self._inner_libraries # data['recursive_digest'] = self.recursive_digest @@ -426,7 +536,7 @@ def load_yaml(self) -> None: if 'subcircuits' in data: subcircuits = [Subcircuit.from_yaml(self, _) for _ in data['subcircuits']] self._subcircuits = {_.name: _ for _ in subcircuits} - if 'inner_libraries' in data: + if 'inner_includes' in data: self._inner_includes = data['inner_includes'] if 'inner_libraries' in data: self._inner_libraries = data['inner_libraries'] diff --git a/PySpice/Spice/Netlist.py b/PySpice/Spice/Netlist.py index 5a420d0ca..e9f42da36 100644 --- a/PySpice/Spice/Netlist.py +++ b/PySpice/Spice/Netlist.py @@ -629,7 +629,7 @@ def include(self, path: Union[Path, str, 'Library.SubCircuit'], warn: bool = Tru # Fixme: str(path) ? # Fixme: circular import... from . import Library - if isinstance(path, Library.Subcircuit): + if isinstance(path, Library.Subcircuit) or isinstance(path, Library.Model): path = path.path path = Path(path).resolve() if path not in self._includes: diff --git a/PySpice/Spice/NgSpice/Shared.py b/PySpice/Spice/NgSpice/Shared.py index 8d805f0a8..fc934a968 100644 --- a/PySpice/Spice/NgSpice/Shared.py +++ b/PySpice/Spice/NgSpice/Shared.py @@ -416,10 +416,22 @@ def setup_platform(cls): cls.LIBRARY_PATH = _ else: if ConfigInstall.OS.on_windows: - ngspice_path = Path(__file__).parent.joinpath('Spice64_dll') - cls.NGSPICE_PATH = ngspice_path - # path = ngspice_path.joinpath('dll-vs', 'ngspice-{version}{id}.dll') - path = ngspice_path.joinpath('dll-vs', 'ngspice{}.dll') + # Check for MSYSTEM environment first + msystem = os.environ.get('MSYSTEM') + mingw_prefix = os.environ.get('MINGW_PREFIX') + + if msystem and mingw_prefix: + # Use MINGW paths + path = str(Path(mingw_prefix) / 'bin' / 'libngspice-0{}.dll') + if 'SPICE_LIB_DIR' not in os.environ: + os.environ['SPICE_LIB_DIR'] = str(Path(mingw_prefix) / 'share' / 'ngspice' / 'scripts') + else: + # Fall back to original Windows paths + ngspice_path = Path(__file__).parent.joinpath('Spice64_dll') + cls.NGSPICE_PATH = ngspice_path + # path = ngspice_path.joinpath('dll-vs', 'ngspice-{version}{id}.dll') + path = str(ngspice_path.joinpath('dll-vs', 'ngspice{}.dll')) + elif ConfigInstall.OS.on_osx: path = 'libngspice{}.dylib' elif ConfigInstall.OS.on_linux: @@ -619,7 +631,24 @@ def _send_char(message_c, ngspice_id, user_data): func = self._logger.info elif content.startswith('Warning:'): func = self._logger.warning - # elif content.startswith('Warning:'): + elif content.startswith('Using'): # Ignore "Using ... as Direct Linear Solver" messages + func = self._logger.debug + elif content.startswith('doAnalyses:'): + func = self._logger.debug + if 'timestep too small' in content.lower(): + self._logger.warning(content) + elif content.startswith('run simulation interrupted'): + func = self._logger.debug + elif content.startswith('simulation interrupted'): + func = self._logger.debug + elif content.startswith('Note:'): + func = self._logger.info + elif content.startswith('Trying'): + func = self._logger.info + elif content.startswith('Supplies reduced'): + func = self._logger.info + elif content.startswith('run simulation(s) aborted'): + func = self._logger.error else: self._error_in_stderr = True func = self._logger.error @@ -630,10 +659,10 @@ def _send_char(message_c, ngspice_id, user_data): else: self._stdout.append(content) # Fixme: Ngspice writes error on stdout and stderr ... - if 'error' in content.lower(): - self._error_in_stdout = True - # if self._error_in_stdout: - # self._logger.warning(content) + # if 'error' in content.lower(): + # self._error_in_stdout = True + if self._error_in_stdout: + self._logger.warning(content) # Fixme: ??? return self.send_char(message, ngspice_id) @@ -695,6 +724,7 @@ def _background_thread_running(is_running, ngspice_id, user_data): self = ffi.from_handle(user_data) self._logger.debug(f'ngspice_id-{ngspice_id} background_thread_running {is_running}') self._is_running = is_running + return 0 ############################################## diff --git a/PySpice/Spice/NgSpice/SimulationType.py b/PySpice/Spice/NgSpice/SimulationType.py index 882e98089..2f9d068d1 100644 --- a/PySpice/Spice/NgSpice/SimulationType.py +++ b/PySpice/Spice/NgSpice/SimulationType.py @@ -82,7 +82,7 @@ 'charge', ) -LAST_VERSION = 42 # released on 2023-12-27 +LAST_VERSION = 45 # released on January 31st, 2021 for version in range(28, LAST_VERSION +1): SIMULATION_TYPE[version] = SIMULATION_TYPE[27] diff --git a/PySpice/Spice/NgSpice/Simulator.py b/PySpice/Spice/NgSpice/Simulator.py index 6787dcdae..526379407 100644 --- a/PySpice/Spice/NgSpice/Simulator.py +++ b/PySpice/Spice/NgSpice/Simulator.py @@ -104,6 +104,8 @@ def ngspice(self): @property def version(self): return self._ngspice_shared.ngspice_version + def _run(self, analysis_method, *args, **kwargs): + background = kwargs.pop('background', False) ############################################## @@ -117,7 +119,6 @@ def run(self, simulation): self._ngspice_shared.load_circuit(str(simulation)) self._ngspice_shared.run() self._logger.debug(str(self._ngspice_shared.plot_names)) - plot_name = self._ngspice_shared.last_plot if plot_name == 'const': raise NameError('Simulation failed') diff --git a/PySpice/Spice/Parser/HighLevelParser.py b/PySpice/Spice/Parser/HighLevelParser.py index 9b34e3239..cdd33a090 100644 --- a/PySpice/Spice/Parser/HighLevelParser.py +++ b/PySpice/Spice/Parser/HighLevelParser.py @@ -84,7 +84,7 @@ from ..Netlist import Node from ..StringTools import remove_multi_space from . import Ast -from . import ElementData +from .ElementData import ElementData from .Ast import AstNode from .Parser import SpiceParser from .SpiceSyntax import ElementLetters @@ -313,7 +313,14 @@ def __init__(self, line: SpiceLine, ast: AstNode) -> None: # Read nodes self._nodes = [] number_of_pins = 0 - data = ElementData.elements[self._letter] + from PySpice.Spice.Element import ElementParameterMetaClass + elements = {} + for letter, classes in ElementParameterMetaClass._classes.items(): + element_data = ElementData(letter, classes) + elements[letter] = element_data + elements[letter.lower()] = element_data + # data = ElementData.elements[self._letter] + data = elements[self._letter] if not data.has_variable_number_of_pins: number_of_pins = data.number_of_pins else: # Q or X @@ -989,6 +996,7 @@ def reset(self) -> None: self._subcircuits = [] self._circuit = Netlist() self._control = [] + self._section = None ############################################## @@ -1007,13 +1015,16 @@ def _split_command_comment(cls, line): ############################################## - def read(self, generator: Generator[tuple[int, str], None, None], title_line: bool=True) -> None: + def read(self, generator: Generator[tuple[int, str], None, None], title_line: bool=True, section: str=None ) -> None: """Preprocess lines. This method merges continuation lines and split command and comment. """ self.reset() last_line = None last_command = None + we_are_in_lib = False + self._section = section + libname = None for line_number, line in generator: # print(f'>>>{line_number}///{line.rstrip()}') ### line = line.strip() @@ -1048,6 +1059,14 @@ def read(self, generator: Generator[tuple[int, str], None, None], title_line: bo last_line.append(line_number, '', comment) else: last_line = SpiceLine(line_number, line_number, command, comment) + if command.startswith('.lib'): + libname = command.split('lib')[1].strip().lower() + we_are_in_lib = True + elif command.startswith('.endl'): + we_are_in_lib = False + libname = None + if self._section and libname != self._section.lower() and we_are_in_lib: + continue self._lines.append(last_line) if command: last_command = last_line @@ -1097,7 +1116,10 @@ def append(obj: Command) -> None: continue cls = Command.get_cls(line, get_state() == SpiceStates.CONTROL) obj = cls(line, ast) - self._obj_lines.append(obj) + + # don't append if in subcircuit. it will be added in the subcircuit anyway + if not_state(SpiceStates.SUBCIRCUIT): + self._obj_lines.append(obj) self._logger.debug(os.linesep + repr(obj)) match obj: case If(): @@ -1105,7 +1127,9 @@ def append(obj: Command) -> None: append(obj) raise NotImplementedError case Include(): - self._includes.append(obj) + self._includes.append(obj.path) + case Library(): + self._libs.append(obj) case Control(): state_stack.append(SpiceStates.CONTROL) control.append(obj) @@ -1157,10 +1181,10 @@ def parse_string(self, source: str, title_line: bool=True) -> None: ############################################## - def parse_file(self, path: str | Path) -> None: + def parse_file(self, path: str | Path, section: str=None) -> None: with open(path, 'r', encoding='utf-8') as fh: generator = enumerate(fh.readlines()) - self.read(generator, title_line=True) + self.read(generator, title_line=True, section=section) self._parse() ############################################## @@ -1269,7 +1293,7 @@ class SpiceFile(SpiceSource): ############################################## - def __init__(self, path: str | Path) -> None: + def __init__(self, path: str | Path, section: str=None) -> None: super().__init__() self._path = Path(path) - self.parse_file(path) + self.parse_file(path, section) diff --git a/PySpice/Spice/Parser/Parser.py b/PySpice/Spice/Parser/Parser.py index 9f452eed6..6478eaa35 100644 --- a/PySpice/Spice/Parser/Parser.py +++ b/PySpice/Spice/Parser/Parser.py @@ -70,6 +70,13 @@ class SpiceParser: ############################################## + # Declare an INCLUSIVE state called PARAM + states = ( + ('NODES', 'inclusive'), + ) + + ############################################### + # When building the master regular expression, rules are added in the following order: # - All tokens defined by functions are added in the same order as they appear in the lexer file. # - Tokens defined by strings are added next by sorting them in order of decreasing regular @@ -127,6 +134,18 @@ def t_error(self, token): # token.lexer.skip(1) raise NameError('Lexer error') + + def t_DOT_COMMAND(self, t): + r'\.(?i:[a-z]+)' + # Switch to the PARAM state on seeing a dot command + t.lexer.begin('NODES') + return t + + def t_NODES_SET(self, t): + r'=' + # Switch back to the DEFAULT state on seeing a = + t.lexer.begin('INITIAL') + return t ############################################## t_ignore = ' \t' @@ -167,7 +186,7 @@ def t_error(self, token): t_LEFT_BRACE = r'\{' t_RIGHT_BRACE = r'\}' - t_QUOTE = r"'" + t_QUOTE = r'[\'"]' t_SET = r'=' t_BRANCH = r'\#branch' @@ -175,15 +194,16 @@ def t_error(self, token): t_TILDE = r'~' - t_STRING = r'"((\\")|[^"])*"' + # t_STRING = r'"((\\")|[^"])*"' - t_DOT_COMMAND = r'\.(?i:[a-z]+)' + # t_DOT_COMMAND = r'\.(?i:[a-z]+)' # Note: # If ID > NUMBER, then it breaks float and yield A - B # If NUMBER < ID, then 2N2222A is split in two NUMBER tokens: Number 2 n, Number 2222 None a # Solution: use [a-z0-9]* for EXTRA_UNIT + def t_NUMBER(self, t): # Fixme: CONTEXTUAL SYNTAX !!! in_offset=[0.1 -0.2] r''' @@ -225,7 +245,6 @@ def t_NUMBER(self, t): else: t.value = Number(value, unit, extra_unit) return t - def t_ID(self, t): # Fixme: r''' @@ -235,6 +254,21 @@ def t_ID(self, t): )''' # t.value = Id(t.value) return t + + + def t_NODES_ID(self, t): + # Fixme: + r''' + (?i: + (?:[+\-\%](?=[a-z_]))? + [a-z0-9_][-a-z0-9_]* + (?:\.[a-z0-9_][-a-z0-9_]*)? + (?:(?<=[a-z0-9])[+\-](?![a-z0-9.-]))? + :? + )''' + # t.value = Id(t.value) + return t + ############################################## # @@ -270,6 +304,7 @@ def p_command(self, p): ''' return self._command(p, Command) + # Fixme: could be merged with command def p_dot_command(self, p): '''command : DOT_COMMAND expression_list_space @@ -303,7 +338,18 @@ def p_id(self, p): '''expression : ID ''' p[0] = Id(p[1]) - + + # -------------------------------------------------------------------------------------- + # CHANGED: Added this rule to handle patterns like "params:" in a .subckt line. + # Example: .subckt genopa1 in+ in- vcc vee out params: POLE=20 ... + # def p_NODES_id_colon(self, p): + # '''expression : ID COLON + # ''' + # # Treat "params:" (or any ID followed by a colon) as a single expression. + # p[0] = Id(p[1] + ':') + + # # --- + def p_uminus(self, p): '''expression : MINUS expression %prec UMINUS''' # %prec UMINUS overrides the default rule precedence-setting it to that of UMINUS in the precedence specifier. diff --git a/PySpice/Spice/Parser/Translator.py b/PySpice/Spice/Parser/Translator.py index 58eb77db3..c02e2a6cb 100644 --- a/PySpice/Spice/Parser/Translator.py +++ b/PySpice/Spice/Parser/Translator.py @@ -74,7 +74,7 @@ def translate(self, spice_code:SpiceSource, ground: int=Node.SPICE_GROUND_NUMBER """ self._circuit = Circuit(spice_code.title) self._ground = ground - + for obj in spice_code.obj_lines: self.handle(obj) @@ -152,6 +152,7 @@ def handle_EndIf(self, obj: EndIf) -> None: def handle_EndSubcircuit(self, obj: EndSubcircuit) -> None: raise NotImplementedError + # pass ############################################## @@ -262,7 +263,15 @@ def handle_Sensitivity(self, obj: Sensitivity) -> None: def handle_Subcircuit(self, obj: Subcircuit, ground=Node.SPICE_GROUND_NUMBER) -> None: subcircuit = SubCircuit(obj._name, *obj._nodes) - SpiceParser._build_circuit(subcircuit, obj._statements, ground) + # Add all elements from obj._items to the subcircuit + for item in obj._items: + # Create a temporary Builder to handle each item in context of the subcircuit + temp_builder = Builder() + temp_builder._circuit = subcircuit + temp_builder._ground = ground + temp_builder.handle(item) + # Original SpiceParser._build_circuit(subcircuit, obj._statements, ground) + self._circuit.subcircuit(subcircuit) return subcircuit ############################################## diff --git a/PySpice/Spice/Simulation.py b/PySpice/Spice/Simulation.py index d4020afbf..311d1fbc2 100644 --- a/PySpice/Spice/Simulation.py +++ b/PySpice/Spice/Simulation.py @@ -738,7 +738,7 @@ def _run(self, analysis_method, *args, **kwargs): if 'probes' in kwargs: self.save(* kwargs.pop('probes')) - + # Execute analysis implementation analysis_method(self, *args, **kwargs) diff --git a/README.html b/README.html index 8d69b6c50..4f53b4c8b 100644 --- a/README.html +++ b/README.html @@ -616,12 +616,11 @@
Quick Links
Travis CI but need free credits...
Disclaimer: PySpice is developed on my free time actually, so I could be busy with other tasks and less reactive.
The free Discourse forum was closed some time ago due to a lack of activity. A HTML backup is stored in the directory pyspice-discourse-backup.
-On HEAD -* fixed the ngspice library loading for recent cffi -* fixed simulation aborting due to a message from newer ngspice -* fixes for Spice parser -* added support for Pint unit library -* implemented SpiceLibrary -* code cleanup but must check for typo...
+On Devel HEAD
+fixed the ngspice library loading for recent cffi
fixed simulation aborting due to a message from newer ngspice
fixes for Spice parser
added support for Pint unit library
implemented SpiceLibrary
code cleanup but must check for typo...
An issue was found with NgSpice Shared, we must setlocale(LC_NUMERIC, "C"); see https://sourceforge.net/p/ngspice/bugs/490/
@@ -690,61 +691,23 @@Authors: Fabrice SALVAIRE and contributors
+Authors: Fabrice Salvaire and contributors
The circuit API is actually low level. It is fastidious to work with -and error-prone. Skidl has a very good approach to make the -connections between elements. A clever idea is to make the -connection through loop, e.g. gnd & C1 & (R1 | R2) & D1 & vcc.
Improve Spice library handling, e.g. we have to read the library -code to know how to map the pins, etc.
Unit should be provided by a third party. We need a library that works well with Spice.
New Simulation API
-# build a circuit
-
-# instantiate a simulator
-simulator = Simulator.factory()
-# or
-simulator = Simulator.factory(simulator='ngspice')
-# same as
-simulator = Simulator.factory(simulator='ngspice-shared')
-
-# create a simulation, it corresponds to the Spice code part with lines starting with ".something ..."
-simulation = simulator.simulation(circuit, temperature=25, nominal_temperature=25)
-# define an analysis and run it
-analysis = simulation.transient(step_time=ac_line.period/200, end_time=ac_line.period*50, log_desk=True)
-# analysis is now Pickable
-Simulation output is now Pickable
The Spice parser was rewritten from scratch using the PLY
-library, which is an implementation of lex and yacc parsing tools for Python. The LALR parser
-generates an AST from a BNF grammar written from scratch using the Ngspice manual. Up to now, it
-only requires a hack to handle the grammar, cf. XSpice vector syntax [1 -1 -2]
which
-interfere with mathematical expression. PySpice is now able to parse completely and properly all
-the examples from the Ngspice manual. However, the processing of the AST does actually the bare
-minimum.
KiCadTools a proof of concept module to read KiCad 6 .kicad_sch schema file and compute the netlist. This module can -be used to perform any kind of processing on a KiCad schema. It is +be used to perform any kind of processings on a KiCad schema. It is actually hosted in the source but could become a standalone project. For PySpice, it provides a very flexible way to draft a circuit with the help of KiCad and then generate the netlist without using the netlist export feature of KiCad. And thus leverage the -writing of fastidious circuit.
The most common PySpice parts can be imported from from PySpice import ...
Logging setup code clean-up
Netlist.py: Fix wrong method when joining parameters during netlist parse #245 (thanks to cyber-g)
Unit: add Pickle support
Add Parser code from #136 (thanks to jmgc) but not yet merged
Unit: add np.mean
Unit: add np.mean
A huge effort, thanks to @stuarteberg Stuart Berg, has been made to make Ngspice and PySpice available on Anaconda (conda-forge) for the Window, OSX and Linux platforms. Thanks to the -conda-forge continuous integration platform, we can now run unit tests and the examples on these +conda-forge continuous integration platform, we can now run unit tests and the examples on theses platforms automatically. Hope this will make the software more robust and easier to run !
PySpice is now available on Anaconda(conda-forge) as well as a wheel on PyPI
This tool can also download the examples and the Ngspice PDF manual.
On Linux and OSX, a Ngspice package is now available on Anaconda(conda-forge). -Note that theses two platforms do not download a binary from Ngspice since a compiler can easily be installed on these platforms.
Updated installation documentation for Linux, the main distributions now provide a ngspice shared package.
Added a front-end website to keep older releases documentation available on the web.
Added a front-end web site so as to keep older releases documentation available on the web.
fixed and rebuilt all examples (but mistakes could happen ...)
examples are now available as Python files and Jupyter notebooks (but some issues must be fixed, e.g. due to the way Jupyter handles Matplotlib plots)
support NgSpice 32 API (no change)
removed @substitution@
in PySpice/__init__.py, beacause it breaks pip install from git
removed @substitution@ in PySpice/__init__.py, beacause it breaks pip install from git
fixed some logging spams
fixed NonLinearVoltageSource
fixed Unicode issue with °C (° is Extended ASCII)
fixed ffi_string_utf8 for UnicodeDecodeError
fixed logging formatter for OSX (removed ANSI codes)
fixed logging formater for OSX (removed ANSI codes)
reworded "Invalid plot name" exception
removed diacritics in example filenames
cir2py has been converted to an entry point to work on all platforms
cir2py has been converted to an entry point so as to work on all platforms
updated Matplotlib subplots in examples
added a unit example
added a NMOS example (thanks to cyber-g) cf. #221
support NgSpice 31 API (no change)
added check for CoupledInductor #157
added check-installation tool to help to fix broken installation
added pole-zero, noise, distortion, transfer-function analyses (thanks to Peter Garrone) #191
added pole-zero, noise, distorsion, transfer-function analyses (thanks to Peter Garrone) #191
added .measure support (thanks to ceprio) #160
added log_desk parameter to CircuitSimulator
added listing command method to NgSpiceShared
Implemented missing transmission line devices
Implemented high level current sources Notice: Some classes were renamed !
Implemented node kwargs e.g. circuit.Q(1, base=1, collector=2, emitter=3, model='npn')
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
Rebased WaveForm to UnitValues
Fixed node order to not confuse users Now PySpice matches SPICE order for two ports elements !
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 kwargs for BJT Notice: it must be passed exclusively as kwargs !
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
Git repository clean-up: filtered generated doc and useless files to shrink the repository size.
Git repository cleanup: filtered generated doc and useless files so as to shrink the repository size.
Improved documentation generator: Implemented format
for 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_mV
or compute the value of 1.2@u_kΩ / 2@u_mA
.
diff --git a/README.rst b/README.rst
index 5151a7294..d3a3102fe 100644
--- a/README.rst
+++ b/README.rst
@@ -43,10 +43,6 @@
.. |Tavis CI master| image:: https://travis-ci.com/FabriceSalvaire/PySpice.svg?branch=master
:target: https://travis-ci.com/FabriceSalvaire/PySpice
:alt: PySpice build status @travis-ci.org
-
-.. |Pyspice Test Workflow| image:: https://github.com/FabriceSalvaire/PySpice/actions/workflows/pyspice-test.yml/badge.svg?branch=devel
- :target: https://github.com/FabriceSalvaire/PySpice/actions/workflows/pyspice-test.yml
- :alt: Pyspice Test
.. -*- Mode: rst -*-
.. _CFFI: http://cffi.readthedocs.org/en/latest/
@@ -79,9 +75,9 @@
.. |Tikz| replace:: Tikz
.. |Xyce| replace:: Xyce
-======================================================================================
+=====================================================================================
PySpice : Simulate Electronic Circuit using Python and the Ngspice / Xyce Simulators
-======================================================================================
+=====================================================================================
|Pypi License|
|Pypi Python Version|
@@ -93,18 +89,14 @@
|Tavis CI master|
-|Pyspice Test Workflow|
-
**Quick Links**
-* `Devel Branch