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.
Files changed (36) hide show
  1. {cicsim-0.2.2 → cicsim-0.2.7}/PKG-INFO +3 -1
  2. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cicsim.py +20 -4
  3. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdresults.py +95 -33
  4. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdrunng.py +135 -57
  5. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdsummary.py +1 -2
  6. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdwave.py +3 -1
  7. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdwave_pg.py +431 -98
  8. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/command.py +17 -0
  9. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/ngraw.py +3 -5
  10. cicsim-0.2.7/cicsim/theme.py +108 -0
  11. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/wavebrowser.py +4 -1
  12. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/wavefiles.py +13 -0
  13. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/wavegraph.py +20 -10
  14. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/PKG-INFO +3 -1
  15. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/SOURCES.txt +1 -0
  16. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/requires.txt +2 -0
  17. {cicsim-0.2.2 → cicsim-0.2.7}/pyproject.toml +3 -1
  18. {cicsim-0.2.2 → cicsim-0.2.7}/LICENSE +0 -0
  19. {cicsim-0.2.2 → cicsim-0.2.7}/README.md +0 -0
  20. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/__init__.py +0 -0
  21. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cdsconf.py +0 -0
  22. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdarchive.py +0 -0
  23. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdip.py +0 -0
  24. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdrun.py +0 -0
  25. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdsimdir.py +0 -0
  26. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdsimdirng.py +0 -0
  27. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/cmdspider.py +0 -0
  28. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/pivot.py +0 -0
  29. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/plot.py +0 -0
  30. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/simcalc.py +0 -0
  31. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/spec.py +0 -0
  32. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim/spiceparser.py +0 -0
  33. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/dependency_links.txt +0 -0
  34. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/entry_points.txt +0 -0
  35. {cicsim-0.2.2 → cicsim-0.2.7}/cicsim.egg-info/top_level.txt +0 -0
  36. {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.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,help="Specify x-axis")
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, help="Specify x-axis")
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
- df_all = pd.DataFrame()
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 = os.path.basename(f).replace(".*_","").replace(".yaml","")
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
- df_all = pd.concat([df,df_all])
133
+ frames.append(df)
153
134
 
135
+ if not frames:
136
+ return None
154
137
 
155
- if(df_all.empty):
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
- logger.warning(f"Could not find referenced file {fpath}")
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
- logger.warning("No spice files have changed")
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
- logger.warning("No meas files have changed")
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
- logger.warning("Not storing sha file, no simulation run")
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
- cmd = f"cd {self.rundir}; ngspice {options} {includes} {self.name}.spi -r {self.name}.raw 2>&1 |tee {self.name}.log"
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
- logger.warning(f"Skipping simulation of {self.name}.spi")
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
- cmd = f"cd {self.rundir}; ngspice -b {self.name}.meas 2>&1 | tee {self.name}.logm"
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
- logger.warning(f"Skipping measurement run of {self.name}.meas")
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 re.search("=",l)):
407
- m = re.search(r"^([^\s]+)\s*=\s*([^\s]+)\s",l)
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
- data[key] = float(val)
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(re.search("binary raw file",l)):
451
+ if(re_binary.search(l)):
414
452
  analysis = False
415
453
 
416
- m = re.search("Measurements for (.*)$",l)
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
- #print(table,prepareTable,l)
434
- # Parse tables
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
- #print(l,end="")
439
- #print(re.search(r"^\d",l))
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 re.search("^Index",l)):
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
- data[k].append(float(v))
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 re.search("=",l)):
465
- m = re.search(r"^([^\s]+)\s*=\s*([^\s]+)\s",l)
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
- data[key] = float(val)
470
- if(re.search(r"^\s*MEAS_START",l)):
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(re.search(r"^\s*MEAS_END",l)):
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 corner in permutations:
561
- for index in range(0,self.count):
562
- if(not simOk):
563
- logger.error(f"Previous simulation failed, skipping {index}")
564
- continue
565
-
566
- #- Run a simulation for a corner
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
- logger.info("Total simulation time: " + str(endTime - startTime))
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
- logger.error(f"Could not find {self.filename}")
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="#2b2b2b", fg="#e0e0e0",
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)