forked from PySpice-org/PySpice
-
Notifications
You must be signed in to change notification settings - Fork 2
Add HSpice Simulator Backend #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
infinitymdm
merged 22 commits into
infinitymdm:master
from
AidenDawn:feature/hspice-backend
Jul 9, 2026
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
eff1ec1
Update simulator class to facilitate other simulators
AidenDawn 1238a97
Integrate HSPICE backend with binary reader and concurrency limits
AidenDawn 3519e7b
Address comments related to simulator import and classes. Move simula…
AidenDawn 373ce32
Address AI review comments
AidenDawn 309c97e
Minor fixes for latest comments
AidenDawn 077b7cd
Whitespace formatting
AidenDawn 9a2336f
Add support for native simulation sweeps. May be useful to increase s…
AidenDawn 88375d2
Run black to enforce formatting in added/updated files, add full copy…
AidenDawn 4ef1382
Update simulator class to facilitate other simulators
AidenDawn 0de11b2
Python implementation and hspice format verification scripts
AidenDawn 5775d32
Add HSPICE binary parser and expand test coverage
AidenDawn 49a4ebf
Fixes after AI review and python formatting
AidenDawn c045f91
AI solutions for AI review
AidenDawn 2a50710
Tiny fixes for failing tests
AidenDawn c43ab42
Refactor hspice_read to account for tests and file_structure analsysis
AidenDawn 6b48d03
Address ai code review comments part 2
AidenDawn 7198fca
Address Null return on error comment
AidenDawn 32b2320
ai code review comments part 3
AidenDawn 761e2ad
Fix a bug where when building the PySpice package it fails with a PEP…
AidenDawn 9e1ad19
Additional fix for numpy dependency on C module
AidenDawn f175f47
Fix simulation measurement parsing failures from ascii files, the par…
AidenDawn a15e764
Address AI review comments
AidenDawn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,247 @@ | ||
| import logging | ||
|
|
||
| import numpy as np | ||
|
|
||
| from PySpice.Probe.WaveForm import (AcAnalysis, DcAnalysis, OperatingPoint, | ||
| TransientAnalysis, WaveForm) | ||
| from PySpice.Unit import u_A, u_Degree, u_Hz, u_s, u_V | ||
|
|
||
|
|
||
| class AnalysisList(list): | ||
| def __init__(self, analyses, measurements=None): | ||
| super().__init__(analyses) | ||
| self._measurements = measurements or {} | ||
|
|
||
| @property | ||
| def measurements(self): | ||
| return self._measurements | ||
|
|
||
| def __getattr__(self, name): | ||
| measurements = self.__dict__.get("_measurements", {}) | ||
| if name in measurements: | ||
| return measurements[name] | ||
| if name.lower() in measurements: | ||
| return measurements[name.lower()] | ||
| raise AttributeError(name) | ||
|
|
||
| def __getitem__(self, item): | ||
| if isinstance(item, (int, slice)): | ||
| return super().__getitem__(item) | ||
| if isinstance(item, str): | ||
| if item in self._measurements: | ||
| return self._measurements[item] | ||
| if item.lower() in self._measurements: | ||
| return self._measurements[item.lower()] | ||
| raise IndexError(item) | ||
| raise KeyError(item) | ||
|
|
||
|
|
||
| _module_logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class HSpiceRawFile: | ||
| def __init__( | ||
| self, | ||
| data, | ||
| simulation=None, | ||
| measurements=None, | ||
| op_nodes=None, | ||
| op_branches=None, | ||
| analysis_type=None, | ||
| ): | ||
| self.data = data | ||
| self._simulation = simulation | ||
| self.measurements = measurements or {} | ||
| self.op_nodes = op_nodes or {} | ||
| self.op_branches = op_branches or {} | ||
| self._analysis_type = analysis_type | ||
|
|
||
| @property | ||
| def simulation(self): | ||
| return self._simulation | ||
|
|
||
| @simulation.setter | ||
| def simulation(self, value): | ||
| self._simulation = value | ||
|
|
||
| def to_analysis(self): | ||
| if self.data is None and self._analysis_type == "o": | ||
| # This is an operating point simulation! | ||
| nodes = [ | ||
| WaveForm.from_unit_values(name, u_V(np.array([val]))) | ||
| for name, val in self.op_nodes.items() | ||
| ] | ||
| branches = [ | ||
| WaveForm.from_unit_values(name, u_A(np.array([val]))) | ||
| for name, val in self.op_branches.items() | ||
| ] | ||
| return OperatingPoint( | ||
| simulation=self.simulation, nodes=nodes, branches=branches | ||
| ) | ||
|
|
||
| # data is a list of sweeps returned by hspice_read | ||
| # data[0] is sweeps tuple (sweep, sweepValues, dataList) | ||
| sweeps = self.data[0][0] | ||
| scale_name_outer = self.data[0][1] | ||
| scale_name_inner, sweep_values, data_list = sweeps | ||
|
|
||
| if len(data_list) == 0: | ||
| analysis_type = self._analysis_type | ||
| if ( | ||
| not analysis_type | ||
| and self.simulation | ||
| and hasattr(self.simulation, "_analyses") | ||
| ): | ||
| analyses_keys = set(self.simulation._analyses.keys()) | ||
| if "ac" in analyses_keys: | ||
| analysis_type = "a" | ||
| elif "dc" in analyses_keys: | ||
| analysis_type = "s" | ||
| elif "op" in analyses_keys: | ||
| analysis_type = "o" | ||
| elif "tran" in analyses_keys: | ||
| analysis_type = "t" | ||
|
|
||
| analysis_type = analysis_type or "t" | ||
|
|
||
| if analysis_type == "a": | ||
| analysis = AcAnalysis( | ||
| simulation=self.simulation, | ||
| frequency=WaveForm.from_array("frequency", np.array([])), | ||
| nodes=[], | ||
| branches=[], | ||
| internal_parameters=[], | ||
| ) | ||
| elif analysis_type == "s": | ||
| analysis = DcAnalysis( | ||
| simulation=self.simulation, | ||
| sweep=WaveForm.from_array("sweep", np.array([])), | ||
| nodes=[], | ||
| branches=[], | ||
| internal_parameters=[], | ||
| ) | ||
| elif analysis_type == "o": | ||
| analysis = OperatingPoint( | ||
| simulation=self.simulation, | ||
| nodes=[], | ||
| branches=[], | ||
| ) | ||
| else: | ||
| analysis = TransientAnalysis( | ||
| simulation=self.simulation, | ||
| time=WaveForm.from_array("time", np.array([])), | ||
| nodes=[], | ||
| branches=[], | ||
| internal_parameters=[], | ||
| ) | ||
| analysis._measurements = dict(self.measurements) | ||
| return analysis | ||
|
|
||
| # Derive analysis type and scale unit once from the parser-provided name. | ||
| _sn_upper = scale_name_outer.upper() | ||
| if _sn_upper == "TIME": | ||
| _analysis_type_outer = "transient" | ||
| _scale_unit_outer = u_s | ||
| elif _sn_upper in ("FREQUENCY", "HERTZ"): | ||
| _analysis_type_outer = "ac" | ||
| _scale_unit_outer = u_Hz | ||
| else: | ||
| _analysis_type_outer = "dc" | ||
| if _sn_upper == "VOLTS": | ||
| _scale_unit_outer = u_V | ||
| elif _sn_upper == "AMPS": | ||
| _scale_unit_outer = u_A | ||
| elif _sn_upper == "DEG_C": | ||
| _scale_unit_outer = u_Degree | ||
| else: | ||
| _scale_unit_outer = None | ||
|
|
||
| analyses = [] | ||
| for res_dict in data_list: | ||
| # Resolve the dict key using the parser-provided scale_name_outer. | ||
| _key_lower = scale_name_outer.lower() | ||
| if _key_lower in res_dict: | ||
| scale_name = _key_lower | ||
| else: | ||
| scale_name = next( | ||
| (k for k in res_dict if k.lower() == _key_lower), None | ||
| ) | ||
| if scale_name is None: | ||
| raise KeyError( | ||
| f"Independent variable '{scale_name_outer}' not found in sweep dict " | ||
| f"(keys: {list(res_dict.keys())})" | ||
| ) | ||
| scale_values = res_dict[scale_name] | ||
|
|
||
| analysis_type = _analysis_type_outer | ||
| scale_unit = _scale_unit_outer | ||
|
|
||
| # Build WaveForms | ||
| abscissa_name = scale_name_outer.lower() | ||
| if analysis_type == "ac": | ||
| abscissa_name = "frequency" | ||
|
|
||
| if scale_unit: | ||
| abscissa_waveform = WaveForm.from_unit_values( | ||
| abscissa_name, scale_unit(scale_values) | ||
| ) | ||
| else: | ||
| abscissa_waveform = WaveForm.from_array(abscissa_name, scale_values) | ||
|
|
||
| nodes = [] | ||
| branches = [] | ||
|
|
||
| for k, v in res_dict.items(): | ||
| if k == scale_name: | ||
| continue | ||
|
|
||
| # Determine if it's a current or voltage | ||
| if k.lower().startswith("i("): | ||
| # Simplify name: strip i( and trailing ) | ||
| simplified_name = k[2:] | ||
| if simplified_name.endswith(")"): | ||
| simplified_name = simplified_name[:-1] | ||
| branches.append( | ||
| WaveForm.from_unit_values( | ||
| simplified_name, u_A(v), abscissa=abscissa_waveform | ||
| ) | ||
| ) | ||
| else: | ||
| simplified_name = k | ||
| nodes.append( | ||
| WaveForm.from_unit_values( | ||
| simplified_name, u_V(v), abscissa=abscissa_waveform | ||
| ) | ||
| ) | ||
|
|
||
| if analysis_type == "transient": | ||
| analysis = TransientAnalysis( | ||
| simulation=self.simulation, | ||
| time=abscissa_waveform, | ||
| nodes=nodes, | ||
| branches=branches, | ||
| internal_parameters=[], | ||
| ) | ||
| elif analysis_type == "ac": | ||
| analysis = AcAnalysis( | ||
| simulation=self.simulation, | ||
| frequency=abscissa_waveform, | ||
| nodes=nodes, | ||
| branches=branches, | ||
| internal_parameters=[], | ||
| ) | ||
| else: | ||
| analysis = DcAnalysis( | ||
| simulation=self.simulation, | ||
| sweep=abscissa_waveform, | ||
| nodes=nodes, | ||
| branches=branches, | ||
| internal_parameters=[], | ||
| ) | ||
|
|
||
| analysis._measurements = dict(self.measurements) | ||
| analyses.append(analysis) | ||
|
|
||
| if len(analyses) == 1: | ||
| return analyses[0] | ||
| return AnalysisList(analyses, dict(self.measurements)) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.