Skip to content

Commit 0ac580b

Browse files
improve kicad support
1 parent 914c993 commit 0ac580b

2 files changed

Lines changed: 202 additions & 41 deletions

File tree

PySpice/KiCad/__init__.py

Lines changed: 177 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,183 @@
88

99
__all__ = [
1010
'PythonDumper',
11+
'Spicedumper',
1112
]
1213

1314
####################################################################################################
1415

15-
import os
1616
import logging
17-
18-
from typing import Callable
17+
import os
18+
from collections.abc import Callable
19+
from typing import cast
1920

2021
try:
21-
from KiCadRW.sexp.schema import KiCadSchema, Symbol
22+
from kicadrw.sexp.schema import KiCadSchema, Symbol
2223
except ImportError:
23-
KiCadSchema = None
24+
KiCadSchema = None # ty: ignore[invalid-assignment]
2425

2526
####################################################################################################
2627

2728
_module_logger = logging.getLogger(__name__)
2829

30+
LINESEP = os.linesep
31+
32+
type ElementHandler = Callable[[PythonDumper, Symbol], str]
33+
34+
####################################################################################################
35+
36+
class BaseDumper:
37+
38+
def generic_wrapper(element: str) -> ElementHandler:
39+
def wrapper(self, symbol):
40+
return self.on_generic(element, symbol)
41+
return wrapper
42+
43+
def generic_model_wrapper(element: str) -> ElementHandler:
44+
def wrapper(self, symbol):
45+
return self.on_generic_model(element, symbol)
46+
return wrapper
47+
48+
def source(element: str) -> ElementHandler:
49+
def wrapper(self, symbol):
50+
return self.on_source(element, symbol)
51+
return wrapper
52+
53+
GROUND = 0
54+
55+
SYMBOL_MAP: dict[str, ElementHandler | int] = {
56+
'R': generic_wrapper('R'),
57+
'L': generic_wrapper('L'),
58+
'C': generic_wrapper('C'),
59+
'D': generic_model_wrapper('D'),
60+
'GND': GROUND, # Fixme: typing is int
61+
'V': source('V'),
62+
# 'VDC': source('V'),
63+
# 'VPULSE': source('V'),
64+
}
65+
66+
##############################################
67+
68+
def __init__(self, kicad_schema: KiCadSchema, use_pyspice_unit: bool = False) -> None:
69+
self._use_pyspice_unit = use_pyspice_unit
70+
self._code = []
71+
72+
for symbol in kicad_schema.symbols_by_reference:
73+
self._logger.info(f"Symbol {symbol.lib_name} {symbol.reference} {symbol.simulation_device}")
74+
handler = self.find_symbol(symbol)
75+
match handler:
76+
case None:
77+
self._logger.warning(f"any correspondance for '{symbol.lib_name}' '{symbol.reference}' '{symbol.simulation_device}'")
78+
case int(): # for ground i.e. != self.GROUND
79+
pass
80+
case _:
81+
_ = handler(self, symbol)
82+
self._code.append(_)
83+
84+
##############################################
85+
86+
def find_symbol(self, symbol: Symbol) -> ElementHandler | int | None:
87+
name = symbol.simulation_device
88+
if not name:
89+
_, name = symbol.lib_name.split(':')
90+
return self.SYMBOL_MAP.get(name, None)
91+
92+
##############################################
93+
94+
def __str__(self) -> str:
95+
return LINESEP.join(self._code)
96+
97+
####################################################################################################
98+
99+
class SpiceDumper(BaseDumper):
100+
101+
_logger = _module_logger.getChild('SpiceDumper')
102+
103+
##############################################
104+
105+
def _pins(self, symbol: Symbol) -> list[int | str]:
106+
pins: list[int | str] = []
107+
for pin in symbol.pins:
108+
_id = cast(int | str, pin.inet.id) # Fixme: due to None...
109+
# if _id == 0:
110+
# _id = 'GND'
111+
pins.append(_id)
112+
return pins
113+
114+
##############################################
115+
116+
def _unit_value(self, element: str, symbol: Symbol) -> str:
117+
return symbol.value
118+
119+
##############################################
120+
121+
def _str_args(self, raw_args: list[str | int | float]) -> str:
122+
args = []
123+
for arg in raw_args:
124+
# if isinstance(arg, str) and ('@' in arg or arg.startswith('circuit.')):
125+
# pass
126+
if isinstance(arg, (int, float)):
127+
arg = str(arg)
128+
args.append(arg)
129+
return ' '.join(args)
130+
131+
##############################################
132+
133+
def on_generic(self, element: str, symbol: Symbol) -> str:
134+
self._logger.info(f"Element '{symbol.reference}' params='{symbol.simulation_paramaters}'")
135+
reference = symbol.reference[len(element):]
136+
value = self._unit_value(element, symbol)
137+
args = [reference, *self._pins(symbol), value]
138+
args_str = self._str_args(args) # ty: ignore[invalid-argument-type]
139+
return f"{element}{args_str}"
140+
141+
##############################################
142+
143+
def on_generic_model(self, element: str, symbol: Symbol) -> str:
144+
self._logger.info(f"Element with model '{symbol.reference}' pins='{symbol.simulation_pins}' params='{symbol.simulation_paramaters}'")
145+
# Fixme: check XD
146+
reference = symbol.reference[len(element):] # +1
147+
# pin_names = [_.name for _ in symbol.pins]
148+
pins = self._pins(symbol)
149+
# match pin_names:
150+
# case ('K', 'A'):
151+
# pins = list(reversed(pins))
152+
args = [reference, *pins]
153+
args_str = self._str_args(args) # ty: ignore[invalid-argument-type]
154+
model_name = f'__{symbol.value}{reference}'
155+
lines = [
156+
f".model {model_name} {element} {symbol.simulation_paramaters}",
157+
f"{element}{args_str} {model_name}",
158+
]
159+
# Fixme: __str__ join
160+
return LINESEP.join(lines)
161+
162+
##############################################
163+
164+
def on_source(self, element: str, symbol: Symbol) -> str:
165+
self._logger.info(f"Source '{symbol.simulation_device}' type='{symbol.simulation_type}' params='{symbol.simulation_paramaters}'")
166+
reference = symbol.reference[len(element):]
167+
args = [reference, *self._pins(symbol)]
168+
args_str = self._str_args(args) # ty: ignore[invalid-argument-type]
169+
return f"{element}{args_str} {symbol.simulation_type}( {symbol.simulation_paramaters} )"
170+
29171
####################################################################################################
30172

31173
class PythonDumper:
32174

33175
_logger = _module_logger.getChild('PythonDumper')
34176

35-
def generic_wrapper(element):
177+
def generic_wrapper(element: str) -> ElementHandler:
36178
def wrapper(self, symbol):
37179
return self.on_generic(element, symbol)
38180
return wrapper
39181

40-
def generic_model_wrapper(element):
182+
def generic_model_wrapper(element: str) -> ElementHandler:
41183
def wrapper(self, symbol):
42184
return self.on_generic_model(element, symbol)
43185
return wrapper
44186

45-
def source(element):
187+
def source(element: str) -> ElementHandler:
46188
def wrapper(self, symbol):
47189
return self.on_source(element, symbol)
48190
return wrapper
@@ -80,59 +222,62 @@ def wrapper(self, symbol):
80222
# 'spice-ngspice:ZENOR': None,
81223
# }
82224

83-
SYMBOL_MAP = {
225+
SYMBOL_MAP: dict[str, ElementHandler | int] = {
84226
'R': generic_wrapper('R'),
85227
'L': generic_wrapper('L'),
86228
'C': generic_wrapper('C'),
87229
'D': generic_model_wrapper('D'),
88-
'GND': GROUND,
230+
'GND': GROUND, # Fixme: typing is int
89231
'V': source('V'),
90232
# 'VDC': source('V'),
91233
# 'VPULSE': source('V'),
92234
}
93235

94236
##############################################
95237

96-
def __init__(self, kicad_schema: KiCadSchema, use_pyspice_unit=False):
238+
def __init__(self, kicad_schema: KiCadSchema, use_pyspice_unit: bool = False) -> None:
97239
self._use_pyspice_unit = use_pyspice_unit
98240
self._code = []
99241

100242
for symbol in kicad_schema.symbols_by_reference:
101243
self._logger.info(f"Symbol {symbol.lib_name} {symbol.reference} {symbol.simulation_device}")
102244
handler = self.find_symbol(symbol)
103-
if handler is None:
104-
self._logger.warning(f"any correspondance for {symbol.lib_name} {symbol.reference} {symbol.simulation_device}")
105-
elif handler != self.GROUND:
106-
_ = handler(self, symbol)
107-
self._code.append(_)
245+
match handler:
246+
case None:
247+
self._logger.warning(f"any correspondance for '{symbol.lib_name}' '{symbol.reference}' '{symbol.simulation_device}'")
248+
case int(): # for ground i.e. != self.GROUND
249+
pass
250+
case _:
251+
_ = handler(self, symbol)
252+
self._code.append(_)
108253

109254
##############################################
110255

111-
def find_symbol(self, symbol: Symbol) -> Callable:
256+
def find_symbol(self, symbol: Symbol) -> ElementHandler | int | None:
112257
name = symbol.simulation_device
113-
if name is None:
258+
if not name:
114259
_, name = symbol.lib_name.split(':')
115260
return self.SYMBOL_MAP.get(name, None)
116261

117262
##############################################
118263

119-
def __str__(self):
120-
return os.linesep.join(self._code)
264+
def __str__(self) -> str:
265+
return LINESEP.join(self._code)
121266

122267
##############################################
123268

124-
def _pins(self, symbol):
125-
pins = []
269+
def _pins(self, symbol: Symbol) -> list[int | str]:
270+
pins: list[int | str] = []
126271
for pin in symbol.pins:
127-
_id = pin.net.id
272+
_id = cast(int | str, pin.inet.id) # Fixme: due to None...
128273
if _id == 0:
129274
_id = 'circuit.gnd'
130275
pins.append(_id)
131276
return pins
132277

133278
##############################################
134279

135-
def _unit_value(self, element, symbol):
280+
def _unit_value(self, element: str, symbol: Symbol) -> str:
136281
value = symbol.value
137282
if not self._use_pyspice_unit:
138283
return value
@@ -150,7 +295,7 @@ def _unit_value(self, element, symbol):
150295

151296
##############################################
152297

153-
def _str_args(self, raw_args):
298+
def _str_args(self, raw_args: list[str | int | float]) -> str:
154299
args = []
155300
for arg in raw_args:
156301
if isinstance(arg, str) and ('@' in arg or arg.startswith('circuit.')):
@@ -165,17 +310,17 @@ def _str_args(self, raw_args):
165310

166311
##############################################
167312

168-
def on_generic(self, element, symbol):
313+
def on_generic(self, element: str, symbol: Symbol) -> str:
169314
self._logger.info(f"Element {symbol.reference} {symbol.simulation_paramaters}")
170315
reference = symbol.reference[len(element):]
171316
value = self._unit_value(element, symbol)
172317
args = [reference, *self._pins(symbol), value]
173-
args_str = self._str_args(args)
318+
args_str = self._str_args(args) # ty: ignore[invalid-argument-type]
174319
return f"circuit.{element}({args_str})"
175320

176321
##############################################
177322

178-
def on_generic_model(self, element, symbol):
323+
def on_generic_model(self, element: str, symbol: Symbol) -> str:
179324
self._logger.info(f"Element with model {symbol.reference} {symbol.simulation_pins} {symbol.simulation_paramaters}")
180325
# Fixme: check XD
181326
reference = symbol.reference[len(element):] # +1
@@ -185,28 +330,28 @@ def on_generic_model(self, element, symbol):
185330
case ('K', 'A'):
186331
pins = list(reversed(pins))
187332
args = [reference, *pins]
188-
args_str = self._str_args(args)
333+
args_str = self._str_args(args) # ty: ignore[invalid-argument-type]
189334
return f"circuit.{element}({args_str}, model='{symbol.value}')"
190335

191336
##############################################
192337

193-
def on_source(self, element, symbol):
338+
def on_source(self, element: str, symbol: Symbol) -> str:
194339
self._logger.info(f"Source {symbol.simulation_device} {symbol.simulation_type} {symbol.simulation_paramaters}")
195340
reference = symbol.reference[len(element):]
196341
match symbol.simulation_type.upper():
197342
case 'PULSE':
198343
element = 'PulseVoltageSource'
199344
args = [reference, *self._pins(symbol)]
200345
params = ''
201-
if symbol.simulation_paramaters is not None:
346+
if symbol.simulation_paramaters:
202347
sep = ', '
203348
params = sep.join(symbol.simulation_paramaters.split(' '))
204349
else:
205350
value = self._unit_value(element, symbol)
206351
if symbol.reference[0] in ('V',):
207352
value += '@u_V'
208353
args.append(value)
209-
args_str = self._str_args(args)
354+
args_str = self._str_args(args) # ty: ignore[invalid-argument-type]
210355
if params:
211356
params = sep + params
212357
return f"circuit.{element}({args_str}{params})"

examples/kicadrw/dump-netlist.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
####################################################################################################
1+
"""This example shows how to read a KiCAD schema in order to dump the netlist, and to convert it
2+
to Python PySpice code and to generate a Circuit_macros draft.
23
3-
import PySpice.Logging.Logging as Logging
4-
logger = Logging.setup_logging()
4+
"""
55

66
####################################################################################################
77

88
from pathlib import Path
99

10-
from KiCadRW.sexp.schema import KiCadSchema
11-
from KiCadRW.drawings.CircuitMacros import CircuitMacrosDumper
12-
from PySpice.KiCad import PythonDumper
10+
import PySpice.Logging.Logging as Logging
11+
from kicadrw.drawings.CircuitMacros import CircuitMacrosDumper
12+
from kicadrw.sexp.schema import KiCadSchema
13+
from PySpice.KiCad import PythonDumper, SpiceDumper
14+
15+
logger = Logging.setup_logging()
1316

1417
####################################################################################################
1518

@@ -23,17 +26,30 @@
2326
'charge-pump', 'charge-pump.kicad_sch'
2427
)
2528

29+
RULE = '─' * 100
30+
31+
#m# Read the schema and dump the netlist:
2632
kicad_schema = KiCadSchema(schema_path)
2733
print()
28-
print('='*100)
34+
print(RULE)
2935
kicad_schema.dump_netlist()
3036

37+
#m# Convert the netlist to PySpice Python code:
3138
print()
32-
print('='*100)
39+
print(RULE)
3340
python_code = PythonDumper(kicad_schema, use_pyspice_unit=True)
3441
print(python_code)
3542

3643
print()
37-
print('='*100)
44+
print(RULE)
45+
spice_circuit = SpiceDumper(kicad_schema)
46+
print(spice_circuit)
47+
48+
#m# Generate a Circuit_macros draft for this circuit.
49+
#m# Then you have to add manually the tracks.
50+
#m# Notice the KiCAD schema format is very simple. A track is only a bunch of segments.
51+
#m# As opposite Circuit_macros is much more sophisticated.
52+
print()
53+
print(RULE)
3854
cm_code = CircuitMacrosDumper(kicad_schema)
3955
print(cm_code)

0 commit comments

Comments
 (0)