spiceybun 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filip Hormot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: spiceybun
3
+ Version: 0.1.0
4
+ Summary: A python verification wrapper for SPICE.
5
+ Author: Filip Hormot
6
+ Author-email: Filip Hormot <f.hormot@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy>=2.4.5
10
+ Requires-Dist: pandas>=3.0.3
11
+ Requires-Python: >=3.14
12
+ Project-URL: Homepage, https://github.com/fhormot/spiceybun
13
+ Project-URL: Issues, https://github.com/fhormot/spiceybun/issues
14
+ Description-Content-Type: text/markdown
15
+
16
+ # SPICEybun
17
+
18
+ An NGSPICE wrapper for simplified verification.
19
+
20
+ # Usage
21
+
22
+ ```Python
23
+ import spiceybun
24
+
25
+ simulator = spiceybun.ngspice('./netlist.spice')
26
+
27
+ simulator.add_transient(1e-6)
28
+
29
+ simulator.save_signal('V(v_out)')
30
+ simulator.save_signal('V(v_in)')
31
+
32
+ simulator.run()
33
+
34
+ ```
35
+
36
+ # Documentation
37
+ TBD
38
+
39
+ # Long term roadmap
40
+
41
+ - [ ] Enabling basic functionality:
42
+ - [ ] All analysis statements
43
+ - [ ] Variables
44
+ - [x] Simple variables
45
+ - [ ] Equations
46
+ - [ ] Advanced variable features (distribution, limits, etc.)
47
+ - [ ] Measurements
48
+ - [x] Explicit spice input
49
+ - [x] Sweeps
50
+ - [x] Monte Carlo
51
+ - [ ] Advanced features
52
+ - [ ] Netlist from XSchem
53
+ - [ ] Simulator options
54
+ - [ ] Report generation
@@ -0,0 +1,39 @@
1
+ # SPICEybun
2
+
3
+ An NGSPICE wrapper for simplified verification.
4
+
5
+ # Usage
6
+
7
+ ```Python
8
+ import spiceybun
9
+
10
+ simulator = spiceybun.ngspice('./netlist.spice')
11
+
12
+ simulator.add_transient(1e-6)
13
+
14
+ simulator.save_signal('V(v_out)')
15
+ simulator.save_signal('V(v_in)')
16
+
17
+ simulator.run()
18
+
19
+ ```
20
+
21
+ # Documentation
22
+ TBD
23
+
24
+ # Long term roadmap
25
+
26
+ - [ ] Enabling basic functionality:
27
+ - [ ] All analysis statements
28
+ - [ ] Variables
29
+ - [x] Simple variables
30
+ - [ ] Equations
31
+ - [ ] Advanced variable features (distribution, limits, etc.)
32
+ - [ ] Measurements
33
+ - [x] Explicit spice input
34
+ - [x] Sweeps
35
+ - [x] Monte Carlo
36
+ - [ ] Advanced features
37
+ - [ ] Netlist from XSchem
38
+ - [ ] Simulator options
39
+ - [ ] Report generation
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "spiceybun"
3
+ version = "0.1.0"
4
+ authors = [
5
+ { name="Filip Hormot", email="f.hormot@gmail.com" },
6
+ ]
7
+ description = "A python verification wrapper for SPICE."
8
+ readme = "README.md"
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "numpy>=2.4.5",
12
+ "pandas>=3.0.3",
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICEN[CS]E*"]
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.11.14,<0.12"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=9.0.3",
24
+ ]
25
+
26
+ [tool.pytest.ini_options]
27
+ pythonpath = ["."]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/fhormot/spiceybun"
31
+ Issues = "https://github.com/fhormot/spiceybun/issues"
File without changes
@@ -0,0 +1,38 @@
1
+ import pandas as pd
2
+
3
+ class Measure_ngspice():
4
+ def __init__(self):
5
+ self._measure = []
6
+
7
+ def get_all(self) -> list:
8
+ return self._measure
9
+
10
+ def explicit(self, measure) -> str:
11
+ arguments = measure.split()
12
+
13
+ self._measure.append({
14
+ "name": arguments[2],
15
+ "analysis": arguments[1],
16
+ "measure": measure
17
+ })
18
+
19
+ return measure
20
+
21
+ def process_measure(self, path) -> object:
22
+ with open(path, 'r') as f:
23
+ header = f.readline().strip().split()
24
+ values = f.readline().strip().split()
25
+
26
+ # df = pd.read_csv(
27
+ # path,
28
+ # delimiter=' ',
29
+ # nrows=1,
30
+ # skipinitialspace=True
31
+ # )
32
+
33
+ # Extract header and first row
34
+ output = pd.DataFrame([values], columns=header)
35
+
36
+ output.to_csv(path, index=False, header=True)
37
+
38
+ return output
@@ -0,0 +1,340 @@
1
+ import enum
2
+ import subprocess
3
+ import os
4
+ import re
5
+ import pandas as pd
6
+ from pathlib import Path
7
+
8
+ from itertools import product
9
+
10
+ from spiceybun.measure_ngspice import Measure_ngspice
11
+ from spiceybun.variable import Variable
12
+
13
+ class Ngspice:
14
+ def __init__(self, path_netlist, **kwargs):
15
+ self._netlist = []
16
+
17
+ self._analysis = []
18
+ self._variables = []
19
+ self._plots = []
20
+
21
+ # Control statements
22
+ self._plot_all = False
23
+
24
+ self._path_netlist = path_netlist
25
+
26
+ self._output_path = Path(__file__).parent
27
+
28
+ self.measure = Measure_ngspice()
29
+
30
+ # Preparation during init
31
+ self._read_dut_nelist()
32
+
33
+ def _read_dut_variables(self) -> None:
34
+ variables = []
35
+
36
+ for line in self._netlist_dut:
37
+ result = re.findall(r'\{\w+\}', line)
38
+
39
+ if len(result) > 0:
40
+ variables.extend(result)
41
+
42
+ variables = list(set(variables))
43
+
44
+ for variable in variables:
45
+ name = variable.strip('{}')
46
+ self._variables.append(Variable(name))
47
+
48
+ def get_variables(self) -> list:
49
+ return self._variables
50
+
51
+ def set_variable(self, name, value) -> Variable | None:
52
+ for variable in self._variables:
53
+ if variable.get_name() == name:
54
+ variable.set_value(value)
55
+ return variable
56
+
57
+ return None
58
+
59
+ def _read_dut_nelist(self) -> None:
60
+ with open(self._path_netlist, 'r') as f:
61
+ self._netlist_dut = f.readlines()
62
+
63
+ # TODO: Strip existing control statements
64
+
65
+ # Extract variables
66
+ self._read_dut_variables()
67
+
68
+ def _include(self, path, **kwargs) -> str:
69
+ # str = f'.include "{path}"'
70
+ str = f'.include {path}'
71
+
72
+ if 'section' in kwargs:
73
+ str = str + ' ' + kwargs['section']
74
+
75
+ self._netlist.append(str)
76
+
77
+ return str
78
+
79
+ def _write_netlist_dut(self, **kwargs) -> str:
80
+ subfolder = kwargs.get('id', '')
81
+
82
+ output_path = os.path.join(self._output_path, subfolder)
83
+ output_netlist = os.path.join(output_path, 'netlist.spice')
84
+
85
+ with open(output_netlist, 'w') as f:
86
+ f.writelines(self._netlist_dut)
87
+ os.chmod(output_netlist, 0o755)
88
+
89
+ return output_netlist
90
+
91
+ def _write_netlist(self, **kwargs) -> str:
92
+ subfolder = kwargs.get('id', '')
93
+
94
+ self._add_control(**kwargs)
95
+
96
+ self._netlist.append('.end')
97
+
98
+ output_path = os.path.join(self._output_path, subfolder)
99
+ output_netlist = os.path.join(output_path, 'tb_test.spice')
100
+
101
+ with open(output_netlist, 'w') as f:
102
+ f.write('\n'.join(self._netlist))
103
+ os.chmod(output_netlist, 0o755)
104
+
105
+ self._netlist_output = output_netlist
106
+
107
+ return output_netlist
108
+
109
+ def _write_run_command(self, **kwargs) -> str:
110
+ subfolder = kwargs.get('id', '')
111
+
112
+ folder_path = os.path.join(self._output_path, subfolder)
113
+
114
+ path_output_netlist = self._netlist_output
115
+
116
+ command_format = "#!/bin/bash\nngspice -i -o {output_path} {input_path} -a || sh"
117
+
118
+ command_path = os.path.join(folder_path, 'run_command')
119
+ output_path = os.path.join(folder_path, 'output.log')
120
+ input_path = path_output_netlist
121
+
122
+ netlist_command = command_format.format(
123
+ output_path=output_path,
124
+ input_path=input_path
125
+ )
126
+
127
+ with open(command_path, 'w') as f:
128
+ f.write(netlist_command)
129
+ os.chmod(command_path, 0o755)
130
+
131
+ return command_path
132
+
133
+ def _add_control(self, **kwargs) -> list:
134
+ # Kwargs
135
+ mc = kwargs.get('mc', False)
136
+
137
+ control_statement = []
138
+ control_statement.append('\n* Control statements added by the tool')
139
+
140
+ # Parameter definitions
141
+ variables = kwargs.get('variables', self._variables)
142
+ for variable in variables:
143
+ control_statement.append(variable.get_value_definition())
144
+
145
+ if mc:
146
+ seed = kwargs.get('seed', 1)
147
+ mc_runs = kwargs.get('mc_runs', 350)
148
+
149
+ # Start of control statements
150
+ control_statement.append('\n.control')
151
+
152
+ # Section
153
+ # Measurement file preparation
154
+ control_statement.extend(self._netlist_define_measurement_setup())
155
+
156
+ # Section Monte Carlo
157
+ if mc:
158
+ control_statement.append('\n\t* Monte Carlo analysis')
159
+ control_statement.append(f'\tsetseed {seed}')
160
+ control_statement.append(f'\tlet mc_runs={mc_runs}')
161
+ control_statement.append('\tlet mc_index=0')
162
+
163
+ control_statement.append('\n\twhile mc_index < mc_runs')
164
+
165
+ # Section
166
+ # Append analysis
167
+ for element in self._analysis:
168
+ control_statement.append(f'\t\t{element}')
169
+
170
+ suffix = element.split()[0]
171
+
172
+ # Section
173
+ # Save statements
174
+ # TODO: Wrap measurements and save statments with their respective analysis
175
+ control_statement.extend(self._netlist_define_plot(**kwargs))
176
+
177
+ # Measureement definition and write to file
178
+ control_statement.extend(self._netlist_define_measurement_write())
179
+
180
+ if mc:
181
+ control_statement.append('\n\t\tlet mc_index = mc_index + 1')
182
+ control_statement.append('\t\treset')
183
+
184
+ control_statement.append('\n\t* End of Monte Carlo iteration')
185
+ control_statement.append('\tend')
186
+
187
+ control_statement.append('\n\texit')
188
+ control_statement.append('.endc\n')
189
+
190
+ self._netlist.extend(control_statement)
191
+ return control_statement
192
+
193
+ def _netlist_define_plot(self, **kwargs) -> list:
194
+ control_statement = []
195
+
196
+ subfolder = kwargs.get('id', '')
197
+ output_path = os.path.join(self._output_path, subfolder, 'output.raw')
198
+
199
+ # Keep vector names in the header
200
+ control_statement.append('\n\t\tset wr_vecnames')
201
+
202
+ # Use a single scale (column) for all signals
203
+ control_statement.append('\t\tset wr_singlescale')
204
+
205
+ if not self._plot_all:
206
+ control_statement.append(f'\t\twrdata {output_path} {' '.join(self._plots)}')
207
+ else:
208
+ control_statement.append('\t\tsave all')
209
+ control_statement.append(f'\t\twrdata {output_path} all')
210
+
211
+ return control_statement
212
+
213
+ def _netlist_define_measurement_setup(self) -> list:
214
+ control_statement = []
215
+
216
+ measurements = self.measure.get_all()
217
+ if len(measurements)>0:
218
+ control_statement.append('\t*Prepare measurement output file with a header')
219
+ measurement_list = ' '.join([measure['name'] for measure in measurements])
220
+ control_statement.append(f'\techo "{measurement_list}" > {os.path.join(self._output_path, "results", "measurements.raw\n")}')
221
+
222
+ return control_statement
223
+
224
+ def _netlist_define_measurement_write(self) -> list:
225
+ control_statement = []
226
+
227
+ control_statement.append('\n\t\t* Measurements')
228
+
229
+ measurements = self.measure.get_all()
230
+ for measure in measurements:
231
+ control_statement.append(f'\t\t{measure["measure"]}')
232
+
233
+ if len(measurements) > 0:
234
+ # control_statement.append('\n\t\tset filetype=ascii')
235
+ # control_statement.append('\t\tset nopadding')
236
+
237
+ # meas_string_list = ' '.join([measure['name'] for measure in measurements])
238
+ # control_statement.append(f'\twrdata {os.path.join(self._output_path, "measurement.raw")} {meas_string_list}')
239
+
240
+ control_statement.append('\n\t\t* Measurement output in separate files')
241
+ measurement_list = ' '.join([f'$&{measure['name']}' for measure in measurements])
242
+ control_statement.append(f'\t\techo "{measurement_list}" >> {os.path.join(self._output_path, "results", "measurements.raw")}')
243
+
244
+ return control_statement
245
+
246
+ def _run_single_run(self, **kwargs) -> str:
247
+ subfolder = kwargs.get('id', '')
248
+
249
+ output_path = os.path.join(self._output_path, subfolder)
250
+
251
+ # Verify the output folder exists
252
+ if not os.path.exists(output_path):
253
+ os.makedirs(output_path)
254
+
255
+ # Verify that the results folder is available
256
+ if not os.path.exists(os.path.join(output_path, "results")):
257
+ os.makedirs(os.path.join(output_path, "results"))
258
+
259
+ path_input_netlist = self._write_netlist_dut(**kwargs)
260
+
261
+ self._include(path_input_netlist)
262
+ path_output_netlist = self._write_netlist(**kwargs)
263
+
264
+ command_path = self._write_run_command(**kwargs)
265
+
266
+ output = subprocess.run(
267
+ command_path,
268
+ # env=self.env,
269
+ shell=True,
270
+ capture_output=True,
271
+ text=True
272
+ )
273
+
274
+ run_path = os.path.join(output_path, 'run.log')
275
+
276
+ with open(run_path, 'w') as f:
277
+ f.write(output.stdout)
278
+ os.chmod(command_path, 0o755)
279
+
280
+ # self.measure.process_measure(os.path.join(self._output_path, 'measurement.raw'))
281
+
282
+ return output.stdout
283
+
284
+ def _run_sweep(self, sweep_list, **kwargs) -> list:
285
+ results = []
286
+
287
+ for idx, run in enumerate(sweep_list):
288
+ self._netlist = []
289
+ results.append(self._run_single_run(variables=run, id=f'{idx}', **kwargs))
290
+
291
+ return results
292
+
293
+ def add_transient(self, t_stop, **kwargs) -> str:
294
+ # Overwrite previous transient statement if it exists
295
+ self._analysis = [x for x in self._analysis if not x.startswith("tran")]
296
+
297
+ # TODO: Calculate suggested/maximum steps
298
+ # Step 1: calculate based on stop time --> Bad for long sims with sharp transients
299
+ # Step 2: Find/assume fastest signal in netlist and adjust t_step accordingly
300
+ # --> Not accounting for fast digital signals
301
+ # TODO: Define transient statement using variables
302
+ t_step = kwargs.get('t_step', 1e-9)
303
+ t_start = kwargs.get('t_start', '0')
304
+ t_max = kwargs.get('t_max', t_step*10)
305
+
306
+ transient_statement = f'tran {t_step} {t_stop} {t_start} {t_max}'
307
+
308
+ self._analysis.append(transient_statement)
309
+
310
+ return transient_statement
311
+
312
+ def save_signal(self, signals) -> list:
313
+ #TODO: Check if valid net/port
314
+ if type(signals) is list:
315
+ self._plots.extend(signals)
316
+ else:
317
+ self._plots.append(signals)
318
+
319
+ return self._plots
320
+
321
+ def save_signal_all(self, flag) -> bool:
322
+ self._plot_all = flag
323
+
324
+ return self._plot_all
325
+
326
+ def set_output_path(self, path) -> None:
327
+ self._output_path = path
328
+
329
+ def run(self, **kwargs) -> str | list:
330
+ if len(self._variables) == 0:
331
+ return self._run_single_run(**kwargs)
332
+
333
+ #Create all variation combinations
334
+ permutation_pre_list = [variable.get_split() for variable in self._variables]
335
+ permutations = list(product(*permutation_pre_list))
336
+
337
+ if len(permutations) == 1:
338
+ return self._run_single_run(**kwargs)
339
+
340
+ return self._run_sweep(permutations, **kwargs)
@@ -0,0 +1,19 @@
1
+
2
+ * Control statements added by the tool
3
+ .param t_edge=
4
+ .param c_load=
5
+ .param v_vdd=
6
+
7
+ .control
8
+ tran 1e-11 500e-9 0 100e-12
9
+
10
+ set wr_vecnames
11
+ set wr_singlescale
12
+ wrdata /home/fhormot/wk/xschem/spipy/src/spipy/output.raw
13
+
14
+ * Measurements
15
+
16
+ exit
17
+ .endc
18
+
19
+ .end
@@ -0,0 +1,32 @@
1
+ import copy
2
+
3
+ class Variable:
4
+ def __init__(self, name: str):
5
+ self._name = name
6
+ self._value = ''
7
+
8
+ def get_name(self) -> str:
9
+ return self._name
10
+
11
+ def get_value(self) -> str:
12
+ return self._value
13
+
14
+ def get_value_definition(self) -> str:
15
+ return f'.param {self._name}={self._value}'
16
+
17
+ def set_value(self, value: str) -> None:
18
+ self._value = value
19
+
20
+ def get_split(self) -> list:
21
+ if not isinstance(self._value, list):
22
+ return [self]
23
+
24
+ return_list = []
25
+
26
+ for idx, variable in enumerate(self.get_value()):
27
+ object_copy = copy.deepcopy(self)
28
+
29
+ object_copy.set_value(variable)
30
+ return_list.append(object_copy)
31
+
32
+ return return_list