cicsim 0.2.2__tar.gz → 0.2.7__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.
- {cicsim-0.2.2 → cicsim-0.2.7}/PKG-INFO +3 -1
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cicsim.py +20 -4
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdresults.py +95 -33
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdrunng.py +135 -57
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdsummary.py +1 -2
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdwave.py +3 -1
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdwave_pg.py +431 -98
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/command.py +17 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/ngraw.py +3 -5
- cicsim-0.2.7/cicsim/theme.py +108 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/wavebrowser.py +4 -1
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/wavefiles.py +13 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/wavegraph.py +20 -10
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/PKG-INFO +3 -1
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/SOURCES.txt +1 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/requires.txt +2 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/pyproject.toml +3 -1
- {cicsim-0.2.2 → cicsim-0.2.7}/LICENSE +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/README.md +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/__init__.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cdsconf.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdarchive.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdip.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdrun.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdsimdir.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdsimdirng.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdspider.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/pivot.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/plot.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/simcalc.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/spec.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/spiceparser.py +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/dependency_links.txt +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/entry_points.txt +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/top_level.txt +0 -0
- {cicsim-0.2.2 → cicsim-0.2.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cicsim
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Custom IC Creator Simulation Tools
|
|
5
5
|
Author-email: Carsten Wulff <carsten@wulff.no>
|
|
6
6
|
License: MIT
|
|
@@ -25,6 +25,8 @@ Requires-Dist: matplotlib
|
|
|
25
25
|
Requires-Dist: click
|
|
26
26
|
Requires-Dist: pyyaml
|
|
27
27
|
Requires-Dist: tabulate
|
|
28
|
+
Requires-Dist: rich
|
|
29
|
+
Requires-Dist: tqdm
|
|
28
30
|
Provides-Extra: pg
|
|
29
31
|
Requires-Dist: PySide6; extra == "pg"
|
|
30
32
|
Requires-Dist: pyqtgraph; extra == "pg"
|
|
@@ -67,12 +67,15 @@ def cli(ctx,color):
|
|
|
67
67
|
@click.option("--ignore/--no-ignore", default=False,is_flag=True, help="Ignore error checks")
|
|
68
68
|
@click.option("--sha/--no-sha", default=None, help="Check SHA of input files")
|
|
69
69
|
@click.option("--replace",default=None, help="YAML file with replacements for netlist")
|
|
70
|
+
@click.option("--threads", default=1, help="Number of parallel simulation threads")
|
|
71
|
+
@click.option("--progress/--no-progress", default=False, help="Show progress bar instead of interleaved logs")
|
|
72
|
+
@click.option("--timeout", default=None, type=int, help="Timeout in seconds per simulation (default: no limit)")
|
|
70
73
|
@click.pass_context
|
|
71
|
-
def run(ctx,testbench,run,corner,count,name,ignore,sha,replace):
|
|
74
|
+
def run(ctx,testbench,run,corner,count,name,ignore,sha,replace,threads,progress,timeout):
|
|
72
75
|
"""Run a ngspice simulation of TESTBENCH
|
|
73
76
|
"""
|
|
74
77
|
|
|
75
|
-
r = cs.CmdRunNg(testbench,run,corner,name,count,sha)
|
|
78
|
+
r = cs.CmdRunNg(testbench,run,corner,name,count,sha,threads=threads,progress=progress,timeout=timeout)
|
|
76
79
|
r.loadReplacements(replace)
|
|
77
80
|
|
|
78
81
|
r.run(ignore)
|
|
@@ -91,7 +94,8 @@ def archive(ctx,name,runfiles):
|
|
|
91
94
|
|
|
92
95
|
@cli.command()
|
|
93
96
|
@click.argument("files",nargs=-1)
|
|
94
|
-
@click.option("--x",default=None,
|
|
97
|
+
@click.option("--x", default=None,
|
|
98
|
+
help="X-axis column; else CICWAVE_X / CICSIM_X_AXIS; else saved default (pg); else auto")
|
|
95
99
|
@click.option("--backend",default="tk",type=click.Choice(["tk","pg"]),
|
|
96
100
|
help="GUI backend: tk (tkinter+matplotlib) or pg (PySide6+pyqtgraph)")
|
|
97
101
|
@click.option("--sheet",default=None,help="Sheet name for Excel files (default: first sheet)")
|
|
@@ -163,8 +167,19 @@ def wave(files,x,backend,sheet,pivot,pivot_info,session,export):
|
|
|
163
167
|
session_path=session, export_path=export)
|
|
164
168
|
|
|
165
169
|
|
|
170
|
+
def _resolve_wave_x_from_cli_env(cli_x):
|
|
171
|
+
"""Apply --x, else CICWAVE_X / CICSIM_X_AXIS environment variables."""
|
|
172
|
+
if cli_x:
|
|
173
|
+
return cli_x
|
|
174
|
+
v = os.environ.get("CICWAVE_X") or os.environ.get("CICSIM_X_AXIS")
|
|
175
|
+
if v and str(v).strip():
|
|
176
|
+
return str(v).strip()
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
166
180
|
def _run_wave(files, x, backend, sheet, pivot_spec=None,
|
|
167
181
|
pivot_info_flag=False, session_path=None, export_path=None):
|
|
182
|
+
x = _resolve_wave_x_from_cli_env(x)
|
|
168
183
|
if pivot_spec:
|
|
169
184
|
from cicsim.pivot import load_spec, pivot_info, apply_pivot
|
|
170
185
|
from cicsim.wavefiles import WaveFile
|
|
@@ -224,7 +239,8 @@ def _run_wave(files, x, backend, sheet, pivot_spec=None,
|
|
|
224
239
|
|
|
225
240
|
@click.command()
|
|
226
241
|
@click.argument("files", nargs=-1)
|
|
227
|
-
@click.option("--x", default=None,
|
|
242
|
+
@click.option("--x", default=None,
|
|
243
|
+
help="X-axis column; else CICWAVE_X / CICSIM_X_AXIS; else saved default; else auto")
|
|
228
244
|
@click.option("--backend", default="pg", type=click.Choice(["tk", "pg"]),
|
|
229
245
|
help="GUI backend (default: pg)")
|
|
230
246
|
@click.option("--sheet", default=None, help="Sheet name for Excel files (default: first sheet)")
|
|
@@ -35,6 +35,10 @@ import glob
|
|
|
35
35
|
import pandas as pd
|
|
36
36
|
import numpy as np
|
|
37
37
|
import time
|
|
38
|
+
from rich.console import Console
|
|
39
|
+
from rich.table import Table
|
|
40
|
+
from rich import box
|
|
41
|
+
from rich.markup import escape
|
|
38
42
|
|
|
39
43
|
logger = logging.getLogger("cicsim")
|
|
40
44
|
|
|
@@ -43,7 +47,7 @@ logger = logging.getLogger("cicsim")
|
|
|
43
47
|
class CmdResults(cs.Command):
|
|
44
48
|
""" Summarize results of TESTBENCH """
|
|
45
49
|
|
|
46
|
-
def __init__(self,runfile,runname=None):
|
|
50
|
+
def __init__(self,runfile,runname=None,progress=False):
|
|
47
51
|
if(runname is None):
|
|
48
52
|
self.runname = runfile.replace(".run","")
|
|
49
53
|
else:
|
|
@@ -51,6 +55,7 @@ class CmdResults(cs.Command):
|
|
|
51
55
|
self.runfile = runfile
|
|
52
56
|
self.ofile = runfile.replace(".run","")
|
|
53
57
|
self.testbench = re.sub("_.*","",runfile)
|
|
58
|
+
self.progress = progress
|
|
54
59
|
super().__init__()
|
|
55
60
|
|
|
56
61
|
def summaryToMarkdown(self,specs,df_all):
|
|
@@ -106,30 +111,6 @@ class CmdResults(cs.Command):
|
|
|
106
111
|
text_file.write(self.footer)
|
|
107
112
|
|
|
108
113
|
|
|
109
|
-
def readCsv(self):
|
|
110
|
-
print("Not updated")
|
|
111
|
-
pass
|
|
112
|
-
files = glob.glob(f"output_{self.testbench}/{self.testbench}_*.csv")
|
|
113
|
-
df_all = pd.DataFrame()
|
|
114
|
-
for f in files:
|
|
115
|
-
df = pd.read_csv(f)
|
|
116
|
-
name = os.path.basename(f).replace(".*_","").replace(".csv","")
|
|
117
|
-
df["name"] = name
|
|
118
|
-
(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(f)
|
|
119
|
-
df["time"] = time.ctime(mtime)
|
|
120
|
-
m = re.search("([A-Z]+[a-z]+)[A-Z]",name)
|
|
121
|
-
if(m):
|
|
122
|
-
df["type"] = m.group(1)
|
|
123
|
-
df_all = pd.concat([df,df_all])
|
|
124
|
-
|
|
125
|
-
if(df_all.empty):
|
|
126
|
-
return None
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
#- Print each corner
|
|
130
|
-
df_all.drop(columns=["Unnamed: 0"],inplace=True)
|
|
131
|
-
return df_all
|
|
132
|
-
|
|
133
114
|
def readYaml(self):
|
|
134
115
|
files = list()
|
|
135
116
|
with open(self.runfile) as fi:
|
|
@@ -137,31 +118,112 @@ class CmdResults(cs.Command):
|
|
|
137
118
|
l = re.sub("\n","",l)
|
|
138
119
|
files.append(l + ".yaml")
|
|
139
120
|
|
|
140
|
-
|
|
121
|
+
frames = []
|
|
141
122
|
for f in files:
|
|
142
123
|
with open(f) as yaml_file:
|
|
143
124
|
yaml_contents = yaml.safe_load(yaml_file)
|
|
144
125
|
df = pd.json_normalize(yaml_contents)
|
|
145
|
-
name =
|
|
126
|
+
name = re.sub(r".*_", "", os.path.basename(f)).replace(".yaml","")
|
|
146
127
|
df["name"] = name
|
|
147
128
|
(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(f)
|
|
148
129
|
df["time"] = time.ctime(mtime)
|
|
149
130
|
m = re.search("([A-Z]+[a-z]+)[A-Z]",name)
|
|
150
131
|
if(m):
|
|
151
132
|
df["type"] = m.group(1)
|
|
152
|
-
|
|
133
|
+
frames.append(df)
|
|
153
134
|
|
|
135
|
+
if not frames:
|
|
136
|
+
return None
|
|
154
137
|
|
|
155
|
-
|
|
138
|
+
df_all = pd.concat(frames, ignore_index=True)
|
|
156
139
|
|
|
140
|
+
if df_all.empty:
|
|
157
141
|
return None
|
|
158
142
|
|
|
159
|
-
|
|
160
143
|
return df_all
|
|
161
144
|
|
|
162
145
|
|
|
163
146
|
|
|
164
147
|
|
|
148
|
+
def printRich(self, df, specs=None):
|
|
149
|
+
console = Console()
|
|
150
|
+
str_cols = {"name", "time", "type"}
|
|
151
|
+
key_cols = [c for c in ["name", "type"] if c in df.columns]
|
|
152
|
+
data_cols = [c for c in df.columns if c not in key_cols]
|
|
153
|
+
|
|
154
|
+
def spec_for(col):
|
|
155
|
+
return specs[col] if specs and col in specs else None
|
|
156
|
+
|
|
157
|
+
def display_val(col, raw):
|
|
158
|
+
"""Return (scaled_float, formatted_string) for a data cell."""
|
|
159
|
+
sp = spec_for(col)
|
|
160
|
+
v = raw * sp.scale if sp and sp.scale != 1 else raw
|
|
161
|
+
return v, f"{v:.4g}"
|
|
162
|
+
|
|
163
|
+
def col_header(col):
|
|
164
|
+
sp = spec_for(col)
|
|
165
|
+
if sp and sp.unit:
|
|
166
|
+
return f"{col} {escape(f'[{sp.unit}]')}"
|
|
167
|
+
return col
|
|
168
|
+
|
|
169
|
+
def col_width(col):
|
|
170
|
+
"""Estimate rendered column width including cell padding."""
|
|
171
|
+
sp = spec_for(col)
|
|
172
|
+
scale = sp.scale if sp else 1
|
|
173
|
+
header_w = len(col_header(col))
|
|
174
|
+
if col in str_cols:
|
|
175
|
+
val_w = df[col].astype(str).str.len().max() if len(df) else 0
|
|
176
|
+
else:
|
|
177
|
+
scaled = df[col].dropna() * scale
|
|
178
|
+
val_w = int(scaled.apply(lambda v: len(f"{v:.4g}")).max()) if len(scaled) else 0
|
|
179
|
+
return max(header_w, val_w) + 2 # +2 for cell padding
|
|
180
|
+
|
|
181
|
+
key_width = sum(col_width(c) + 1 for c in key_cols)
|
|
182
|
+
available = console.width - key_width
|
|
183
|
+
|
|
184
|
+
# Group data columns into chunks that fit the terminal width
|
|
185
|
+
chunks, chunk, chunk_w = [], [], 0
|
|
186
|
+
for col in data_cols:
|
|
187
|
+
w = col_width(col) + 1
|
|
188
|
+
if chunk and chunk_w + w > available:
|
|
189
|
+
chunks.append(chunk)
|
|
190
|
+
chunk, chunk_w = [col], w
|
|
191
|
+
else:
|
|
192
|
+
chunk.append(col)
|
|
193
|
+
chunk_w += w
|
|
194
|
+
if chunk:
|
|
195
|
+
chunks.append(chunk)
|
|
196
|
+
|
|
197
|
+
def make_table(cols):
|
|
198
|
+
t = Table(box=box.SIMPLE_HEAD, show_footer=False, highlight=True)
|
|
199
|
+
for col in cols:
|
|
200
|
+
if col in str_cols:
|
|
201
|
+
t.add_column(col, style="cyan", no_wrap=True)
|
|
202
|
+
else:
|
|
203
|
+
t.add_column(col_header(col), justify="right")
|
|
204
|
+
for _, row in df.iterrows():
|
|
205
|
+
cells = []
|
|
206
|
+
for col in cols:
|
|
207
|
+
val = row[col]
|
|
208
|
+
if col in str_cols:
|
|
209
|
+
cells.append(str(val))
|
|
210
|
+
elif pd.isna(val):
|
|
211
|
+
cells.append("")
|
|
212
|
+
else:
|
|
213
|
+
scaled, formatted = display_val(col, val)
|
|
214
|
+
sp = spec_for(col)
|
|
215
|
+
if sp:
|
|
216
|
+
if sp.OK(scaled):
|
|
217
|
+
formatted = f"[green]{formatted}[/green]"
|
|
218
|
+
else:
|
|
219
|
+
formatted = f"[bold red]{formatted}[/bold red]"
|
|
220
|
+
cells.append(formatted)
|
|
221
|
+
t.add_row(*cells)
|
|
222
|
+
return t
|
|
223
|
+
|
|
224
|
+
for chunk in chunks:
|
|
225
|
+
console.print(make_table(key_cols + chunk))
|
|
226
|
+
|
|
165
227
|
def run(self):
|
|
166
228
|
|
|
167
229
|
df_all = self.readYaml()
|
|
@@ -169,11 +231,11 @@ class CmdResults(cs.Command):
|
|
|
169
231
|
logger.warning("No results found in yaml file")
|
|
170
232
|
return
|
|
171
233
|
|
|
172
|
-
#-Print all results
|
|
173
|
-
print(df_all.to_markdown(index=False,tablefmt="github",floatfmt=".5g"))
|
|
174
|
-
|
|
175
234
|
specs = cs.Specification(self.testbench)
|
|
176
235
|
|
|
236
|
+
#-Print all results
|
|
237
|
+
self.printRich(df_all, specs if len(specs) > 0 else None)
|
|
238
|
+
|
|
177
239
|
if(len(specs) > 0):
|
|
178
240
|
df = df_all[specs.sources + ["name","type","time"]]
|
|
179
241
|
df.reset_index(inplace=True)
|
|
@@ -36,13 +36,15 @@ import datetime
|
|
|
36
36
|
import hashlib
|
|
37
37
|
import inspect
|
|
38
38
|
import subprocess
|
|
39
|
+
import signal
|
|
39
40
|
import numpy as np
|
|
41
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
40
42
|
|
|
41
43
|
logger = logging.getLogger("cicsim")
|
|
42
44
|
|
|
43
45
|
class Simulation(cs.CdsConfig):
|
|
44
46
|
|
|
45
|
-
def __init__(self,testbench,corner,runsim,config,index,sha=None):
|
|
47
|
+
def __init__(self,testbench,corner,runsim,config,index,sha=None,progress=False,timeout=None):
|
|
46
48
|
#- Permutation variables
|
|
47
49
|
self.runsim = runsim
|
|
48
50
|
self.runmeas = True
|
|
@@ -52,6 +54,8 @@ class Simulation(cs.CdsConfig):
|
|
|
52
54
|
self.pyscript = testbench + ".py"
|
|
53
55
|
self.rundir = f"output_{self.testbench}"
|
|
54
56
|
self.sha = sha
|
|
57
|
+
self.progress = progress
|
|
58
|
+
self.timeout = timeout
|
|
55
59
|
|
|
56
60
|
#- Store sha128 checksums for files
|
|
57
61
|
self.shas = dict()
|
|
@@ -94,7 +98,8 @@ class Simulation(cs.CdsConfig):
|
|
|
94
98
|
with open(fpath,"rb") as f:
|
|
95
99
|
self.shas[filename] = hashlib.sha256(f.read()).hexdigest()
|
|
96
100
|
else:
|
|
97
|
-
|
|
101
|
+
if not self.progress:
|
|
102
|
+
logger.warning(f"Could not find referenced file {fpath}")
|
|
98
103
|
|
|
99
104
|
def loadSha(self):
|
|
100
105
|
shafile = self.oname + ".sha"
|
|
@@ -143,7 +148,8 @@ class Simulation(cs.CdsConfig):
|
|
|
143
148
|
|
|
144
149
|
#- Maybe run sim if no input files have changed
|
|
145
150
|
if(self.sha and self.matchAllSha()):
|
|
146
|
-
|
|
151
|
+
if not self.progress:
|
|
152
|
+
logger.warning("No spice files have changed")
|
|
147
153
|
self.runsim = False
|
|
148
154
|
|
|
149
155
|
#- Run simulation, or not, depends on runsim
|
|
@@ -157,7 +163,8 @@ class Simulation(cs.CdsConfig):
|
|
|
157
163
|
|
|
158
164
|
#- Run measurement if it exists, check sha though
|
|
159
165
|
if(self.sha and not self.runsim and self.matchSha(self.name + ".meas")):
|
|
160
|
-
|
|
166
|
+
if not self.progress:
|
|
167
|
+
logger.warning("No meas files have changed")
|
|
161
168
|
self.runmeas = False
|
|
162
169
|
measOk = self.ngspiceMeas(ignore)
|
|
163
170
|
|
|
@@ -167,7 +174,8 @@ class Simulation(cs.CdsConfig):
|
|
|
167
174
|
if(self.runsim):
|
|
168
175
|
self.saveSha()
|
|
169
176
|
else:
|
|
170
|
-
|
|
177
|
+
if not self.progress:
|
|
178
|
+
logger.warning("Not storing sha file, no simulation run")
|
|
171
179
|
|
|
172
180
|
if(simOk and measOk):
|
|
173
181
|
self.parseLog()
|
|
@@ -181,6 +189,7 @@ class Simulation(cs.CdsConfig):
|
|
|
181
189
|
|
|
182
190
|
def ngspice(self,ignore=True):
|
|
183
191
|
simOk = True
|
|
192
|
+
self.err = 0
|
|
184
193
|
|
|
185
194
|
if(self.runsim):
|
|
186
195
|
tickTime = datetime.datetime.now()
|
|
@@ -202,15 +211,22 @@ class Simulation(cs.CdsConfig):
|
|
|
202
211
|
self.removeFile(self.oname + ".yaml")
|
|
203
212
|
|
|
204
213
|
# Run NGSPICE
|
|
205
|
-
|
|
214
|
+
if self.progress:
|
|
215
|
+
cmd = f"cd {self.rundir}; ngspice {options} {includes} {self.name}.spi -r {self.name}.raw > {self.name}.log 2>&1"
|
|
216
|
+
else:
|
|
217
|
+
cmd = f"cd {self.rundir}; ngspice {options} {includes} {self.name}.spi -r {self.name}.raw 2>&1 |tee {self.name}.log"
|
|
206
218
|
logger.debug("Cmd: " + cmd)
|
|
207
219
|
try:
|
|
208
|
-
result = subprocess.run(cmd, shell=True)
|
|
220
|
+
result = subprocess.run(cmd, shell=True, timeout=self.timeout)
|
|
209
221
|
self.err = result.returncode
|
|
222
|
+
except subprocess.TimeoutExpired:
|
|
223
|
+
logger.error(f"Simulation {self.name} timed out after {self.timeout}s")
|
|
224
|
+
self.err = 1
|
|
210
225
|
except KeyboardInterrupt:
|
|
211
226
|
sys.exit(2)
|
|
212
227
|
except Exception as e:
|
|
213
228
|
logger.error(str(e))
|
|
229
|
+
self.err = 1
|
|
214
230
|
|
|
215
231
|
if(self.err > 0):
|
|
216
232
|
simOk = False
|
|
@@ -219,7 +235,8 @@ class Simulation(cs.CdsConfig):
|
|
|
219
235
|
logger.info("Corner simulation time: " + str(nextTime - tickTime))
|
|
220
236
|
tickTime = nextTime
|
|
221
237
|
else:
|
|
222
|
-
|
|
238
|
+
if not self.progress:
|
|
239
|
+
logger.warning(f"Skipping simulation of {self.name}.spi")
|
|
223
240
|
|
|
224
241
|
#- Check logfile. ngspice does not always exit cleanly
|
|
225
242
|
errors = list()
|
|
@@ -260,6 +277,8 @@ class Simulation(cs.CdsConfig):
|
|
|
260
277
|
for l in fi:
|
|
261
278
|
if(l.startswith("#ifdef")):
|
|
262
279
|
d = re.split(r"\s+",l)
|
|
280
|
+
if len(d) < 2:
|
|
281
|
+
continue
|
|
263
282
|
dkey = d[1]
|
|
264
283
|
state = 1
|
|
265
284
|
continue
|
|
@@ -369,11 +388,17 @@ class Simulation(cs.CdsConfig):
|
|
|
369
388
|
|
|
370
389
|
#- Run measurement
|
|
371
390
|
if(self.runmeas):
|
|
372
|
-
|
|
391
|
+
if self.progress:
|
|
392
|
+
cmd = f"cd {self.rundir}; ngspice -b {self.name}.meas > {self.name}.logm 2>&1"
|
|
393
|
+
else:
|
|
394
|
+
cmd = f"cd {self.rundir}; ngspice -b {self.name}.meas 2>&1 | tee {self.name}.logm"
|
|
373
395
|
logger.debug("Cmd: " + cmd)
|
|
374
|
-
subprocess.run(cmd, shell=True)
|
|
396
|
+
result = subprocess.run(cmd, shell=True)
|
|
397
|
+
if result.returncode != 0:
|
|
398
|
+
logger.warning(f"ngspice measurement run returned {result.returncode} for {self.name}")
|
|
375
399
|
else:
|
|
376
|
-
|
|
400
|
+
if not self.progress:
|
|
401
|
+
logger.warning(f"Skipping measurement run of {self.name}.meas")
|
|
377
402
|
|
|
378
403
|
#- Check meas logfile. ngspice does not always exit cleanly
|
|
379
404
|
errors = list()
|
|
@@ -399,21 +424,34 @@ class Simulation(cs.CdsConfig):
|
|
|
399
424
|
def parseLog(self):
|
|
400
425
|
analysis = False
|
|
401
426
|
|
|
427
|
+
re_eq = re.compile(r"=")
|
|
428
|
+
re_kv = re.compile(r"^([^\s]+)\s*=\s*([^\s]+)\s")
|
|
429
|
+
re_binary = re.compile(r"binary raw file")
|
|
430
|
+
re_meas = re.compile(r"Measurements for (.*)$")
|
|
431
|
+
re_sep = re.compile(r"^---------------------------------")
|
|
432
|
+
re_index = re.compile(r"^Index")
|
|
433
|
+
re_digit = re.compile(r"^\d")
|
|
434
|
+
re_start = re.compile(r"^\s*MEAS_START")
|
|
435
|
+
re_end = re.compile(r"^\s*MEAS_END")
|
|
436
|
+
|
|
402
437
|
#- Parse main log file for measurements
|
|
403
438
|
data = dict()
|
|
404
439
|
with open(self.oname + ".log") as fi:
|
|
405
440
|
for l in fi:
|
|
406
|
-
if(analysis and
|
|
407
|
-
m =
|
|
441
|
+
if(analysis and re_eq.search(l)):
|
|
442
|
+
m = re_kv.search(l)
|
|
408
443
|
if(m):
|
|
409
444
|
key = m.groups()[0].strip()
|
|
410
445
|
val = m.groups()[1].strip()
|
|
411
|
-
|
|
446
|
+
try:
|
|
447
|
+
data[key] = float(val)
|
|
448
|
+
except ValueError:
|
|
449
|
+
logger.warning(f"Could not parse measurement value: {key}={val}")
|
|
412
450
|
|
|
413
|
-
if(
|
|
451
|
+
if(re_binary.search(l)):
|
|
414
452
|
analysis = False
|
|
415
453
|
|
|
416
|
-
m =
|
|
454
|
+
m = re_meas.search(l)
|
|
417
455
|
if(m):
|
|
418
456
|
analysis = True
|
|
419
457
|
|
|
@@ -428,19 +466,16 @@ class Simulation(cs.CdsConfig):
|
|
|
428
466
|
with open(measlog) as fi:
|
|
429
467
|
for l in fi:
|
|
430
468
|
|
|
431
|
-
|
|
432
469
|
if(analysis):
|
|
433
|
-
#
|
|
434
|
-
|
|
435
|
-
if(not prepareTable and re.search("^---------------------------------",l)):
|
|
470
|
+
# Parse tables
|
|
471
|
+
if(not prepareTable and re_sep.search(l)):
|
|
436
472
|
prepareTable = True
|
|
437
473
|
continue
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if(table and not prepareTable and (not re.search(r"^\d",l) )):
|
|
474
|
+
|
|
475
|
+
if(table and not prepareTable and (not re_digit.search(l))):
|
|
441
476
|
table = False
|
|
442
477
|
|
|
443
|
-
if(prepareTable and
|
|
478
|
+
if(prepareTable and re_index.search(l)):
|
|
444
479
|
arr = re.split(r"\s+",l)
|
|
445
480
|
table_header = arr[1:]
|
|
446
481
|
table = True
|
|
@@ -448,28 +483,32 @@ class Simulation(cs.CdsConfig):
|
|
|
448
483
|
|
|
449
484
|
if(table and not prepareTable):
|
|
450
485
|
arr = re.split(r"\s+",l)
|
|
451
|
-
|
|
452
486
|
for k,v in zip(table_header,arr[1:]):
|
|
453
487
|
if(v== ""):
|
|
454
488
|
continue
|
|
455
489
|
if(k not in data):
|
|
456
490
|
data[k] = list()
|
|
457
|
-
|
|
458
|
-
|
|
491
|
+
try:
|
|
492
|
+
data[k].append(float(v))
|
|
493
|
+
except ValueError:
|
|
494
|
+
logger.warning(f"Could not parse table value: {k}={v}")
|
|
459
495
|
|
|
460
496
|
if(prepareTable):
|
|
461
497
|
prepareTable = False
|
|
462
498
|
|
|
463
499
|
#- Find key=val pairs
|
|
464
|
-
if(analysis and
|
|
465
|
-
m =
|
|
500
|
+
if(analysis and re_eq.search(l)):
|
|
501
|
+
m = re_kv.search(l)
|
|
466
502
|
if(m):
|
|
467
503
|
key = m.groups()[0].strip()
|
|
468
504
|
val = m.groups()[1].strip()
|
|
469
|
-
|
|
470
|
-
|
|
505
|
+
try:
|
|
506
|
+
data[key] = float(val)
|
|
507
|
+
except ValueError:
|
|
508
|
+
logger.warning(f"Could not parse measurement value: {key}={val}")
|
|
509
|
+
if(re_start.search(l)):
|
|
471
510
|
analysis = True
|
|
472
|
-
if(
|
|
511
|
+
if(re_end.search(l)):
|
|
473
512
|
analysis = False
|
|
474
513
|
|
|
475
514
|
yamlfile = self.oname + ".yaml"
|
|
@@ -515,13 +554,16 @@ class CmdRunNg(cs.CdsConfig):
|
|
|
515
554
|
""" Run ngspice
|
|
516
555
|
"""
|
|
517
556
|
|
|
518
|
-
def __init__(self,testbench,runsim,corners,cornername,count,sha):
|
|
557
|
+
def __init__(self,testbench,runsim,corners,cornername,count,sha,threads=1,progress=False,timeout=None):
|
|
519
558
|
self.testbench = testbench
|
|
520
559
|
self.count = count
|
|
521
560
|
self.runsim = runsim
|
|
522
561
|
self.corners = corners
|
|
523
562
|
self.cornername = cornername
|
|
524
563
|
self.sha = sha
|
|
564
|
+
self.threads = max(1, threads)
|
|
565
|
+
self.progress = progress
|
|
566
|
+
self.timeout = timeout
|
|
525
567
|
self.replace = None
|
|
526
568
|
|
|
527
569
|
super().__init__()
|
|
@@ -545,7 +587,7 @@ class CmdRunNg(cs.CdsConfig):
|
|
|
545
587
|
|
|
546
588
|
def run(self,ignore=False):
|
|
547
589
|
startTime = datetime.datetime.now()
|
|
548
|
-
|
|
590
|
+
|
|
549
591
|
self.filename = self.testbench + ".spi"
|
|
550
592
|
|
|
551
593
|
if(not os.path.exists(self.filename)):
|
|
@@ -553,36 +595,72 @@ class CmdRunNg(cs.CdsConfig):
|
|
|
553
595
|
return
|
|
554
596
|
|
|
555
597
|
permutations = self.getPermutations(self.corners)
|
|
598
|
+
|
|
599
|
+
# Build ordered list of (corner, index) jobs
|
|
600
|
+
jobs = [(corner, index) for corner in permutations for index in range(self.count)]
|
|
601
|
+
|
|
602
|
+
def run_one(job):
|
|
603
|
+
corner, index = job
|
|
604
|
+
c = Simulation(self.testbench, corner, self.runsim, self.config, index, self.sha,
|
|
605
|
+
progress=self.progress, timeout=self.timeout)
|
|
606
|
+
c.loadReplace(self.replace)
|
|
607
|
+
ok = c.run(ignore)
|
|
608
|
+
return c, bool(ok)
|
|
609
|
+
|
|
610
|
+
executor = None
|
|
611
|
+
try:
|
|
612
|
+
if self.progress:
|
|
613
|
+
from tqdm import tqdm
|
|
614
|
+
from cicsim.command import suppress_console_logging
|
|
615
|
+
with suppress_console_logging():
|
|
616
|
+
if self.threads > 1:
|
|
617
|
+
executor = ThreadPoolExecutor(max_workers=self.threads)
|
|
618
|
+
results = list(tqdm(executor.map(run_one, jobs),
|
|
619
|
+
total=len(jobs), desc=self.testbench, unit="sim"))
|
|
620
|
+
else:
|
|
621
|
+
results = list(tqdm((run_one(job) for job in jobs),
|
|
622
|
+
total=len(jobs), desc=self.testbench, unit="sim"))
|
|
623
|
+
elif self.threads > 1:
|
|
624
|
+
logger.info(f"Running {len(jobs)} simulation(s) with {self.threads} threads")
|
|
625
|
+
executor = ThreadPoolExecutor(max_workers=self.threads)
|
|
626
|
+
results = list(executor.map(run_one, jobs))
|
|
627
|
+
else:
|
|
628
|
+
results = [run_one(job) for job in jobs]
|
|
629
|
+
except KeyboardInterrupt:
|
|
630
|
+
logger.warning("Interrupted — cancelling pending simulations")
|
|
631
|
+
if executor is not None:
|
|
632
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
633
|
+
sys.exit(2)
|
|
634
|
+
finally:
|
|
635
|
+
if executor is not None:
|
|
636
|
+
executor.shutdown(wait=False)
|
|
637
|
+
|
|
556
638
|
pyRunLater = list()
|
|
557
639
|
files = list()
|
|
558
640
|
simOk = True
|
|
641
|
+
failed_names = []
|
|
559
642
|
|
|
560
|
-
for
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
c = Simulation(self.testbench,corner,self.runsim,self.config,index,self.sha)
|
|
568
|
-
|
|
569
|
-
#- Setup additional replacements
|
|
570
|
-
c.loadReplace(self.replace)
|
|
571
|
-
|
|
572
|
-
#- Run simulation
|
|
573
|
-
if(not c.run(ignore)):
|
|
574
|
-
simOk = False
|
|
575
|
-
continue
|
|
576
|
-
|
|
643
|
+
for c, ok in results:
|
|
644
|
+
if not ok:
|
|
645
|
+
simOk = False
|
|
646
|
+
failed_names.append(c.name)
|
|
647
|
+
if not self.progress:
|
|
648
|
+
logger.error(f"Simulation {c.name} failed")
|
|
649
|
+
else:
|
|
577
650
|
files.append(c.oname)
|
|
578
|
-
|
|
579
|
-
#- Run python post parsing if py file exist and simulation is OK
|
|
580
|
-
if(simOk and os.path.exists(c.pyscript)):
|
|
651
|
+
if os.path.exists(c.pyscript):
|
|
581
652
|
pyRunLater.append(c)
|
|
582
653
|
|
|
583
|
-
|
|
584
654
|
endTime = datetime.datetime.now()
|
|
585
|
-
|
|
655
|
+
elapsed = str(endTime - startTime)
|
|
656
|
+
|
|
657
|
+
if self.progress:
|
|
658
|
+
passed = len(results) - len(failed_names)
|
|
659
|
+
print(f"Done: {passed}/{len(results)} passed [{elapsed}]")
|
|
660
|
+
for name in failed_names:
|
|
661
|
+
print(f" FAILED: {name}")
|
|
662
|
+
else:
|
|
663
|
+
logger.info("Total simulation time: " + elapsed)
|
|
586
664
|
|
|
587
665
|
#- Make runfile
|
|
588
666
|
if(self.cornername):
|
|
@@ -607,7 +685,7 @@ class CmdRunNg(cs.CdsConfig):
|
|
|
607
685
|
else:
|
|
608
686
|
tb.main(c.oname)
|
|
609
687
|
#- Extract results
|
|
610
|
-
r = cs.CmdResults(runfile)
|
|
688
|
+
r = cs.CmdResults(runfile, progress=self.progress)
|
|
611
689
|
r.run()
|
|
612
690
|
else:
|
|
613
691
|
logger.warning("Skipping post processing, one simulation failed")
|
|
@@ -146,8 +146,7 @@ class Summary(cs.Command):
|
|
|
146
146
|
super().__init__()
|
|
147
147
|
|
|
148
148
|
if(not os.path.exists(self.filename)):
|
|
149
|
-
|
|
150
|
-
exit()
|
|
149
|
+
raise FileNotFoundError(f"Could not find {self.filename}")
|
|
151
150
|
|
|
152
151
|
with open(self.filename) as fi:
|
|
153
152
|
obj =yaml.safe_load(fi)
|
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
|
|
9
9
|
from .wavebrowser import *
|
|
10
10
|
from .wavegraph import *
|
|
11
|
+
from .theme import _get_theme
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
#- I try to follow a Model, View, Controller type of design pattern
|
|
@@ -261,9 +262,10 @@ class CmdWave(cs.Command):
|
|
|
261
262
|
win = Toplevel(self.root)
|
|
262
263
|
win.title("Keyboard Shortcuts")
|
|
263
264
|
win.resizable(False, False)
|
|
265
|
+
theme = _get_theme()
|
|
264
266
|
label = Label(win, text=text, justify=LEFT,
|
|
265
267
|
font=("Courier", 11),
|
|
266
|
-
bg=
|
|
268
|
+
bg=theme['panel_bg'], fg=theme['panel_fg'],
|
|
267
269
|
padx=16, pady=12)
|
|
268
270
|
label.pack(fill=BOTH, expand=True)
|
|
269
271
|
btn = ttk.Button(win, text="Close", command=win.destroy)
|