diff --git a/README.md b/README.md index 0907b03..ccf72ba 100644 --- a/README.md +++ b/README.md @@ -819,15 +819,17 @@ For support and improvement requests please open an Issue in [GitHub spicelib is ## History -* Version TBD +* Version 1.4.1 * Fixed Issue #158 - improve xyce path detection, improve runner switch parameter help texts + * Fixed Issue #154 - support embedded subcircuits in Qspice * Fixed Issue #139 - support xyce raw files + * Added `get_all_parameter_names()` function to all editors (#159) * Version 1.4.0 (Python 3.9+ only) * Fixed Issue #152 - python version compatibility too limited on PyPi. * Version 1.3.8 * Solving deprecation in GitHub artifact actions v3 -> v4 * Version 1.3.7 - * Fixed Issue #143 - ltsteps example fixed + * Fixed Issue #143 - ltsteps example fixed * Fixed Issue #141 - Raw file reader cannot handle complex values (AC analysis) in ASCII RAW files * Fixed Issue #140 and #131 - Compatibility with LTspice 24+ * Fixed Issue #145 - Allow easy hiding of simulator's console message diff --git a/examples/testfiles/Qspice_bug_embedded_subckt.qsch b/examples/testfiles/Qspice_bug_embedded_subckt.qsch new file mode 100644 index 0000000..7c37256 --- /dev/null +++ b/examples/testfiles/Qspice_bug_embedded_subckt.qsch @@ -0,0 +1,63 @@ +ÿØÿÛ«schematic + «component (-6700,-2000) 0 0 + «symbol V + «type: V» + «description: Independent Voltage Source» + «shorted pins: false» + «line (0,-130) (0,-200) 0 0 0x1000000 -1 -1» + «line (0,200) (0,130) 0 0 0x1000000 -1 -1» + «rect (-25,77) (25,73) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-2,50) (2,100) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-25,-73) (25,-77) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «ellipse (-130,130) (130,-130) 0 0 0 0x1000000 0x1000000 -1 -1» + «text (100,150) 1 7 0 0x1000000 -1 -1 "V1"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "1"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "+"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "-"» + » + » + «component (-3000,-1800) 0 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (100,150) 1 7 0 0x1000000 -1 -1 "R1"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "1Meg"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «component (-4800,-1300) 0 0 + «symbol Proportional + «type: X» + «description: Proportional» + «library file: |.subckt mycomp OUT IN\nE1 OUT 0 IN 0 {P}\n.ends mycomp» + «shorted pins: false» + «triangle (-100,200) (-100,-200) (300,0) 0 0 0x1000000 0x2000000 -1 -1» + «text (-100,250) 1 6 2 0x1000000 -1 -1 "X2"» + «text (0,0) 1 0 0 0x1000000 -1 -1 "mycomp"» + «text (0,-300) 1 0 0 0x1000000 -1 -1 "P=2"» + «pin (300,0) (30,150) 0.63 13 0 0x0 -1 "OUT"» + «pin (-100,0) (-100,10) 0.63 14 0 0x0 -1 "IN"» + » + » + «net (-6700,-2500) 1 13 0 "GND"» + «net (-6100,-1300) 1 14 0 "IN"» + «net (-3300,-1300) 1 14 0 "OUT"» + «junction (-6700,-2400)» + «wire (-6700,-2400) (-6700,-2200) "GND"» + «wire (-6700,-1800) (-6700,-1300) "IN"» + «wire (-3300,-1300) (-3000,-1300) "OUT"» + «wire (-3000,-2000) (-3000,-2400) "GND"» + «wire (-3000,-2400) (-6700,-2400) "GND"» + «wire (-6700,-2500) (-6700,-2400) "GND"» + «wire (-6700,-1300) (-6100,-1300) "IN"» + «wire (-3000,-1300) (-3000,-1600) "OUT"» + «wire (-4500,-1300) (-3300,-1300) "OUT"» + «wire (-6100,-1300) (-4900,-1300) "IN"» + «text (-9180,470) 1 7 0 0x1000000 -1 -1 ".tran 0 1m 0 100n uic"» +» + diff --git a/examples/testfiles/Qspice_top.qsch b/examples/testfiles/Qspice_top.qsch new file mode 100644 index 0000000..2f5d1c8 --- /dev/null +++ b/examples/testfiles/Qspice_top.qsch @@ -0,0 +1,96 @@ +ÿØÿÛ«schematic + «component (-1000,0) 0 0 + «symbol V + «type: V» + «description: Independent Voltage Source» + «shorted pins: false» + «line (0,-130) (0,-200) 0 0 0x1000000 -1 -1» + «line (0,200) (0,130) 0 0 0x1000000 -1 -1» + «rect (-25,77) (25,73) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-2,50) (2,100) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-25,-73) (25,-77) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «ellipse (-130,130) (130,-130) 0 0 0 0x1000000 0x1000000 -1 -1» + «text (100,150) 1 7 0 0x1000000 -1 -1 "V1"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "PWL 0 0 1m 5"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "+"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "-"» + » + » + «component (1400,200) 0 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (100,150) 1 7 0 0x1000000 -1 -1 "R1"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "10K"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «component (-800,800) 0 0 + «symbol V + «type: V» + «description: Independent Voltage Source» + «shorted pins: false» + «line (0,-130) (0,-200) 0 0 0x1000000 -1 -1» + «line (0,200) (0,130) 0 0 0x1000000 -1 -1» + «rect (-25,77) (25,73) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-2,50) (2,100) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-25,-73) (25,-77) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «ellipse (-130,130) (130,-130) 0 0 0 0x1000000 0x1000000 -1 -1» + «text (100,150) 1 7 0 0x1000000 -1 -1 "V2"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "5"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "+"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "-"» + » + » + «component (300,400) 0 0 + «symbol + «type: » + «description: MyDiff» + «shorted pins: false» + «rect (-500,-400) (500,400) 0 0 0 0xff0000 0xc8c8c8 -1 1 -1» + «text (-100,0) 1 12 0 0x1000000 -1 -1 "X2"» + «text (-300,-400) 0.681 13 0 0x1000000 -1 -1 "sub_circuit2"» + «pin (-500,-100) (0,0) 1 7 0 0x0 -1 "INP"» + «pin (-500,100) (0,0) 1 7 0 0x0 -1 "INN"» + «pin (0,400) (180,-190) 1 14 0 0x0 -1 "VDD"» + «pin (0,-400) (150,0) 1 14 0 0x0 -1 "VSS"» + «pin (500,0) (-150,0) 1 11 0 0x0 -1 "OUT"» + » + » + «component (-1700,300) 0 0 + «symbol V + «type: V» + «description: Independent Voltage Source» + «shorted pins: false» + «line (0,-130) (0,-200) 0 0 0x1000000 -1 -1» + «line (0,200) (0,130) 0 0 0x1000000 -1 -1» + «rect (-25,77) (25,73) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-2,50) (2,100) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «rect (-25,-73) (25,-77) 0 0 0 0x1000000 0x3000000 -1 0 -1» + «ellipse (-130,130) (130,-130) 0 0 0 0x1000000 0x1000000 -1 -1» + «text (150,50) 1 7 0 0x1000000 -1 -1 "V3"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "1"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "+"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "-"» + » + » + «net (-1000,-200) 1 13 0 "GND"» + «net (1400,0) 1 13 0 "GND"» + «net (-800,600) 1 13 0 "GND"» + «net (300,-500) 1 13 0 "GND"» + «net (-1700,100) 1 13 0 "GND"» + «wire (800,400) (1400,400) "N01"» + «wire (-800,1000) (300,1000) "N02"» + «wire (300,1000) (300,800) "N02"» + «wire (300,0) (300,-500) "GND"» + «wire (-200,500) (-1700,500) "N03"» + «wire (-1000,200) (-1000,300) "N04"» + «wire (-1000,300) (-200,300) "N04"» + «text (-1400,-350) 1 7 0 0x1000000 -1 -1 ".tran 1m"» +» + diff --git a/examples/testfiles/sub_circuit2.qsch b/examples/testfiles/sub_circuit2.qsch new file mode 100644 index 0000000..78513d7 --- /dev/null +++ b/examples/testfiles/sub_circuit2.qsch @@ -0,0 +1,157 @@ +ÿØÿÛ«schematic + «component (-1300,200) 2 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (30,350) 1 110 0 0x1000000 -1 -1 "R1"» + «text (320,0) 1 109 0 0x1000000 -1 -1 "10K"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «component (-1300,-200) 2 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (30,350) 1 110 0 0x1000000 -1 -1 "R2"» + «text (-80,0) 1 109 0 0x1000000 -1 -1 "10K"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «component (-700,-600) 4 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (130,-150) 1 75 0 0x1000000 -1 -1 "R3"» + «text (130,150) 1 75 0 0x1000000 -1 -1 "22K"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «component (100,1200) 6 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (-80,0) 1 46 0 0x1000000 -1 -1 "R4"» + «text (80,0) 1 45 0 0x1000000 -1 -1 "22K"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «component (-200,0) 0 0 + «symbol OpAmp + «type: û + «description: Generic Rail-to-Rail Output OpAmp» + «shorted pins: false» + «line (-100,450) (-100,-450) 0 0 0x1000000 -1 -1» + «line (500,0) (-100,450) 0 0 0x1000000 -1 -1» + «line (-50,200) (50,200) 0 0 0x1000000 -1 3» + «line (-50,-200) (50,-200) 0 0 0x1000000 -1 4» + «line (0,-250) (0,-150) 0 0 0x1000000 -1 4» + «line (500,0) (-100,-450) 0 0 0x1000000 -1 -1» + «text (202,300) 1 7 0 0x1000000 -1 -1 "Ã1"» + «text (200,0) 0.5 0 2 0x1000000 -1 -1 "RRopAmp"» + «text (454,-71) 0.5 7 0 0x1000000 -1 -1 "Avol=<100K>"» + «text (334,-151) 0.5 7 0 0x1000000 -1 -1 "GBW=<5Meg>"» + «text (244,-221) 0.5 7 0 0x1000000 -1 -1 "Slew=<5Meg>"» + «text (244,-321) 0.5 7 0 0x1000000 -1 -1 "Rload=<2K>"» + «text (244,-421) 0.5 7 0 0x1000000 -1 -1 "Phi=<60>"» + «pin (100,300) (0,0) 1 0 0 0x1000000 -1 "Vdd"» + «pin (100,-300) (0,0) 1 0 0 0x1000000 -1 "Vss"» + «pin (500,0) (0,0) 1 15 0 0x1000000 -1 "OUT"» + «pin (-100,200) (50,0) 1 15 0 0x1000000 -1 "IN-"» + «pin (-100,-200) (60,0) 1 15 0 0x1000000 -1 "IN+"» + «pin (0,100) (0,0) 1 15 0 0x1000000 -1 "MULT+" "¥"» + «pin (0,-100) (0,0) 1 15 0 0x1000000 -1 "MULT-" "¥"» + «pin (300,0) (60,0) 1 15 0 0x1000000 -1 "IN--" "¥"» + «pin (400,0) (20,0) 0.794 15 0 0x1000000 -1 "IN++" "¥"» + «pin (-100,0) (20,0) 0.794 7 0 0x1000000 -1 "EN" "¥"» + » + » + «component (1500,1000) 0 0 + «symbol Zener + «type: D» + «description: Zener Diode» + «library file: Zener.txt» + «shorted pins: false» + «line (80,80) (-80,80) 0 0 0x1000000 -1 -1» + «line (0,200) (0,80) 0 0 0x1000000 -1 -1» + «line (0,-200) (0,-70) 0 0 0x1000000 -1 -1» + «line (80,80) (130,130) 0 0 0x1000000 -1 -1» + «line (-130,30) (-80,80) 0 0 0x1000000 -1 -1» + «triangle (0,80) (100,-70) (-100,-70) 0 0 0x1000000 0x2000000 -1 -1» + «text (100,200) 1 7 0 0x1000000 -1 -1 "D1"» + «text (100,-200) 1 7 0 0x1000000 -1 -1 "MM3Z5V1T1G"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "A"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "K"» + » + » + «component (1500,1700) 0 0 + «symbol R + «type: R» + «description: Resistor(USA Style Symbol)» + «shorted pins: false» + «line (0,200) (0,180) 0 0 0x1000000 -1 -1» + «line (0,-180) (0,-200) 0 0 0x1000000 -1 -1» + «zigzag (-80,180) (80,-180) 0 0 0 0x1000000 -1 -1» + «text (100,150) 1 7 0 0x1000000 -1 -1 "R5"» + «text (100,-150) 1 7 0 0x1000000 -1 -1 "100"» + «pin (0,200) (0,0) 1 0 0 0x0 -1 "1"» + «pin (0,-200) (0,0) 1 0 0 0x0 -1 "2"» + » + » + «net (-2100,200) 1 11 1 "INN"» + «net (1500,2100) 1 14 1 "VDD"» + «net (-100,-1200) 1 13 1 "VSS"» + «net (1800,0) 1 7 1 "OUT"» + «net (-2100,-200) 1 11 1 "INP"» + «junction (1500,1300)» + «junction (800,0)» + «junction (-100,-1000)» + «junction (-700,200)» + «junction (-700,-200)» + «wire (800,0) (1800,0) "OUT"» + «wire (-100,-1000) (-100,-1200) "VSS"» + «wire (-1500,-200) (-2100,-200) "INP"» + «wire (-1500,200) (-2100,200) "INN"» + «wire (300,1200) (800,1200) "OUT"» + «wire (800,1200) (800,0) "OUT"» + «wire (300,0) (800,0) "OUT"» + «wire (-700,1200) (-700,200) "N01"» + «wire (-300,200) (-700,200) "N01"» + «wire (-700,-200) (-1100,-200) "N02"» + «wire (-700,200) (-1100,200) "N01"» + «wire (-700,1200) (-100,1200) "N01"» + «wire (-700,-400) (-700,-200) "N02"» + «wire (-300,-200) (-700,-200) "N02"» + «wire (-700,-800) (-700,-1000) "VSS"» + «wire (-100,-300) (-100,-1000) "VSS"» + «wire (-700,-1000) (-100,-1000) "VSS"» + «wire (-100,300) (-100,800) "N03"» + «wire (-100,800) (1100,800) "N03"» + «wire (-100,-1000) (1500,-1000) "VSS"» + «wire (1500,-1000) (1500,800) "VSS"» + «wire (1500,1900) (1500,2100) "VDD"» + «wire (1100,800) (1100,1300) "N03"» + «wire (1100,1300) (1500,1300) "N03"» + «wire (1500,1200) (1500,1300) "N03"» + «wire (1500,1300) (1500,1500) "N03"» +» + diff --git a/spicelib/__init__.py b/spicelib/__init__.py index 3913ea0..8905026 100644 --- a/spicelib/__init__.py +++ b/spicelib/__init__.py @@ -18,12 +18,14 @@ def all_loggers(): """ return [ "spicelib.AscEditor", + "spicelib.AscToQsch", "spicelib.AsyReader", "spicelib.BaseEditor", "spicelib.BaseSchematic", "spicelib.LTSpiceSimulator", "spicelib.LTSteps", "spicelib.NGSpiceSimulator", + "spicelib.QschEditor", "spicelib.qspice_log_reader", "spicelib.QSpiceSimulator", "spicelib.RawRead", diff --git a/spicelib/editor/asc_editor.py b/spicelib/editor/asc_editor.py index b58b5b7..0edbe9c 100644 --- a/spicelib/editor/asc_editor.py +++ b/spicelib/editor/asc_editor.py @@ -304,7 +304,9 @@ def _get_subcircuit(self, symbol: AsyReader) -> Union[SpiceEditor, 'AscEditor']: def get_subcircuit(self, reference: str) -> 'AscEditor': """Returns an AscEditor file corresponding to the symbol""" sub = self.get_component(reference) - return sub.attributes['_SUBCKT'] + if '_SUBCKT' in sub.attributes: + return sub.attributes['_SUBCKT'] + raise AttributeError(f"An associated subcircuit was not found for {reference}") def get_component_info(self, reference) -> dict: """Returns the reference information as a dictionary""" @@ -332,7 +334,19 @@ def _get_param_named(self, param_name): if match.group("name").upper() == param_name_uppercase: return match, directive return None, None - + + def get_all_parameter_names(self) -> List[str]: + # docstring inherited from BaseEditor + param_names = [] + search_expression = re.compile(PARAM_REGEX(r"\w+"), re.IGNORECASE) + for directive in self.directives: + if directive.text.upper().startswith(".PARAM"): + matches = search_expression.finditer(directive.text) + for match in matches: + param_name = match.group('name') + param_names.append(param_name.upper()) + return sorted(param_names) + def get_parameter(self, param: str) -> str: match, directive = self._get_param_named(param) if match: diff --git a/spicelib/editor/base_editor.py b/spicelib/editor/base_editor.py index 3109196..eca45ec 100644 --- a/spicelib/editor/base_editor.py +++ b/spicelib/editor/base_editor.py @@ -478,6 +478,16 @@ def get_parameter(self, param: str) -> str: :raises: ParameterNotFoundError - In case the component is not found """ ... + + @abstractmethod + def get_all_parameter_names(self, param: str) -> str: + """ + Returns all parameter names from the netlist. + + :return: A list of parameter names found in the netlist + :rtype: List[str] + """ + ... def set_parameter(self, param: str, value: Union[str, int, float]) -> None: """Adds a parameter to the SPICE netlist. diff --git a/spicelib/editor/qsch_editor.py b/spicelib/editor/qsch_editor.py index 7b1fa49..5daa67f 100644 --- a/spicelib/editor/qsch_editor.py +++ b/spicelib/editor/qsch_editor.py @@ -37,7 +37,7 @@ __all__ = ('QschEditor', 'QschTag', 'QschReadingError') -_logger = logging.getLogger("qspice.QschEditor") +_logger = logging.getLogger("spicelib.QschEditor") QSCH_HEADER = (255, 216, 255, 219) @@ -171,6 +171,11 @@ def decap(s: str) -> str: return regex.sub(r"\1=\2", s) +def smart_split(s): + """Splits a string into chunks based on spaces. What is inside "" is not divided.""" + return re.findall(r'[^"\s]+|"[^"]*"', s) + + class QschReadingError(IOError): ... @@ -205,32 +210,31 @@ def parse(cls, stream: str, start: int = 0) -> Tuple['QschTag', int]: child, i = QschTag.parse(stream, i) i0 = i + 1 self.items.append(child) - elif stream[i] == '»': - stop = i + 1 - if i > i0: - self.tokens.append(stream[i0:i]) - return self, stop - elif stream[i] == ' ' or stream[i] == '\n': - if i > i0: - self.tokens.append(stream[i0:i]) - i0 = i + 1 elif stream[i] == '"': # get all characters until the next " sign i += 1 while stream[i] != '"': i += 1 - elif stream[i] == '(': - # todo: support also [] and {} - nested = 1 - while nested > 0: - i += 1 - if stream[i] == '(': - nested += 1 - elif stream[i] == ')': - nested -= 1 + elif stream[i] == '»': + stop = i + 1 + break + elif stream[i] == '\n': + if i > i0: + tokens = smart_split(stream[i0:i]) + self.tokens.extend(tokens) + i0 = i + 1 i += 1 else: raise IOError("Missing » when reading file") + line = stream[i0:i] + # Now dividing the + if ': ' in line: + name, text = line.split(': ') + self.tokens.append(name + ":") + self.tokens.append(text) + else: + self.tokens.extend(smart_split(line)) + return self, stop def __str__(self): """Returns only the first line of the tag. The children are not shown.""" @@ -255,7 +259,7 @@ def out(self, level): def tag(self) -> str: """Returns the tag id of the object. The tag id is the first token in the tag.""" return self.tokens[0] - + def get_items(self, item) -> List['QschTag']: """Returns a list of children tags that match the given tag id.""" answer = [tag for tag in self.items if tag.tag == item] @@ -285,10 +289,13 @@ def get_attr(self, index: int): try: value = int(a) except ValueError: - value = float(a) + try: + value = float(a) + except ValueError: + value = a return value - def set_attr(self, index: int, value): + def set_attr(self, index: int, value: Union[str, int, tuple]): """Sets the attribute at the given index. The attribute can be a string, an integer or a tuple. Integer values are written as integers, strings are written between quotes unless it starts with "0x" and tuples are written between parenthesis. @@ -296,7 +303,7 @@ def set_attr(self, index: int, value): :param index: The index of the attribute to be set :type index: int :param value: The value to be set - :type value: Union[str, int, tuple] + :type value: Union[str, int, Tuple[Any, Any]] :return: Nothing """ if isinstance(value, int): @@ -312,9 +319,19 @@ def set_attr(self, index: int, value): raise ValueError("Object not supported in set_attr") self.tokens[index] = value_str - def get_text(self, label, default: str = None) -> str: + def get_text(self, label: str, default: str = None) -> str: """ - Returns the text of the first child tag that matches the given label. + Returns the text of the first child tag that matches the given label. The label can have up to 1 space in it. + It will return the entire text of the tag, after the label. + If the label is not found, it returns the default value. + + :param label: label to be found. Can have up to 1 space (e.g. "library file" or "shorted pins") + :type label: str + :param default: Default value, defaults to None + :type default: str, optional + :raises IndexError: When the label is not found and the default value is None + :return: the found text or the default value + :rtype: str """ a = self.get_items(label + ':') if len(a) != 1: @@ -352,7 +369,7 @@ class QschEditor(BaseSchematic): :meta hide-value: """ - + def __init__(self, qsch_file: str, create_blank: bool = False): super().__init__() self._qsch_file_path = Path(qsch_file) @@ -427,23 +444,43 @@ def write_spice_to_file(self, netlist_file: TextIO): ports += ['Â¥'] * (16 - len(ports)) nets = " ".join(ports) + + have_embedded_subcircuit = False + # Check the libraries and embedded subcircuits + library_name = symbol_tag.get_text('library file', default="") + if library_name and (library_name not in libraries_to_include): + marker = "|.subckt" + if library_name.startswith(marker): + # This is an embedded subcircuit, print it here, not at the end. It must be printed before the component + sub_circuit_content = library_name[len(marker):].strip() # The section after "|.subckt" + sub_circuit_content = sub_circuit_content.replace("\\n", "\n") + netlist_file.write(f".subckt {refdes}•{sub_circuit_content}\n") + have_embedded_subcircuit = True + else: + # List the libraries at the end + libraries_to_include.append(library_name) if typ == 'X': model = texts[1].get_text_attr(QSCH_TEXT_STR_ATTR) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" # schedule to write .SUBCKT clauses at the end if model not in subcircuits_to_write: - pins = symbol_tag.get_items("pin") - sub_ports = " ".join(pin.get_attr(QSCH_SYMBOL_PIN_NET) for pin in pins) - subcircuits_to_write[model] = ( - component.attributes['_SUBCKT'], # the subcircuit schematic is saved - sub_ports, # and also storing the port position now, so to save time later. - ) + if '_SUBCKT' in component.attributes: + pins = symbol_tag.get_items("pin") + sub_ports = " ".join(pin.get_attr(QSCH_SYMBOL_PIN_NET) for pin in pins) + subcircuits_to_write[model] = ( + component.attributes['_SUBCKT'], # the subcircuit schematic is saved + sub_ports, # and also storing the port position now, so to save time later. + ) nets = " ".join(component.ports) netlist_file.write(f'{refdes} {nets} {model}{parameters}\n') elif typ in ('QP', 'QN'): model = texts[1].get_text_attr(QSCH_TEXT_STR_ATTR) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" if symbol == 'NPNS' or symbol == 'PNPS' or symbol == 'LPNP': ports[3] = '[' + ports[3] + ']' nets = ' '.join(ports) @@ -453,6 +490,8 @@ def write_spice_to_file(self, netlist_file: TextIO): netlist_file.write(f'{refdes} {nets} [0] {model} {symbol}{parameters}\n') elif typ in ('MN', 'MP'): model = texts[1].get_text_attr(QSCH_TEXT_STR_ATTR) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" if symbol == 'NMOSB' or symbol == 'PMOSB': symbol = symbol[0:4] if len(ports) == 3: @@ -461,17 +500,25 @@ def write_spice_to_file(self, netlist_file: TextIO): netlist_file.write(f'{refdes} {nets} {model} {symbol}{parameters}\n') elif typ == 'T': model = decap(texts[1].get_text_attr(QSCH_TEXT_STR_ATTR)) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" netlist_file.write(f'{refdes} {nets} {model}{parameters}\n') elif typ in ('JN', 'JP'): model = decap(texts[1].get_text_attr(QSCH_TEXT_STR_ATTR)) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" if symbol.startswith('Pwr'): # Hack alert. I don't know why the symbol is Pwr symbol = symbol[3:] # remove the Pwr from the symbol netlist_file.write(f'{refdes} {nets} {model} {symbol}{parameters}\n') elif typ == '×': model = decap(texts[1].get_text_attr(QSCH_TEXT_STR_ATTR)) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" netlist_file.write(f'{refdes} «{nets}» {model}{parameters}\n') elif typ in ('ZP', 'ZN'): model = texts[1].get_text_attr(QSCH_TEXT_STR_ATTR) + if have_embedded_subcircuit: + model = f"{refdes}•{model}" netlist_file.write(f'{refdes} {nets} {model} {symbol}{parameters}\n') else: value = texts[1].get_text_attr(QSCH_TEXT_STR_ATTR) @@ -479,12 +526,6 @@ def write_spice_to_file(self, netlist_file: TextIO): # else: # netlist_file.write(f'{symbol}†{refdes} {nets} {value}\n') - library_tags = symbol_tag.get_items('library') - for lib in library_tags: - library_name = lib.get_text_attr(2) - if library_name not in libraries_to_include: - libraries_to_include.append(library_name) - for sub_circuit in subcircuits_to_write: sub_circuit_schematic, ports = subcircuits_to_write[sub_circuit] netlist_file.write("\n") @@ -499,7 +540,8 @@ def write_spice_to_file(self, netlist_file: TextIO): netlist_file.write(line.strip() + '\n') for library in libraries_to_include: - library_path = self._qsch_file_find(library) + mydir = self.circuit_file.parent.absolute().as_posix() + library_path = self._qsch_file_find(library, mydir) if library_path is None: netlist_file.write(f'.lib {library}\n') else: @@ -619,6 +661,7 @@ def _parse_qsch_stream(self, stream): components = self.schematic.get_items('component') for component in components: + have_embedded_subcircuit = False symbol: QschTag = component.get_items('symbol')[0] texts = symbol.get_items('text') if len(texts) < 2: @@ -632,6 +675,10 @@ def _parse_qsch_stream(self, stream): sch_comp.position = Point(x, y) sch_comp.rotation = orientation * 45 sch_comp.attributes['type'] = symbol.get_text('type', "X") # Assuming a sub-circuit + # a bit complicated way to detect embedded subcircuits: they are in the library tag, + lib = symbol.get_text('library file', "-") + if lib.startswith("|.subckt"): + have_embedded_subcircuit = True sch_comp.attributes['description'] = symbol.get_text('description', "No Description") sch_comp.attributes['value'] = value sch_comp.attributes['tag'] = component @@ -665,11 +712,15 @@ def _parse_qsch_stream(self, stream): self.components[refdes] = sch_comp if refdes.startswith('X'): - sub_circuit_name = value + os.path.extsep + 'qsch' - sub_circuit_schematic_file = self._qsch_file_find(sub_circuit_name) - if sub_circuit_schematic_file: - sub_schematic = QschEditor(sub_circuit_schematic_file) - sch_comp.attributes['_SUBCKT'] = sub_schematic # Store it for future use. + if not have_embedded_subcircuit: + sub_circuit_name = value + os.path.extsep + 'qsch' + mydir = self.circuit_file.parent.absolute().as_posix() + sub_circuit_schematic_file = self._qsch_file_find(sub_circuit_name, mydir) + if sub_circuit_schematic_file: + sub_schematic = QschEditor(sub_circuit_schematic_file) + sch_comp.attributes['_SUBCKT'] = sub_schematic # Store it for future use. + else: + _logger.warning(f"Subcircuit '{sub_circuit_name}' not found. Have you set the correct search paths?") for text_tag in self.schematic.get_items('text'): x, y = text_tag.get_attr(QSCH_TEXT_POS) @@ -720,13 +771,31 @@ def _get_param_named(self, param_name): return tag, match else: return None, None + + def get_all_parameter_names(self) -> List[str]: + # docstring inherited from BaseEditor + param_names = [] + param_regex = re.compile(PARAM_REGEX(r"\w+"), re.IGNORECASE) + text_tags = self.schematic.get_items('text') + for tag in text_tags: + line = tag.get_attr(QSCH_TEXT_STR_ATTR) + line = line.lstrip(QSCH_TEXT_INSTR_QUALIFIER) + if line.upper().startswith('.PARAM'): + matches = param_regex.finditer(line) + for match in matches: + param_name = match.group('name') + param_names.append(param_name.upper()) + return sorted(param_names) - def _qsch_file_find(self, filename) -> Optional[str]: + def _qsch_file_find(self, filename: str, work_dir: str = None) -> Optional[str]: containers = ['.'] + self.custom_lib_paths + self.simulator_lib_paths # '.' is the directory where the script is located + if (work_dir is not None) and work_dir != ".": + containers = [work_dir] + containers # put work directory first return search_file_in_containers(filename, *containers) def get_subcircuit(self, reference: str) -> 'QschEditor': + """Returns an QschEditor file corresponding to the symbol""" subcircuit = self.get_component(reference) if '_SUBCKT' in subcircuit.attributes: # Optimization: if it was already stored, return it return subcircuit.attributes['_SUBCKT'] diff --git a/spicelib/editor/spice_editor.py b/spicelib/editor/spice_editor.py index 5e0c491..37dcf0a 100644 --- a/spicelib/editor/spice_editor.py +++ b/spicelib/editor/spice_editor.py @@ -394,6 +394,19 @@ def _get_param_named(self, param_name) -> Tuple[int, Union[re.Match, None]]: line_no += 1 return -1, None # If it fails, it returns an invalid line number and No match + def get_all_parameter_names(self) -> List[str]: + # docstring inherited from BaseEditor + param_names = [] + search_expression = re.compile(PARAM_REGEX(r"\w+"), re.IGNORECASE) + for line in self.netlist: + cmd = get_line_command(line) + if cmd == '.PARAM': + matches = search_expression.finditer(line) + for match in matches: + param_name = match.group('name') + param_names.append(param_name.upper()) + return sorted(param_names) + def get_subcircuit_names(self) -> List[str]: """ Returns a list of the names of the sub-circuits in the netlist. diff --git a/spicelib/scripts/asc_to_qsch.py b/spicelib/scripts/asc_to_qsch.py index 8d75e4a..58ce5a4 100644 --- a/spicelib/scripts/asc_to_qsch.py +++ b/spicelib/scripts/asc_to_qsch.py @@ -26,7 +26,7 @@ from spicelib.editor.qsch_editor import QschEditor from spicelib.utils.file_search import find_file_in_directory -_logger = logging.getLogger() +_logger = logging.getLogger("spicelib.AscToQsch") def main(): diff --git a/spicelib/utils/file_search.py b/spicelib/utils/file_search.py index ddb9954..132d2d4 100644 --- a/spicelib/utils/file_search.py +++ b/spicelib/utils/file_search.py @@ -77,4 +77,5 @@ def search_file_in_containers(filename, *containers) -> Optional[str]: if filefound is not None: _logger.debug(f"Found '{filefound}'") return filefound + _logger.debug(f"Searching for '{filename}': NOT Found") return None diff --git a/unittests/golden/Qspice_top_edit.net b/unittests/golden/Qspice_top_edit.net new file mode 100644 index 0000000..e30ebe3 --- /dev/null +++ b/unittests/golden/Qspice_top_edit.net @@ -0,0 +1,20 @@ +* C:\sandbox\spicelib_dev\examples\testfiles\Qspice_top.qsch +V1 N04 0 PWL 0 0 1m 5 +R1 N01 0 10K +V2 N02 0 5 +X2 N04 N03 N02 0 N01 sub_circuit2 +V3 N03 0 1 + +.subckt sub_circuit2 INP INN VDD VSS OUT +R1 INN N01 10K +R2 INP N02 10K +R3 VSS N02 22K +R4 OUT N01 22K +Ã1 N03 VSS OUT N01 N02 ¥ ¥ ¥ ¥ ¥ ¥ ¥ ¥ ¥ ¥ ¥ RRopAmp Avol=100K GBW=5Meg Slew=5Meg Rload=2K Phi=60 +D1 VSS N03 MM3Z5V1T1G +R5 VDD N03 220 +.lib C:\PROGRA~1\QSPICE\Zener.txt +.ends sub_circuit2 + +.tran 1m +.end diff --git a/unittests/golden/qsch_embedded_subckt.net b/unittests/golden/qsch_embedded_subckt.net new file mode 100644 index 0000000..104f2bb --- /dev/null +++ b/unittests/golden/qsch_embedded_subckt.net @@ -0,0 +1,9 @@ +* C:\Users\hans\Documents\workspace\spicelib\reproducing_error\154\bug_test.qsch +V1 IN 0 1 +R1 OUT 0 1Meg +.subckt X2•mycomp OUT IN +E1 OUT 0 IN 0 {P} +.ends mycomp +X2 OUT IN X2•mycomp P=2 +.tran 0 1m 0 100n uic +.end diff --git a/unittests/test_asc_editor.py b/unittests/test_asc_editor.py index fb5cb59..a54cde4 100644 --- a/unittests/test_asc_editor.py +++ b/unittests/test_asc_editor.py @@ -78,6 +78,7 @@ def test_component_legacy_editing(self): self.equalFiles(temp_dir + 'test_components_output_1.asc', golden_dir + 'test_components_output_1.asc') def test_parameter_edit(self): + self.assertEqual(self.edt.get_all_parameter_names(), ['RES', 'TEMP']) self.assertEqual(self.edt.get_parameter('TEMP'), '0', "Tested TEMP Parameter") # add assertion here self.edt.set_parameter('TEMP', 25) self.assertEqual(self.edt.get_parameter('TEMP'), '25', "Tested TEMP Parameter") # add assertion here diff --git a/unittests/test_qsch_editor.py b/unittests/test_qsch_editor.py index 480a77b..fbf9368 100644 --- a/unittests/test_qsch_editor.py +++ b/unittests/test_qsch_editor.py @@ -94,6 +94,7 @@ def test_component_editing_obj(self): self.assertEqual(r1_params[key], value, f"Tested R1 {key} Parameter") def test_parameter_edit(self): + self.assertEqual(self.edt.get_all_parameter_names(), ['RES', 'TEMP']) self.assertEqual(self.edt.get_parameter('TEMP'), '0', "Tested TEMP Parameter") # add assertion here self.edt.set_parameter('TEMP', 25) self.assertEqual(self.edt.get_parameter('TEMP'), '25', "Tested TEMP Parameter") # add assertion here @@ -170,5 +171,25 @@ def test_floating_net(self): equalFiles(self, temp_dir + 'qsch_floating_net.net', golden_dir + "qsch_floating_net.net") +class QschEditorEmbeddedSubckt(unittest.TestCase): + + def test_embedded_subckt(self): + self.edt = spicelib.editor.qsch_editor.QschEditor(test_dir + "Qspice_bug_embedded_subckt.qsch") + self.edt.save_netlist(temp_dir + 'qsch_embedded_subckt.net') + equalFiles(self, temp_dir + 'qsch_embedded_subckt.net', golden_dir + "qsch_embedded_subckt.net") + + def test_sub_circuit(self): + self.edt = spicelib.editor.qsch_editor.QschEditor(test_dir + "Qspice_top.qsch") + # get sub-circuit + sub = self.edt.get_subcircuit("X2") + sub_desc = self.edt.get_component_value('X2') + self.assertEqual("sub_circuit2", sub_desc, "Value mismatch") + self.assertEqual('10K', sub.get_component_value("R1")) + self.assertEqual('22K', sub.get_component_value("R4")) + sub.set_component_value('R5', 220) + self.edt.save_netlist(temp_dir + 'Qspice_top_edit.net') + equalFiles(self, golden_dir + 'Qspice_top_edit.net', temp_dir + 'Qspice_top_edit.net') + + if __name__ == '__main__': unittest.main() diff --git a/unittests/test_spice_editor.py b/unittests/test_spice_editor.py index 0af225b..6c94c55 100644 --- a/unittests/test_spice_editor.py +++ b/unittests/test_spice_editor.py @@ -122,6 +122,7 @@ def test_component_editing_2(self): self.equalFiles(temp_dir + 'opamptest_output_1.net', golden_dir + 'opamptest_output_1.net') def test_parameter_edit(self): + self.assertEqual(self.edt.get_all_parameter_names(), ['RES', 'TEMP']) self.assertEqual(self.edt.get_parameter('TEMP'), '0', "Tested TEMP Parameter") # add assertion here self.edt.set_parameter('TEMP', 25) self.assertEqual(self.edt.get_parameter('TEMP'), '25', "Tested TEMP Parameter") # add assertion here