Tim Edwards | dae621a | 2021-09-08 09:28:02 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 2 | # |
| 3 | #-------------------------------------------------------- |
emayecs | 1474831 | 2021-08-05 14:21:26 -0400 | [diff] [blame] | 4 | # Project Manager GUI. |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 5 | # |
| 6 | # This is a Python tkinter script that handles local |
| 7 | # project management. Much of this involves the |
| 8 | # running of ng-spice for characterization, allowing |
| 9 | # the user to determine where a circuit is failing |
| 10 | # characterization; and when the design passes local |
| 11 | # characterization, it may be submitted to the |
| 12 | # marketplace for official characterization. |
| 13 | # |
| 14 | #-------------------------------------------------------- |
| 15 | # Written by Tim Edwards |
| 16 | # efabless, inc. |
| 17 | # September 9, 2016 |
| 18 | # Version 1.0 |
| 19 | #-------------------------------------------------------- |
| 20 | |
| 21 | import io |
| 22 | import re |
| 23 | import os |
| 24 | import sys |
| 25 | import copy |
| 26 | import json |
| 27 | import time |
| 28 | import signal |
| 29 | import select |
| 30 | import datetime |
| 31 | import contextlib |
| 32 | import subprocess |
| 33 | import faulthandler |
| 34 | |
| 35 | import tkinter |
| 36 | from tkinter import ttk |
| 37 | from tkinter import filedialog |
| 38 | |
| 39 | import tksimpledialog |
| 40 | import tooltip |
| 41 | from consoletext import ConsoleText |
| 42 | from helpwindow import HelpWindow |
| 43 | from failreport import FailReport |
| 44 | from textreport import TextReport |
| 45 | from editparam import EditParam |
| 46 | from settings import Settings |
| 47 | from simhints import SimHints |
| 48 | |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 49 | # User preferences file (if it exists) |
| 50 | prefsfile = '~/design/.profile/prefs.json' |
| 51 | |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 52 | # Application path (path where this script is located) |
| 53 | apps_path = os.path.realpath(os.path.dirname(__file__)) |
| 54 | |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 55 | #------------------------------------------------------ |
| 56 | # Simple dialog for confirming quit or upload |
| 57 | #------------------------------------------------------ |
| 58 | |
| 59 | class ConfirmDialog(tksimpledialog.Dialog): |
| 60 | def body(self, master, warning, seed): |
| 61 | ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns') |
| 62 | return self |
| 63 | |
| 64 | def apply(self): |
| 65 | return 'okay' |
| 66 | |
| 67 | #------------------------------------------------------ |
| 68 | # Simple dialog with no "OK" button (can only cancel) |
| 69 | #------------------------------------------------------ |
| 70 | |
| 71 | class PuntDialog(tksimpledialog.Dialog): |
| 72 | def body(self, master, warning, seed): |
| 73 | if warning: |
| 74 | ttk.Label(master, text=warning, wraplength=500).grid(row = 0, columnspan = 2, sticky = 'wns') |
| 75 | return self |
| 76 | |
| 77 | def buttonbox(self): |
| 78 | # Add button box with "Cancel" only. |
| 79 | box = ttk.Frame(self.obox) |
| 80 | w = ttk.Button(box, text="Cancel", width=10, command=self.cancel) |
| 81 | w.pack(side='left', padx=5, pady=5) |
| 82 | self.bind("<Escape>", self.cancel) |
| 83 | box.pack(fill='x', expand='true') |
| 84 | |
| 85 | def apply(self): |
| 86 | return 'okay' |
| 87 | |
| 88 | #------------------------------------------------------ |
| 89 | # Main class for this application |
| 90 | #------------------------------------------------------ |
| 91 | |
Tim Edwards | 63dbde9 | 2023-03-07 20:58:29 -0500 | [diff] [blame] | 92 | class CACECharacterize(ttk.Frame): |
emayecs | 1474831 | 2021-08-05 14:21:26 -0400 | [diff] [blame] | 93 | """local characterization GUI.""" |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 94 | |
| 95 | def __init__(self, parent, *args, **kwargs): |
| 96 | ttk.Frame.__init__(self, parent, *args, **kwargs) |
| 97 | self.root = parent |
| 98 | self.init_gui() |
| 99 | parent.protocol("WM_DELETE_WINDOW", self.on_quit) |
| 100 | |
| 101 | def on_quit(self): |
| 102 | """Exits program.""" |
| 103 | if not self.check_saved(): |
| 104 | warning = 'Warning: Simulation results have not been saved.' |
| 105 | confirm = ConfirmDialog(self, warning).result |
| 106 | if not confirm == 'okay': |
| 107 | print('Quit canceled.') |
| 108 | return |
| 109 | if self.logfile: |
| 110 | self.logfile.close() |
| 111 | quit() |
| 112 | |
| 113 | def on_mousewheel(self, event): |
| 114 | if event.num == 5: |
| 115 | self.datasheet_viewer.yview_scroll(1, "units") |
| 116 | elif event.num == 4: |
| 117 | self.datasheet_viewer.yview_scroll(-1, "units") |
| 118 | |
| 119 | def init_gui(self): |
| 120 | """Builds GUI.""" |
| 121 | global prefsfile |
| 122 | |
| 123 | message = [] |
| 124 | fontsize = 11 |
| 125 | |
| 126 | # Read user preferences file, get default font size from it. |
| 127 | prefspath = os.path.expanduser(prefsfile) |
| 128 | if os.path.exists(prefspath): |
| 129 | with open(prefspath, 'r') as f: |
| 130 | self.prefs = json.load(f) |
| 131 | if 'fontsize' in self.prefs: |
| 132 | fontsize = self.prefs['fontsize'] |
| 133 | else: |
| 134 | self.prefs = {} |
| 135 | |
| 136 | s = ttk.Style() |
| 137 | |
| 138 | available_themes = s.theme_names() |
| 139 | s.theme_use(available_themes[0]) |
| 140 | |
| 141 | s.configure('bg.TFrame', background='gray40') |
| 142 | s.configure('italic.TLabel', font=('Helvetica', fontsize, 'italic')) |
| 143 | s.configure('title.TLabel', font=('Helvetica', fontsize, 'bold italic'), |
| 144 | foreground = 'brown', anchor = 'center') |
| 145 | s.configure('normal.TLabel', font=('Helvetica', fontsize)) |
| 146 | s.configure('red.TLabel', font=('Helvetica', fontsize), foreground = 'red') |
| 147 | s.configure('green.TLabel', font=('Helvetica', fontsize), foreground = 'green3') |
| 148 | s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue') |
| 149 | s.configure('hlight.TLabel', font=('Helvetica', fontsize), background='gray93') |
| 150 | s.configure('rhlight.TLabel', font=('Helvetica', fontsize), foreground = 'red', |
| 151 | background='gray93') |
| 152 | s.configure('ghlight.TLabel', font=('Helvetica', fontsize), foreground = 'green3', |
| 153 | background='gray93') |
| 154 | s.configure('blue.TLabel', font=('Helvetica', fontsize), foreground = 'blue') |
| 155 | s.configure('blue.TMenubutton', font=('Helvetica', fontsize), foreground = 'blue', |
| 156 | border = 3, relief = 'raised') |
| 157 | s.configure('normal.TButton', font=('Helvetica', fontsize), |
| 158 | border = 3, relief = 'raised') |
| 159 | s.configure('red.TButton', font=('Helvetica', fontsize), foreground = 'red', |
| 160 | border = 3, relief = 'raised') |
| 161 | s.configure('green.TButton', font=('Helvetica', fontsize), foreground = 'green3', |
| 162 | border = 3, relief = 'raised') |
| 163 | s.configure('hlight.TButton', font=('Helvetica', fontsize), |
| 164 | border = 3, relief = 'raised', background='gray93') |
| 165 | s.configure('rhlight.TButton', font=('Helvetica', fontsize), foreground = 'red', |
| 166 | border = 3, relief = 'raised', background='gray93') |
| 167 | s.configure('ghlight.TButton', font=('Helvetica', fontsize), foreground = 'green3', |
| 168 | border = 3, relief = 'raised', background='gray93') |
| 169 | s.configure('blue.TButton', font=('Helvetica', fontsize), foreground = 'blue', |
| 170 | border = 3, relief = 'raised') |
| 171 | s.configure('redtitle.TButton', font=('Helvetica', fontsize, 'bold italic'), |
| 172 | foreground = 'red', border = 3, relief = 'raised') |
| 173 | s.configure('bluetitle.TButton', font=('Helvetica', fontsize, 'bold italic'), |
| 174 | foreground = 'blue', border = 3, relief = 'raised') |
| 175 | |
| 176 | # Create the help window |
| 177 | self.help = HelpWindow(self, fontsize = fontsize) |
| 178 | |
| 179 | with io.StringIO() as buf, contextlib.redirect_stdout(buf): |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 180 | self.help.add_pages_from_file(apps_path + '/characterize_help.txt') |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 181 | message = buf.getvalue() |
| 182 | |
| 183 | # Set the help display to the first page |
| 184 | self.help.page(0) |
| 185 | |
| 186 | # Create the failure report window |
| 187 | self.failreport = FailReport(self, fontsize = fontsize) |
| 188 | |
| 189 | # LVS results get a text window of results |
| 190 | self.textreport = TextReport(self, fontsize = fontsize) |
| 191 | |
| 192 | # Create the settings window |
| 193 | self.settings = Settings(self, fontsize = fontsize, callback = self.callback) |
| 194 | |
| 195 | # Create the simulation hints window |
| 196 | self.simhints = SimHints(self, fontsize = fontsize) |
| 197 | |
| 198 | # Create the edit parameter window |
| 199 | self.editparam = EditParam(self, fontsize = fontsize) |
| 200 | |
| 201 | # Variables used by option menus and other stuff |
| 202 | self.origin = tkinter.StringVar(self) |
| 203 | self.cur_project = tkinter.StringVar(self) |
| 204 | self.cur_datasheet = "(no selection)" |
| 205 | self.datatop = {} |
| 206 | self.status = {} |
| 207 | self.caceproc = None |
| 208 | self.logfile = None |
| 209 | |
| 210 | # Root window title |
emayecs | 1474831 | 2021-08-05 14:21:26 -0400 | [diff] [blame] | 211 | self.root.title('Characterization') |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 212 | self.root.option_add('*tearOff', 'FALSE') |
| 213 | self.pack(side = 'top', fill = 'both', expand = 'true') |
| 214 | |
| 215 | pane = tkinter.PanedWindow(self, orient = 'vertical', sashrelief='groove', sashwidth=6) |
| 216 | pane.pack(side = 'top', fill = 'both', expand = 'true') |
| 217 | self.toppane = ttk.Frame(pane) |
| 218 | self.botpane = ttk.Frame(pane) |
| 219 | |
| 220 | # Get username |
| 221 | if 'username' in self.prefs: |
| 222 | username = self.prefs['username'] |
| 223 | else: |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 224 | username = os.environ['USER'] |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 225 | |
| 226 | # Label with the user |
| 227 | self.toppane.title_frame = ttk.Frame(self.toppane) |
| 228 | self.toppane.title_frame.grid(column = 0, row=0, sticky = 'nswe') |
| 229 | |
| 230 | self.toppane.title_frame.title = ttk.Label(self.toppane.title_frame, text='User:', style = 'red.TLabel') |
| 231 | self.toppane.title_frame.user = ttk.Label(self.toppane.title_frame, text=username, style = 'blue.TLabel') |
| 232 | |
| 233 | self.toppane.title_frame.title.grid(column=0, row=0, ipadx = 5) |
| 234 | self.toppane.title_frame.user.grid(column=1, row=0, ipadx = 5) |
| 235 | |
| 236 | #--------------------------------------------- |
| 237 | ttk.Separator(self.toppane, orient='horizontal').grid(column = 0, row = 1, sticky = 'nswe') |
| 238 | #--------------------------------------------- |
| 239 | |
| 240 | self.toppane.title2_frame = ttk.Frame(self.toppane) |
| 241 | self.toppane.title2_frame.grid(column = 0, row = 2, sticky = 'nswe') |
| 242 | self.toppane.title2_frame.datasheet_label = ttk.Label(self.toppane.title2_frame, text="Datasheet:", |
| 243 | style = 'normal.TLabel') |
| 244 | self.toppane.title2_frame.datasheet_label.grid(column=0, row=0, ipadx = 5) |
| 245 | |
| 246 | # New datasheet select button |
| 247 | self.toppane.title2_frame.datasheet_select = ttk.Button(self.toppane.title2_frame, |
| 248 | text=self.cur_datasheet, style='normal.TButton', command=self.choose_datasheet) |
| 249 | self.toppane.title2_frame.datasheet_select.grid(column=1, row=0, ipadx = 5) |
| 250 | |
| 251 | tooltip.ToolTip(self.toppane.title2_frame.datasheet_select, |
| 252 | text = "Select new datasheet file") |
| 253 | |
| 254 | # Show path to datasheet |
| 255 | self.toppane.title2_frame.path_label = ttk.Label(self.toppane.title2_frame, text=self.cur_datasheet, |
| 256 | style = 'normal.TLabel') |
| 257 | self.toppane.title2_frame.path_label.grid(column=2, row=0, ipadx = 5, padx = 10) |
| 258 | |
| 259 | # Spacer in middle moves selection button to right |
| 260 | self.toppane.title2_frame.sep_label = ttk.Label(self.toppane.title2_frame, text=' ', |
| 261 | style = 'normal.TLabel') |
| 262 | self.toppane.title2_frame.sep_label.grid(column=3, row=0, ipadx = 5, padx = 10) |
| 263 | self.toppane.title2_frame.columnconfigure(3, weight = 1) |
| 264 | self.toppane.title2_frame.rowconfigure(0, weight=0) |
| 265 | |
| 266 | # Selection for origin of netlist |
| 267 | self.toppane.title2_frame.origin_label = ttk.Label(self.toppane.title2_frame, |
| 268 | text='Netlist from:', style = 'normal.TLabel') |
| 269 | self.toppane.title2_frame.origin_label.grid(column=4, row=0, ipadx = 5, padx = 10) |
| 270 | |
| 271 | self.origin.set('Schematic Capture') |
| 272 | self.toppane.title2_frame.origin_select = ttk.OptionMenu(self.toppane.title2_frame, |
| 273 | self.origin, 'Schematic Capture', 'Schematic Capture', 'Layout Extracted', |
| 274 | style='blue.TMenubutton', command=self.load_results) |
| 275 | self.toppane.title2_frame.origin_select.grid(column=5, row=0, ipadx = 5) |
| 276 | |
| 277 | #--------------------------------------------- |
| 278 | ttk.Separator(self.toppane, orient='horizontal').grid(column = 0, row = 3, sticky = 'news') |
| 279 | #--------------------------------------------- |
| 280 | |
| 281 | # Datasheet information goes here when datasheet is loaded. |
| 282 | self.mframe = ttk.Frame(self.toppane) |
| 283 | self.mframe.grid(column = 0, row = 4, sticky = 'news') |
| 284 | |
| 285 | # Row 4 (mframe) is expandable, the other rows are not. |
| 286 | self.toppane.rowconfigure(0, weight = 0) |
| 287 | self.toppane.rowconfigure(1, weight = 0) |
| 288 | self.toppane.rowconfigure(2, weight = 0) |
| 289 | self.toppane.rowconfigure(3, weight = 0) |
| 290 | self.toppane.rowconfigure(4, weight = 1) |
| 291 | self.toppane.columnconfigure(0, weight = 1) |
| 292 | |
| 293 | #--------------------------------------------- |
| 294 | # ttk.Separator(self, orient='horizontal').grid(column=0, row=5, sticky='ew') |
| 295 | #--------------------------------------------- |
| 296 | |
| 297 | # Add a text window below the datasheet to capture output. Redirect |
| 298 | # print statements to it. |
| 299 | |
| 300 | self.botpane.console = ttk.Frame(self.botpane) |
| 301 | self.botpane.console.pack(side = 'top', fill = 'both', expand = 'true') |
| 302 | |
| 303 | self.text_box = ConsoleText(self.botpane.console, wrap='word', height = 4) |
| 304 | self.text_box.pack(side='left', fill='both', expand='true') |
| 305 | console_scrollbar = ttk.Scrollbar(self.botpane.console) |
| 306 | console_scrollbar.pack(side='right', fill='y') |
| 307 | # attach console to scrollbar |
| 308 | self.text_box.config(yscrollcommand = console_scrollbar.set) |
| 309 | console_scrollbar.config(command = self.text_box.yview) |
| 310 | |
| 311 | # Add button bar at the bottom of the window |
| 312 | self.bbar = ttk.Frame(self.botpane) |
| 313 | self.bbar.pack(side = 'top', fill = 'x') |
| 314 | # Progress bar expands with the window, buttons don't |
| 315 | self.bbar.columnconfigure(6, weight = 1) |
| 316 | |
| 317 | # Define the "quit" button and action |
| 318 | self.bbar.quit_button = ttk.Button(self.bbar, text='Close', command=self.on_quit, |
| 319 | style = 'normal.TButton') |
| 320 | self.bbar.quit_button.grid(column=0, row=0, padx = 5) |
| 321 | |
| 322 | # Define the save button |
| 323 | self.bbar.save_button = ttk.Button(self.bbar, text='Save', command=self.save_results, |
| 324 | style = 'normal.TButton') |
| 325 | self.bbar.save_button.grid(column=1, row=0, padx = 5) |
| 326 | |
| 327 | # Define the save-as button |
| 328 | self.bbar.saveas_button = ttk.Button(self.bbar, text='Save As', command=self.save_manual, |
| 329 | style = 'normal.TButton') |
| 330 | |
| 331 | # Also a load button |
| 332 | self.bbar.load_button = ttk.Button(self.bbar, text='Load', command=self.load_manual, |
| 333 | style = 'normal.TButton') |
| 334 | |
| 335 | # Define help button |
| 336 | self.bbar.help_button = ttk.Button(self.bbar, text='Help', command=self.help.open, |
| 337 | style = 'normal.TButton') |
| 338 | self.bbar.help_button.grid(column = 4, row = 0, padx = 5) |
| 339 | |
| 340 | # Define settings button |
| 341 | self.bbar.settings_button = ttk.Button(self.bbar, text='Settings', |
| 342 | command=self.settings.open, style = 'normal.TButton') |
| 343 | self.bbar.settings_button.grid(column = 5, row = 0, padx = 5) |
| 344 | |
| 345 | # Define upload action |
| 346 | self.bbar.upload_button = ttk.Button(self.bbar, text='Submit', state = 'enabled', |
| 347 | command=self.upload_to_marketplace, style = 'normal.TButton') |
| 348 | # "Submit" button remains unplaced; upload may be done from the web side. . . |
| 349 | # self.bbar.upload_button.grid(column = 8, row = 0, padx = 5, sticky = 'ens') |
| 350 | |
| 351 | tooltip.ToolTip(self.bbar.quit_button, text = "Exit characterization tool") |
| 352 | tooltip.ToolTip(self.bbar.save_button, text = "Save current characterization state") |
| 353 | tooltip.ToolTip(self.bbar.saveas_button, text = "Save current characterization state") |
| 354 | tooltip.ToolTip(self.bbar.load_button, text = "Load characterization state from file") |
| 355 | tooltip.ToolTip(self.bbar.help_button, text = "Start help tool") |
| 356 | tooltip.ToolTip(self.bbar.settings_button, text = "Manage characterization tool settings") |
| 357 | tooltip.ToolTip(self.bbar.upload_button, text = "Submit completed design to Marketplace") |
| 358 | |
| 359 | # Inside frame with main electrical parameter display and scrollbar |
| 360 | # To make the frame scrollable, it must be a frame inside a canvas. |
| 361 | self.datasheet_viewer = tkinter.Canvas(self.mframe) |
| 362 | self.datasheet_viewer.grid(row = 0, column = 0, sticky = 'nsew') |
| 363 | self.datasheet_viewer.dframe = ttk.Frame(self.datasheet_viewer, |
| 364 | style='bg.TFrame') |
| 365 | # Place the frame in the canvas |
| 366 | self.datasheet_viewer.create_window((0,0), |
| 367 | window=self.datasheet_viewer.dframe, |
| 368 | anchor="nw", tags="self.frame") |
| 369 | |
| 370 | # Make sure the main window resizes, not the scrollbars. |
| 371 | self.mframe.rowconfigure(0, weight = 1) |
| 372 | self.mframe.columnconfigure(0, weight = 1) |
| 373 | # X scrollbar for datasheet viewer |
| 374 | main_xscrollbar = ttk.Scrollbar(self.mframe, orient = 'horizontal') |
| 375 | main_xscrollbar.grid(row = 1, column = 0, sticky = 'nsew') |
| 376 | # Y scrollbar for datasheet viewer |
| 377 | main_yscrollbar = ttk.Scrollbar(self.mframe, orient = 'vertical') |
| 378 | main_yscrollbar.grid(row = 0, column = 1, sticky = 'nsew') |
| 379 | # Attach console to scrollbars |
| 380 | self.datasheet_viewer.config(xscrollcommand = main_xscrollbar.set) |
| 381 | main_xscrollbar.config(command = self.datasheet_viewer.xview) |
| 382 | self.datasheet_viewer.config(yscrollcommand = main_yscrollbar.set) |
| 383 | main_yscrollbar.config(command = self.datasheet_viewer.yview) |
| 384 | |
| 385 | # Make sure that scrollwheel pans window |
| 386 | self.datasheet_viewer.bind_all("<Button-4>", self.on_mousewheel) |
| 387 | self.datasheet_viewer.bind_all("<Button-5>", self.on_mousewheel) |
| 388 | |
| 389 | # Set up configure callback |
| 390 | self.datasheet_viewer.dframe.bind("<Configure>", self.frame_configure) |
| 391 | |
| 392 | # Add the panes once the internal geometry is known |
| 393 | pane.add(self.toppane) |
| 394 | pane.add(self.botpane) |
| 395 | pane.paneconfig(self.toppane, stretch='first') |
| 396 | |
| 397 | # Initialize variables |
| 398 | self.sims_to_go = [] |
| 399 | |
| 400 | # Capture time of start to compare against the annotated |
| 401 | # output file timestamp. |
| 402 | self.starttime = time.time() |
| 403 | |
| 404 | # Redirect stdout and stderr to the console as the last thing to do. . . |
| 405 | # Otherwise errors in the GUI get sucked into the void. |
| 406 | self.stdout = sys.stdout |
| 407 | self.stderr = sys.stderr |
| 408 | sys.stdout = ConsoleText.StdoutRedirector(self.text_box) |
| 409 | sys.stderr = ConsoleText.StderrRedirector(self.text_box) |
| 410 | |
| 411 | if message: |
| 412 | print(message) |
| 413 | |
| 414 | def frame_configure(self, event): |
| 415 | self.update_idletasks() |
| 416 | self.datasheet_viewer.configure(scrollregion=self.datasheet_viewer.bbox("all")) |
| 417 | |
| 418 | def logstart(self): |
| 419 | # Start a logfile (or append to it, if it already exists) |
| 420 | # Disabled by default, as it can get very large. |
| 421 | # Can be enabled from Settings. |
| 422 | if self.settings.get_log() == True: |
| 423 | dataroot = os.path.splitext(self.cur_datasheet)[0] |
| 424 | if not self.logfile: |
| 425 | self.logfile = open(dataroot + '.log', 'a') |
| 426 | |
| 427 | # Print some initial information to the logfile. |
| 428 | self.logprint('-------------------------') |
| 429 | self.logprint('Starting new log file ' + datetime.datetime.now().strftime('%c'), |
| 430 | doflush=True) |
| 431 | |
| 432 | def logstop(self): |
| 433 | if self.logfile: |
| 434 | self.logprint('-------------------------', doflush=True) |
| 435 | self.logfile.close() |
| 436 | self.logfile = [] |
| 437 | |
| 438 | def logprint(self, message, doflush=False): |
| 439 | if self.logfile: |
| 440 | self.logfile.buffer.write(message.encode('utf-8')) |
| 441 | self.logfile.buffer.write('\n'.encode('utf-8')) |
| 442 | if doflush: |
| 443 | self.logfile.flush() |
| 444 | |
| 445 | def set_datasheet(self, datasheet): |
| 446 | if self.logfile: |
| 447 | self.logprint('end of log.') |
| 448 | self.logprint('-------------------------', doflush=True) |
| 449 | self.logfile.close() |
| 450 | self.logfile = None |
| 451 | |
| 452 | if not os.path.isfile(datasheet): |
| 453 | print('Error: File ' + datasheet + ' not found.') |
| 454 | return |
| 455 | |
| 456 | [dspath, dsname] = os.path.split(datasheet) |
| 457 | # Read the datasheet |
| 458 | with open(datasheet) as ifile: |
| 459 | try: |
| 460 | datatop = json.load(ifile) |
| 461 | except json.decoder.JSONDecodeError as e: |
| 462 | print("Error: Parse error reading JSON file " + datasheet + ':') |
| 463 | print(str(e)) |
| 464 | return |
| 465 | else: |
| 466 | # 'request-hash' set to '.' for local simulation |
| 467 | datatop['request-hash'] = '.' |
| 468 | try: |
| 469 | dsheet = datatop['data-sheet'] |
| 470 | except KeyError: |
| 471 | print("Error: JSON file is not a datasheet!\n") |
| 472 | else: |
| 473 | self.datatop = datatop |
| 474 | self.cur_datasheet = datasheet |
| 475 | self.create_datasheet_view() |
| 476 | self.toppane.title2_frame.datasheet_select.configure(text=dsname) |
| 477 | self.toppane.title2_frame.path_label.configure(text=datasheet) |
| 478 | |
| 479 | # Determine if there is a saved, annotated JSON file that is |
| 480 | # more recent than the netlist used for simulation. |
| 481 | self.load_results() |
| 482 | |
| 483 | # Attempt to set the datasheet viewer width to the interior width |
| 484 | # but do not set it larger than the available desktop. |
| 485 | self.update_idletasks() |
| 486 | widthnow = self.datasheet_viewer.winfo_width() |
| 487 | width = self.datasheet_viewer.dframe.winfo_width() |
| 488 | screen_width = self.root.winfo_screenwidth() |
| 489 | if width > widthnow: |
| 490 | if width < screen_width - 10: |
| 491 | self.datasheet_viewer.configure(width=width) |
| 492 | else: |
| 493 | self.datasheet_viewer.configure(width=screen_width - 10) |
| 494 | elif widthnow > screen_width: |
| 495 | self.datasheet_viewer.configure(width=screen_width - 10) |
| 496 | elif widthnow > width: |
| 497 | self.datasheet_viewer.configure(width=width) |
| 498 | |
| 499 | # Likewise for the height, up to 3/5 of the desktop height. |
| 500 | height = self.datasheet_viewer.dframe.winfo_height() |
| 501 | heightnow = self.datasheet_viewer.winfo_height() |
| 502 | screen_height = self.root.winfo_screenheight() |
| 503 | if height > heightnow: |
| 504 | if height < screen_height * 0.6: |
| 505 | self.datasheet_viewer.configure(height=height) |
| 506 | else: |
| 507 | self.datasheet_viewer.configure(height=screen_height * 0.6) |
| 508 | elif heightnow > screen_height: |
| 509 | self.datasheet_viewer.configure(height=screen_height - 10) |
| 510 | elif heightnow > height: |
| 511 | self.datasheet_viewer.configure(height=height) |
| 512 | |
| 513 | def choose_datasheet(self): |
| 514 | datasheet = filedialog.askopenfilename(multiple = False, |
| 515 | initialdir = os.path.expanduser('~/design'), |
| 516 | filetypes = (("JSON File", "*.json"),("All Files","*.*")), |
| 517 | title = "Find a datasheet.") |
| 518 | if datasheet != '': |
| 519 | self.set_datasheet(datasheet) |
| 520 | |
| 521 | def cancel_upload(self): |
| 522 | # Post a cancelation message to CACE. CACE responds by setting the |
| 523 | # status to 'canceled'. The watchprogress procedure is responsible for |
| 524 | # returning the button to 'Submit' when the characterization finishes |
| 525 | # or is canceled. |
| 526 | dspath = os.path.split(self.cur_datasheet)[0] |
| 527 | datasheet = os.path.split(self.cur_datasheet)[1] |
| 528 | designname = os.path.splitext(datasheet)[0] |
| 529 | print('Cancel characterization of ' + designname + ' (' + dspath + ' )') |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 530 | subprocess.run([apps_path + '/cace_design_upload.py', '-cancel', |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 531 | dspath]) |
| 532 | self.removeprogress() |
| 533 | self.bbar.upload_button.configure(text='Submit', state = 'enabled', |
| 534 | command=self.upload_to_marketplace, |
| 535 | style = 'normal.TButton') |
| 536 | # Delete the remote status file. |
| 537 | dsdir = dspath + '/ngspice/char' |
| 538 | statusname = dsdir + '/remote_status.json' |
| 539 | if os.path.exists(statusname): |
| 540 | os.remove(statusname) |
| 541 | |
| 542 | def progress_bar_setup(self, dspath): |
| 543 | # Create the progress bar at the bottom of the window to indicate |
| 544 | # the status of a challenge submission. |
| 545 | |
| 546 | # Disable the Submit button |
| 547 | self.bbar.upload_button.configure(state='disabled') |
| 548 | |
| 549 | # Start progress bar watchclock |
| 550 | dsdir = dspath + '/ngspice/char' |
| 551 | statusname = dsdir + '/remote_status.json' |
| 552 | if os.path.exists(statusname): |
| 553 | statbuf = os.stat(statusname) |
| 554 | mtime = statbuf.st_mtime |
| 555 | else: |
| 556 | if os.path.exists(dsdir): |
| 557 | # Write a simple status |
| 558 | status = {'message': 'not started', 'total': '0', 'completed': '0'} |
| 559 | with open(statusname, 'w') as f: |
| 560 | json.dump(status, f) |
| 561 | mtime = 0 |
| 562 | # Create a TTK progress bar widget in the buttonbar. |
| 563 | self.bbar.progress_label = ttk.Label(self.bbar, text="Characterization: ", |
| 564 | style = 'normal.TLabel') |
| 565 | self.bbar.progress_label.grid(column=4, row=0, ipadx = 5) |
| 566 | |
| 567 | self.bbar.progress_message = ttk.Label(self.bbar, text="(not started)", |
| 568 | style = 'blue.TLabel') |
| 569 | self.bbar.progress_message.grid(column=5, row=0, ipadx = 5) |
| 570 | self.bbar.progress = ttk.Progressbar(self.bbar, |
| 571 | orient='horizontal', mode='determinate') |
| 572 | self.bbar.progress.grid(column = 6, row = 0, padx = 5, sticky = 'nsew') |
| 573 | self.bbar.progress_text = ttk.Label(self.bbar, text="0/0", |
| 574 | style = 'blue.TLabel') |
| 575 | self.bbar.progress_text.grid(column=7, row=0, ipadx = 5) |
| 576 | |
| 577 | # Start the timer to watch the progress |
| 578 | self.watchprogress(statusname, mtime, 1) |
| 579 | |
| 580 | def check_ongoing_upload(self): |
| 581 | # Determine if an upload is ongoing when the characterization tool is |
| 582 | # started. If so, immediately go to the 'characterization running' |
| 583 | # state with progress bar. |
| 584 | dspath = os.path.split(self.cur_datasheet)[0] |
| 585 | datasheet = os.path.split(self.cur_datasheet)[1] |
| 586 | designname = os.path.splitext(datasheet)[0] |
| 587 | dsdir = dspath + '/ngspice/char' |
| 588 | statusname = dsdir + '/remote_status.json' |
| 589 | if os.path.exists(statusname): |
| 590 | with open(statusname, 'r') as f: |
| 591 | status = json.load(f) |
| 592 | if 'message' in status: |
| 593 | if status['message'] == 'in progress': |
| 594 | print('Design characterization in progress for ' + designname + ' (' + dspath + ' )') |
| 595 | self.progress_bar_setup(dspath) |
| 596 | else: |
| 597 | print("No message in status file") |
| 598 | |
| 599 | def upload_to_marketplace(self): |
| 600 | dspath = os.path.split(self.cur_datasheet)[0] |
| 601 | datasheet = os.path.split(self.cur_datasheet)[1] |
| 602 | dsheet = self.datatop['data-sheet'] |
| 603 | designname = dsheet['ip-name'] |
| 604 | |
| 605 | # Make sure a netlist has been generated. |
| 606 | if self.sim_param('check') == False: |
| 607 | print('No netlist was generated, cannot submit!') |
| 608 | return |
| 609 | |
| 610 | # For diagnostic purposes, place all of the characterization tool |
| 611 | # settings into datatop['settings'] when uploading to remote CACE. |
| 612 | runtime_settings = {} |
| 613 | runtime_settings['force-regenerate'] = self.settings.get_force() |
| 614 | runtime_settings['edit-all-params'] = self.settings.get_edit() |
| 615 | runtime_settings['keep-files'] = self.settings.get_keep() |
| 616 | runtime_settings['make-plots'] = self.settings.get_plot() |
| 617 | runtime_settings['submit-test-mode'] = self.settings.get_test() |
| 618 | runtime_settings['submit-as-schematic'] = self.settings.get_schem() |
| 619 | runtime_settings['submit-failing'] = self.settings.get_submitfailed() |
| 620 | runtime_settings['log-output'] = self.settings.get_log() |
| 621 | |
| 622 | # Write out runtime settings as a JSON file |
| 623 | with open(dspath + '/settings.json', 'w') as file: |
| 624 | json.dump(runtime_settings, file, indent = 4) |
| 625 | |
| 626 | warning = '' |
| 627 | must_confirm = False |
| 628 | if self.settings.get_schem() == True: |
| 629 | # If a layout exists but "submit as schematic" was chosen, then |
| 630 | # flag a warning and insist on confirmation. |
| 631 | if os.path.exists(dspath + '/mag/' + designname + '.mag'): |
| 632 | warning += 'Warning: layout exists but only schematic has been selected for submission' |
| 633 | must_confirm = True |
| 634 | else: |
| 635 | print('No layout in ' + dspath + '/mag/' + designname + '.mag') |
| 636 | print('Schematic only submission selection is not needed.') |
| 637 | else: |
| 638 | # Likewise, check if schematic netlist results are showing but a layout |
| 639 | # exists, which means that the existing results are not the ones that are |
| 640 | # going to be tested. |
| 641 | if self.origin.get() == 'Schematic Capture': |
| 642 | if os.path.exists(dspath + '/mag/' + designname + '.mag'): |
| 643 | warning += 'Warning: schematic results are shown but remote CACE will be run on layout results.' |
| 644 | must_confirm = True |
| 645 | |
| 646 | |
| 647 | # Make a check to see if all simulations have been made and passed. If so, |
| 648 | # then just do the upload. If not, then generate a warning dialog and |
| 649 | # require the user to respond to force an upload in spite of an incomplete |
| 650 | # simulation. Give dire warnings if any simulation has failed. |
| 651 | |
| 652 | failures = 0 |
| 653 | missed = 0 |
| 654 | for param in dsheet['electrical-params']: |
| 655 | if 'max' in param: |
| 656 | pmax = param['max'] |
| 657 | if not 'value' in pmax: |
| 658 | missed += 1 |
| 659 | elif 'score' in pmax: |
| 660 | if pmax['score'] == 'fail': |
| 661 | failures += 1 |
| 662 | if 'min' in param: |
| 663 | pmin = param['min'] |
| 664 | if not 'value' in pmin: |
| 665 | missed += 1 |
| 666 | elif 'score' in pmin: |
| 667 | if pmin['score'] == 'fail': |
| 668 | failures += 1 |
| 669 | |
| 670 | if missed > 0: |
| 671 | if must_confirm == True: |
| 672 | warning += '\n' |
| 673 | warning += 'Warning: Not all critical parameters have been simulated.' |
| 674 | if missed > 0 and failures > 0: |
| 675 | warning += '\n' |
| 676 | if failures > 0: |
| 677 | warning += 'Dire Warning: This design has errors on critical parameters!' |
| 678 | |
| 679 | # Require confirmation |
| 680 | if missed > 0 or failures > 0: |
| 681 | must_confirm = True |
| 682 | |
| 683 | if must_confirm: |
| 684 | if self.settings.get_submitfailed() == True: |
| 685 | confirm = ConfirmDialog(self, warning).result |
| 686 | else: |
| 687 | confirm = PuntDialog(self, warning).result |
| 688 | if not confirm == 'okay': |
| 689 | print('Upload canceled.') |
| 690 | return |
| 691 | print('Upload selected') |
| 692 | |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 693 | # Save hints in file in spice/ directory. |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 694 | hintlist = [] |
| 695 | for eparam in dsheet['electrical-params']: |
| 696 | if not 'editable' in eparam: |
| 697 | if 'hints' in eparam: |
| 698 | hintlist.append(eparam['hints']) |
| 699 | else: |
| 700 | # Must have a placeholder |
| 701 | hintlist.append({}) |
| 702 | if hintlist: |
| 703 | hfilename = dspath + '/hints.json' |
| 704 | with open(hfilename, 'w') as hfile: |
| 705 | json.dump(hintlist, hfile, indent = 4) |
| 706 | |
| 707 | print('Uploading design ' + designname + ' (' + dspath + ' )') |
| 708 | print('to marketplace and submitting for characterization.') |
| 709 | if not self.settings.get_test(): |
| 710 | self.progress_bar_setup(dspath) |
| 711 | self.update_idletasks() |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 712 | subprocess.run([apps_path + '/cace_design_upload.py', dspath]) |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 713 | |
| 714 | # Remove the settings file |
| 715 | os.remove(dspath + '/settings.json') |
| 716 | os.remove(dspath + '/hints.json') |
| 717 | |
| 718 | def removeprogress(self): |
| 719 | # Remove the progress bar. This is left up for a second after |
| 720 | # completion or cancelation so that the final message has time |
| 721 | # to be seen. |
| 722 | try: |
| 723 | self.bbar.progress_label.destroy() |
| 724 | self.bbar.progress_message.destroy() |
| 725 | self.bbar.progress.destroy() |
| 726 | self.bbar.progress_text.destroy() |
| 727 | except: |
| 728 | pass |
| 729 | |
| 730 | def watchprogress(self, filename, filemtime, timeout): |
| 731 | new_timeout = timeout + 1 if timeout > 0 else 0 |
| 732 | # 2 minute timeout for startup (note that all simulation files have to be |
| 733 | # made during this period. |
| 734 | if new_timeout == 120: |
| 735 | self.cancel_upload() |
| 736 | return |
| 737 | |
| 738 | # If file does not exist, then keep checking at 2 second intervals. |
| 739 | if not os.path.exists(filename): |
| 740 | self.after(2000, lambda: self.watchprogress(filename, filemtime, new_timeout)) |
| 741 | return |
| 742 | |
| 743 | # If filename file is modified, then update progress bar; |
| 744 | # otherwise, restart the clock. |
| 745 | statbuf = os.stat(filename) |
| 746 | if statbuf.st_mtime > filemtime: |
| 747 | self.after(250) # Otherwise can catch file while it's incomplete. . . |
| 748 | if self.update_progress(filename) == True: |
| 749 | self.after(1000, lambda: self.watchprogress(filename, filemtime, 0)) |
| 750 | else: |
| 751 | # Remove the progress bar when done, after letting the final |
| 752 | # message display for a second. |
| 753 | self.after(1500, self.removeprogress) |
| 754 | # And return the button to "Submit" and in an enabled state. |
| 755 | self.bbar.upload_button.configure(text='Submit', state = 'enabled', |
| 756 | command=self.upload_to_marketplace, |
| 757 | style = 'normal.TButton') |
| 758 | else: |
| 759 | self.after(1000, lambda: self.watchprogress(filename, filemtime, new_timeout)) |
| 760 | |
| 761 | def update_progress(self, filename): |
| 762 | # On first update, button changes from "Submit" to "Cancel" |
| 763 | # This ensures that the 'remote_status.json' file has been sent |
| 764 | # from the CACE with the hash value needed for the CACE to identify |
| 765 | # the right simulation and cancel it. |
| 766 | if self.bbar.upload_button.configure('text')[-1] == 'Submit': |
| 767 | self.bbar.upload_button.configure(text='Cancel', state = 'enabled', |
| 768 | command=self.cancel_upload, style = 'red.TButton') |
| 769 | |
| 770 | if not os.path.exists(filename): |
| 771 | return False |
| 772 | |
| 773 | # Update the progress bar during an CACE simulation run. |
| 774 | # Read the status file |
| 775 | try: |
| 776 | with open(filename, 'r') as f: |
| 777 | status = json.load(f) |
| 778 | except (PermissionError, FileNotFoundError): |
| 779 | # For a very short time the user does not have ownership of |
| 780 | # the file and the read will fail. This is a rare case, so |
| 781 | # just punt until the next cycle. |
| 782 | return True |
| 783 | |
| 784 | if 'message' in status: |
| 785 | self.bbar.progress_message.configure(text = status['message']) |
| 786 | |
| 787 | try: |
| 788 | total = int(status['total']) |
| 789 | except: |
| 790 | total = 0 |
| 791 | else: |
| 792 | self.bbar.progress.configure(maximum = total) |
| 793 | |
| 794 | try: |
| 795 | completed = int(status['completed']) |
| 796 | except: |
| 797 | completed = 0 |
| 798 | else: |
| 799 | self.bbar.progress.configure(value = completed) |
| 800 | |
| 801 | self.bbar.progress_text.configure(text = str(completed) + '/' + str(total)) |
| 802 | if completed > 0 and completed == total: |
| 803 | print('Notice: Design completed.') |
| 804 | print('The CACE server has finished characterizing the design.') |
| 805 | print('Go to the efabless marketplace to view submission.') |
| 806 | return False |
| 807 | elif status['message'] == 'canceled': |
| 808 | print('Notice: Design characterization was canceled.') |
| 809 | return False |
| 810 | else: |
| 811 | return True |
| 812 | |
| 813 | def topfilter(self, line): |
| 814 | # Check output for ubiquitous "Reference value" lines and remove them. |
| 815 | # This happens before logging both to the file and to the console. |
| 816 | refrex = re.compile('Reference value') |
| 817 | rmatch = refrex.match(line) |
| 818 | if not rmatch: |
| 819 | return line |
| 820 | else: |
| 821 | return None |
| 822 | |
| 823 | def spicefilter(self, line): |
| 824 | # Check for the alarmist 'tran simulation interrupted' message and remove it. |
| 825 | # Check for error or warning and print as stderr or stdout accordingly. |
| 826 | intrex = re.compile('tran simulation interrupted') |
| 827 | warnrex = re.compile('.*warning', re.IGNORECASE) |
| 828 | errrex = re.compile('.*error', re.IGNORECASE) |
| 829 | |
| 830 | imatch = intrex.match(line) |
| 831 | if not imatch: |
| 832 | ematch = errrex.match(line) |
| 833 | wmatch = warnrex.match(line) |
| 834 | if ematch or wmatch: |
| 835 | print(line, file=sys.stderr) |
| 836 | else: |
| 837 | print(line, file=sys.stdout) |
| 838 | |
| 839 | def printwarn(self, output): |
| 840 | # Check output for warning or error |
| 841 | if not output: |
| 842 | return 0 |
| 843 | |
| 844 | warnrex = re.compile('.*warning', re.IGNORECASE) |
| 845 | errrex = re.compile('.*error', re.IGNORECASE) |
| 846 | |
| 847 | errors = 0 |
| 848 | outlines = output.splitlines() |
| 849 | for line in outlines: |
| 850 | try: |
| 851 | wmatch = warnrex.match(line) |
| 852 | except TypeError: |
| 853 | line = line.decode('utf-8') |
| 854 | wmatch = warnrex.match(line) |
| 855 | ematch = errrex.match(line) |
| 856 | if ematch: |
| 857 | errors += 1 |
| 858 | if ematch or wmatch: |
| 859 | print(line) |
| 860 | return errors |
| 861 | |
| 862 | def sim_all(self): |
| 863 | if self.caceproc: |
| 864 | # Failsafe |
| 865 | if self.caceproc.poll() != None: |
| 866 | self.caceproc = None |
| 867 | else: |
| 868 | print('Simulation in progress must finish first.') |
| 869 | return |
| 870 | |
| 871 | # Create netlist if necessary, check for valid result |
| 872 | if self.sim_param('check') == False: |
| 873 | return |
| 874 | |
| 875 | # Simulate all of the electrical parameters in turn |
| 876 | self.sims_to_go = [] |
| 877 | for puniq in self.status: |
| 878 | self.sims_to_go.append(puniq) |
| 879 | |
| 880 | # Start first sim |
| 881 | if len(self.sims_to_go) > 0: |
| 882 | puniq = self.sims_to_go[0] |
| 883 | self.sims_to_go = self.sims_to_go[1:] |
| 884 | self.sim_param(puniq) |
| 885 | |
| 886 | # Button now stops the simulations |
| 887 | self.allsimbutton.configure(style = 'redtitle.TButton', text='Stop Simulations', |
| 888 | command=self.stop_sims) |
| 889 | |
| 890 | def stop_sims(self): |
| 891 | # Make sure there will be no more simulations |
| 892 | self.sims_to_go = [] |
| 893 | if not self.caceproc: |
| 894 | print("No simulation running.") |
| 895 | return |
| 896 | self.caceproc.terminate() |
| 897 | # Use communicate(), not wait() , on piped processes to avoid deadlock. |
| 898 | try: |
| 899 | self.caceproc.communicate(timeout=10) |
| 900 | except subprocess.TimeoutExpired: |
| 901 | self.caceproc.kill() |
| 902 | self.caceproc.communicate() |
| 903 | print("CACE process killed.") |
| 904 | else: |
| 905 | print("CACE process exited.") |
| 906 | # Let watchdog timer see that caceproc is gone and reset the button. |
| 907 | |
| 908 | def edit_param(self, param): |
| 909 | # Edit the conditions under which the parameter is tested. |
| 910 | if ('editable' in param and param['editable'] == True) or self.settings.get_edit() == True: |
| 911 | self.editparam.populate(param) |
| 912 | self.editparam.open() |
| 913 | else: |
| 914 | print('Parameter is not editable') |
| 915 | |
| 916 | def copy_param(self, param): |
| 917 | # Make a copy of the parameter (for editing) |
| 918 | newparam = param.copy() |
| 919 | # Make the copied parameter editable |
| 920 | newparam['editable'] = True |
| 921 | # Append this to the electrical parameter list after the item being copied |
| 922 | if 'display' in param: |
| 923 | newparam['display'] = param['display'] + ' (copy)' |
| 924 | datatop = self.datatop |
| 925 | dsheet = datatop['data-sheet'] |
| 926 | eparams = dsheet['electrical-params'] |
| 927 | eidx = eparams.index(param) |
| 928 | eparams.insert(eidx + 1, newparam) |
| 929 | self.create_datasheet_view() |
| 930 | |
| 931 | def delete_param(self, param): |
| 932 | # Remove an electrical parameter from the datasheet. This is only |
| 933 | # allowed if the parameter has been copied from another and so does |
| 934 | # not belong to the original set of parameters. |
| 935 | datatop = self.datatop |
| 936 | dsheet = datatop['data-sheet'] |
| 937 | eparams = dsheet['electrical-params'] |
| 938 | eidx = eparams.index(param) |
| 939 | eparams.pop(eidx) |
| 940 | self.create_datasheet_view() |
| 941 | |
| 942 | def add_hints(self, param, simbutton): |
| 943 | # Raise hints window and configure appropriately for the parameter. |
| 944 | # Fill in any existing hints. |
| 945 | self.simhints.populate(param, simbutton) |
| 946 | self.simhints.open() |
| 947 | |
| 948 | def sim_param(self, method): |
| 949 | if self.caceproc: |
| 950 | # Failsafe |
| 951 | if self.caceproc.poll() != None: |
| 952 | self.caceproc = None |
| 953 | else: |
| 954 | print('Simulation in progress, queued for simulation.') |
| 955 | if not method in self.sims_to_go: |
| 956 | self.sims_to_go.append(method) |
| 957 | return False |
| 958 | |
| 959 | # Get basic values for datasheet and ip-name |
| 960 | |
| 961 | dspath = os.path.split(self.cur_datasheet)[0] |
| 962 | dsheet = self.datatop['data-sheet'] |
| 963 | dname = dsheet['ip-name'] |
| 964 | |
| 965 | # Open log file, if specified |
| 966 | self.logstart() |
| 967 | |
| 968 | # Check for whether the netlist is specified to come from schematic |
| 969 | # or layout. Add a record to the datasheet depending on whether |
| 970 | # the netlist is from layout or extracted. The settings window has |
| 971 | # a checkbox to force submitting as a schematic even if layout exists. |
| 972 | |
| 973 | if self.origin.get() == 'Schematic Capture': |
| 974 | dsheet['netlist-source'] = 'schematic' |
| 975 | else: |
| 976 | dsheet['netlist-source'] = 'layout' |
| 977 | |
| 978 | if self.settings.get_force() == True: |
| 979 | dsheet['regenerate'] = 'force' |
| 980 | |
| 981 | basemethod = method.split('.')[0] |
| 982 | if basemethod == 'check': # used by submit to ensure netlist exists |
| 983 | return True |
| 984 | |
| 985 | if basemethod == 'physical': |
| 986 | print('Checking ' + method.split('.')[1]) |
| 987 | else: |
| 988 | print('Simulating method = ' + basemethod) |
| 989 | self.stat_label = self.status[method] |
| 990 | self.stat_label.configure(text='(in progress)', style='blue.TLabel') |
| 991 | # Update status now |
| 992 | self.update_idletasks() |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 993 | |
| 994 | if dspath == '': |
| 995 | dspath = '.' |
| 996 | |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 997 | print('Datasheet directory is = ' + dspath + '\n') |
| 998 | |
| 999 | # Instead of using the original datasheet, use the one in memory so that |
| 1000 | # it accumulates results. A "save" button will update the original. |
| 1001 | if not os.path.isdir(dspath + '/ngspice'): |
| 1002 | os.makedirs(dspath + '/ngspice') |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1003 | dsdir = dspath + '/ngspice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1004 | if not os.path.isdir(dsdir): |
| 1005 | os.makedirs(dsdir) |
| 1006 | with open(dsdir + '/datasheet.json', 'w') as file: |
| 1007 | json.dump(self.datatop, file, indent = 4) |
| 1008 | # As soon as we call CACE, we will be watching the status of file |
| 1009 | # datasheet_anno. So create it if it does not exist, else attempting |
| 1010 | # to stat a nonexistant file will cause the 1st simulation to fail. |
| 1011 | if not os.path.exists(dsdir + '/datasheet_anno.json'): |
| 1012 | open(dsdir + '/datasheet_anno.json', 'a').close() |
| 1013 | # Call cace_gensim with full set of options |
| 1014 | # First argument is the root directory |
| 1015 | # (Diagnostic) |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1016 | design_path = dspath + '/spice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1017 | |
| 1018 | print('Calling cace_gensim.py ' + dspath + |
| 1019 | ' -local -method=' + method) |
| 1020 | |
| 1021 | modetext = ['-local'] |
| 1022 | if self.settings.get_keep() == True: |
| 1023 | print(' -keep ') |
| 1024 | modetext.append('-keep') |
| 1025 | |
| 1026 | if self.settings.get_plot() == True: |
| 1027 | print(' -plot ') |
| 1028 | modetext.append('-plot') |
| 1029 | |
| 1030 | print(' -simdir=' + dsdir + ' -datasheetdir=' + dsdir + ' -designdir=' + design_path) |
| 1031 | print(' -layoutdir=' + dspath + '/mag' + ' -testbenchdir=' + dspath + '/testbench') |
| 1032 | print(' -datasheet=datasheet.json') |
| 1033 | |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1034 | self.caceproc = subprocess.Popen([apps_path + '/cace_gensim.py', dspath, |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1035 | *modetext, |
| 1036 | '-method=' + method, # Call local mode w/method |
| 1037 | '-simdir=' + dsdir, |
| 1038 | '-datasheetdir=' + dsdir, |
| 1039 | '-designdir=' + design_path, |
| 1040 | '-layoutdir=' + dspath + '/mag', |
| 1041 | '-testbenchdir=' + dspath + '/testbench', |
| 1042 | '-datasheet=datasheet.json'], |
| 1043 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) |
| 1044 | |
| 1045 | # Simulation finishes on its own time. Use watchdog to handle. |
| 1046 | # Note that python "watchdog" is threaded, and tkinter is not thread-safe. |
| 1047 | # So watchdog is done with a simple timer loop. |
| 1048 | statbuf = os.stat(dsdir + '/datasheet.json') |
| 1049 | checktime = statbuf.st_mtime |
| 1050 | |
| 1051 | filename = dsdir + '/datasheet_anno.json' |
| 1052 | statbuf = os.stat(filename) |
| 1053 | self.watchclock(filename, statbuf.st_mtime, checktime) |
| 1054 | |
| 1055 | def watchclock(self, filename, filemtime, checktime): |
| 1056 | # In case simulations cleared while watchclock was pending |
| 1057 | if self.caceproc == None: |
| 1058 | return |
| 1059 | # Poll cace_gensim to see if it finished |
| 1060 | cace_status = self.caceproc.poll() |
| 1061 | if cace_status != None: |
| 1062 | try: |
| 1063 | output = self.caceproc.communicate(timeout=1) |
| 1064 | except ValueError: |
| 1065 | print("CACE gensim forced stop, status " + str(cace_status)) |
| 1066 | else: |
| 1067 | outlines = output[0] |
| 1068 | errlines = output[1] |
| 1069 | for line in outlines.splitlines(): |
| 1070 | print(line.decode('utf-8')) |
| 1071 | for line in errlines.splitlines(): |
| 1072 | print(line.decode('utf-8')) |
| 1073 | print("CACE gensim exited with status " + str(cace_status)) |
| 1074 | else: |
| 1075 | n = 0 |
| 1076 | while True: |
| 1077 | self.update_idletasks() |
| 1078 | # Attempt to avoid infinite loop, unsure of the cause. |
| 1079 | n += 1 |
| 1080 | if n > 100: |
| 1081 | n = 0 |
| 1082 | cace_status = self.caceproc.poll() |
| 1083 | if cace_status != None: |
| 1084 | break |
| 1085 | self.logprint("100 lines of output", doflush=True) |
| 1086 | # Something went wrong. Kill the process. |
| 1087 | # self.stop_sims() |
| 1088 | sresult = select.select([self.caceproc.stdout, self.caceproc.stderr], [], [], 0)[0] |
| 1089 | if self.caceproc.stdout in sresult: |
| 1090 | outstring = self.caceproc.stdout.readline().decode().strip() |
| 1091 | self.logprint(outstring, doflush=True) |
| 1092 | print(outstring) |
| 1093 | elif self.caceproc.stderr in sresult: |
| 1094 | # ngspice passes back simulation time on stderr. This ends in \r but no |
| 1095 | # newline. '\r' ends the transmission, so return. |
| 1096 | # errstring = self.topfilter(self.caceproc.stderr.readline().decode().strip()) |
| 1097 | # if errstring: |
| 1098 | # self.logprint(errstring, doflush=True) |
| 1099 | # # Recast everything that isn't an error back into stdout. |
| 1100 | # self.spicefilter(errstring) |
| 1101 | ochar = str(self.caceproc.stderr.read(1).decode()) |
| 1102 | if ochar == '\r': |
| 1103 | print('') |
| 1104 | break |
| 1105 | else: |
| 1106 | print(ochar, end='') |
| 1107 | else: |
| 1108 | break |
| 1109 | |
| 1110 | # If filename file is modified, then call annotate; otherwise, restart the clock. |
| 1111 | statbuf = os.stat(filename) |
| 1112 | if (statbuf.st_mtime > filemtime) or (cace_status != None): |
| 1113 | if cace_status != None: |
| 1114 | self.caceproc = None |
| 1115 | else: |
| 1116 | # Re-run to catch last output. |
| 1117 | self.after(500, lambda: self.watchclock(filename, statbuf.st_mtime, checktime)) |
| 1118 | return |
| 1119 | if cace_status != 0: |
| 1120 | print('Errors encountered in simulation.') |
| 1121 | self.logprint('Errors in simulation, CACE status = ' + str(cace_status), doflush=True) |
| 1122 | self.annotate('anno', checktime) |
| 1123 | if len(self.sims_to_go) > 0: |
| 1124 | puniq = self.sims_to_go[0] |
| 1125 | self.sims_to_go = self.sims_to_go[1:] |
| 1126 | self.sim_param(puniq) |
| 1127 | else: |
| 1128 | # Button goes back to original text and command |
| 1129 | self.allsimbutton.configure(style = 'bluetitle.TButton', |
| 1130 | text='Simulate All', command = self.sim_all) |
| 1131 | elif not self.caceproc: |
| 1132 | # Process terminated by "stop" |
| 1133 | # Button goes back to original text and command |
| 1134 | self.allsimbutton.configure(style = 'bluetitle.TButton', |
| 1135 | text='Simulate All', command = self.sim_all) |
| 1136 | # Just redraw everthing so that the "(in progress)" message goes away. |
| 1137 | self.annotate('anno', checktime) |
| 1138 | else: |
| 1139 | self.after(500, lambda: self.watchclock(filename, filemtime, checktime)) |
| 1140 | |
| 1141 | def clear_results(self, dsheet): |
| 1142 | # Remove results from the window by clearing parameter results |
| 1143 | paramstodo = [] |
| 1144 | if 'electrical-params' in dsheet: |
| 1145 | paramstodo.extend(dsheet['electrical-params']) |
| 1146 | if 'physical-params' in dsheet: |
| 1147 | paramstodo.extend(dsheet['physical-params']) |
| 1148 | |
| 1149 | for param in paramstodo: |
| 1150 | # Fill frame with electrical parameter information |
| 1151 | if 'max' in param: |
| 1152 | maxrec = param['max'] |
| 1153 | if 'value' in maxrec: |
| 1154 | maxrec.pop('value') |
| 1155 | if 'score' in maxrec: |
| 1156 | maxrec.pop('score') |
| 1157 | if 'typ' in param: |
| 1158 | typrec = param['typ'] |
| 1159 | if 'value' in typrec: |
| 1160 | typrec.pop('value') |
| 1161 | if 'score' in typrec: |
| 1162 | typrec.pop('score') |
| 1163 | if 'min' in param: |
| 1164 | minrec = param['min'] |
| 1165 | if 'value' in minrec: |
| 1166 | minrec.pop('value') |
| 1167 | if 'score' in minrec: |
| 1168 | minrec.pop('score') |
| 1169 | if 'results' in param: |
| 1170 | param.pop('results') |
| 1171 | |
| 1172 | if 'plot' in param: |
| 1173 | plotrec = param['plot'] |
| 1174 | if 'status' in plotrec: |
| 1175 | plotrec.pop('status') |
| 1176 | |
| 1177 | # Regenerate datasheet view |
| 1178 | self.create_datasheet_view() |
| 1179 | |
| 1180 | def annotate(self, suffix, checktime): |
| 1181 | # Pull results back from datasheet_anno.json. Do NOT load this |
| 1182 | # file if it predates the unannotated datasheet (that indicates |
| 1183 | # simulator failure, and no results). |
| 1184 | dspath = os.path.split(self.cur_datasheet)[0] |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1185 | if dspath == '': |
| 1186 | dspath = '.' |
| 1187 | dsdir = dspath + '/ngspice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1188 | anno = dsdir + '/datasheet_' + suffix + '.json' |
| 1189 | unanno = dsdir + '/datasheet.json' |
| 1190 | |
| 1191 | if os.path.exists(anno): |
| 1192 | statbuf = os.stat(anno) |
| 1193 | mtimea = statbuf.st_mtime |
| 1194 | if checktime >= mtimea: |
| 1195 | # print('original = ' + str(checktime) + ' annotated = ' + str(mtimea)) |
| 1196 | print('Error in simulation, no update to results.', file=sys.stderr) |
| 1197 | elif statbuf.st_size == 0: |
| 1198 | print('Error in simulation, no results.', file=sys.stderr) |
| 1199 | else: |
| 1200 | with open(anno, 'r') as file: |
| 1201 | self.datatop = json.load(file) |
| 1202 | else: |
| 1203 | print('Error in simulation, no update to results.', file=sys.stderr) |
| 1204 | |
| 1205 | # Regenerate datasheet view |
| 1206 | self.create_datasheet_view() |
| 1207 | |
| 1208 | # Close log file, if it was enabled in the settings |
| 1209 | self.logstop() |
| 1210 | |
| 1211 | def save_results(self): |
| 1212 | # Write datasheet_save with all the locally processed results. |
| 1213 | dspath = os.path.split(self.cur_datasheet)[0] |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1214 | dsdir = dspath + '/ngspice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1215 | |
| 1216 | if self.origin.get() == 'Layout Extracted': |
| 1217 | jsonfile = dsdir + '/datasheet_lsave.json' |
| 1218 | else: |
| 1219 | jsonfile = dsdir + '/datasheet_save.json' |
| 1220 | |
| 1221 | with open(jsonfile, 'w') as ofile: |
| 1222 | json.dump(self.datatop, ofile, indent = 4) |
| 1223 | self.last_save = os.path.getmtime(jsonfile) |
| 1224 | |
| 1225 | # Create copy of datasheet without result data. This is |
| 1226 | # the file appropriate to insert into the IP catalog |
| 1227 | # metadata JSON file. |
| 1228 | |
| 1229 | datacopy = copy.copy(self.datatop) |
| 1230 | dsheet = datacopy['data-sheet'] |
| 1231 | if 'electrical-params' in dsheet: |
| 1232 | for eparam in dsheet['electrical-params']: |
| 1233 | if 'results' in eparam: |
| 1234 | eparam.pop('results') |
| 1235 | |
| 1236 | datacopy.pop('request-hash') |
| 1237 | jsonfile = dsdir + '/datasheet_compact.json' |
| 1238 | with open(jsonfile, 'w') as ofile: |
| 1239 | json.dump(datacopy, ofile, indent = 4) |
| 1240 | |
| 1241 | print('Characterization results saved.') |
| 1242 | |
| 1243 | def check_saved(self): |
| 1244 | # Check if there is a file 'datasheet_save' and if it is more |
| 1245 | # recent than 'datasheet_anno'. If so, return True, else False. |
| 1246 | |
| 1247 | [dspath, dsname] = os.path.split(self.cur_datasheet) |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1248 | dsdir = dspath + '/ngspice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1249 | |
| 1250 | if self.origin.get() == 'Layout Extracted': |
| 1251 | savefile = dsdir + '/datasheet_lsave.json' |
| 1252 | else: |
| 1253 | savefile = dsdir + '/datasheet_save.json' |
| 1254 | |
| 1255 | annofile = dsdir + '/datasheet_anno.json' |
| 1256 | if os.path.exists(annofile): |
| 1257 | annotime = os.path.getmtime(annofile) |
| 1258 | |
| 1259 | # If nothing has been updated since the characterization |
| 1260 | # tool was started, then there is no new information to save. |
| 1261 | if annotime < self.starttime: |
| 1262 | return True |
| 1263 | |
| 1264 | if os.path.exists(savefile): |
| 1265 | savetime = os.path.getmtime(savefile) |
| 1266 | # return True if (savetime > annotime) else False |
| 1267 | if savetime > annotime: |
| 1268 | print("Save is more recent than sim, so no need to save.") |
| 1269 | return True |
| 1270 | else: |
| 1271 | print("Sim is more recent than save, so need to save.") |
| 1272 | return False |
| 1273 | else: |
| 1274 | # There is a datasheet_anno file but no datasheet_save, |
| 1275 | # so there are necessarily unsaved results. |
| 1276 | print("no datasheet_save, so any results have not been saved.") |
| 1277 | return False |
| 1278 | else: |
| 1279 | # There is no datasheet_anno file, so datasheet_save |
| 1280 | # is either current or there have been no simulations. |
| 1281 | print("no datasheet_anno, so there are no results to save.") |
| 1282 | return True |
| 1283 | |
| 1284 | def callback(self): |
| 1285 | # Check for manual load/save-as status from settings window (callback |
| 1286 | # when the settings window is closed). |
| 1287 | if self.settings.get_loadsave() == True: |
| 1288 | self.bbar.saveas_button.grid(column=2, row=0, padx = 5) |
| 1289 | self.bbar.load_button.grid(column=3, row=0, padx = 5) |
| 1290 | else: |
| 1291 | self.bbar.saveas_button.grid_forget() |
| 1292 | self.bbar.load_button.grid_forget() |
| 1293 | |
| 1294 | def save_manual(self, value={}): |
| 1295 | dspath = self.cur_datasheet |
| 1296 | # Set initialdir to the project where cur_datasheet is located |
| 1297 | dsparent = os.path.split(dspath)[0] |
| 1298 | |
| 1299 | datasheet = filedialog.asksaveasfilename(multiple = False, |
| 1300 | initialdir = dsparent, |
| 1301 | confirmoverwrite = True, |
| 1302 | defaultextension = ".json", |
| 1303 | filetypes = (("JSON File", "*.json"),("All Files","*.*")), |
| 1304 | title = "Select filename for saved datasheet.") |
| 1305 | with open(datasheet, 'w') as ofile: |
| 1306 | json.dump(self.datatop, ofile, indent = 4) |
| 1307 | |
| 1308 | def load_manual(self, value={}): |
| 1309 | dspath = self.cur_datasheet |
| 1310 | # Set initialdir to the project where cur_datasheet is located |
| 1311 | dsparent = os.path.split(dspath)[0] |
| 1312 | |
| 1313 | datasheet = filedialog.askopenfilename(multiple = False, |
| 1314 | initialdir = dsparent, |
| 1315 | filetypes = (("JSON File", "*.json"),("All Files","*.*")), |
| 1316 | title = "Find a datasheet.") |
| 1317 | if datasheet != '': |
| 1318 | try: |
| 1319 | with open(datasheet, 'r') as file: |
| 1320 | self.datatop = json.load(file) |
| 1321 | except: |
| 1322 | print('Error in file, no update to results.', file=sys.stderr) |
| 1323 | |
| 1324 | else: |
| 1325 | # Regenerate datasheet view |
| 1326 | self.create_datasheet_view() |
| 1327 | |
| 1328 | def load_results(self, value={}): |
| 1329 | # Check if datasheet_save exists and is more recent than the |
| 1330 | # latest design netlist. If so, load it; otherwise, not. |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1331 | # NOTE: Name of .spice file comes from the project 'ip-name' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1332 | # in the datasheet. |
| 1333 | |
| 1334 | [dspath, dsname] = os.path.split(self.cur_datasheet) |
| 1335 | try: |
| 1336 | dsheet = self.datatop['data-sheet'] |
| 1337 | except KeyError: |
| 1338 | return |
| 1339 | |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1340 | if dspath == '': |
| 1341 | dspath = '.' |
| 1342 | |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1343 | dsroot = dsheet['ip-name'] |
| 1344 | |
| 1345 | # Remove any existing results from the datasheet records |
| 1346 | self.clear_results(dsheet) |
| 1347 | |
| 1348 | # Also must be more recent than datasheet |
| 1349 | jtime = os.path.getmtime(self.cur_datasheet) |
| 1350 | |
| 1351 | # dsroot = os.path.splitext(dsname)[0] |
| 1352 | |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1353 | dsdir = dspath + '/spice' |
| 1354 | |
| 1355 | if not os.path.exists(dsdir): |
| 1356 | print('Error: Cannot find directory spice/ in path ' + dspath) |
| 1357 | |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1358 | if self.origin.get() == 'Layout Extracted': |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1359 | spifile = dsdir + '/pex/' + dsroot + '.spice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1360 | savesuffix = 'lsave' |
| 1361 | else: |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1362 | spifile = dsdir + '/' + dsroot + '.spice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1363 | savesuffix = 'save' |
| 1364 | |
Tim Edwards | 32d012a | 2023-03-07 10:11:32 -0500 | [diff] [blame] | 1365 | dsdir = dspath + '/ngspice' |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1366 | savefile = dsdir + '/datasheet_' + savesuffix + '.json' |
| 1367 | |
| 1368 | if os.path.exists(savefile): |
| 1369 | savetime = os.path.getmtime(savefile) |
| 1370 | |
| 1371 | if os.path.exists(spifile): |
| 1372 | spitime = os.path.getmtime(spifile) |
| 1373 | |
| 1374 | if os.path.exists(savefile): |
| 1375 | if (savetime > spitime and savetime > jtime): |
| 1376 | self.annotate(savesuffix, 0) |
| 1377 | print('Characterization results loaded.') |
| 1378 | # print('(' + savefile + ' timestamp = ' + str(savetime) + '; ' + self.cur_datasheet + ' timestamp = ' + str(jtime)) |
| 1379 | else: |
| 1380 | print('Saved datasheet is out-of-date, not loading') |
| 1381 | else: |
| 1382 | print('Datasheet file ' + savefile) |
| 1383 | print('No saved datasheet file, nothing to pre-load') |
| 1384 | else: |
| 1385 | print('No netlist file ' + spifile + '!') |
| 1386 | |
| 1387 | # Remove outdated datasheet.json and datasheet_anno.json to prevent |
| 1388 | # them from overwriting characterization document entries |
| 1389 | |
| 1390 | if os.path.exists(savefile): |
| 1391 | if savetime < jtime: |
| 1392 | print('Removing outdated save file ' + savefile) |
| 1393 | os.remove(savefile) |
| 1394 | |
| 1395 | savefile = dsdir + '/datasheet_anno.json' |
| 1396 | if os.path.exists(savefile): |
| 1397 | savetime = os.path.getmtime(savefile) |
| 1398 | if savetime < jtime: |
| 1399 | print('Removing outdated results file ' + savefile) |
| 1400 | os.remove(savefile) |
| 1401 | |
| 1402 | savefile = dsdir + '/datasheet.json' |
| 1403 | if os.path.exists(savefile): |
| 1404 | savetime = os.path.getmtime(savefile) |
| 1405 | if savetime < jtime: |
| 1406 | print('Removing outdated results file ' + savefile) |
| 1407 | os.remove(savefile) |
| 1408 | |
| 1409 | def create_datasheet_view(self): |
| 1410 | dframe = self.datasheet_viewer.dframe |
| 1411 | |
| 1412 | # Destroy the existing datasheet frame contents (if any) |
| 1413 | for widget in dframe.winfo_children(): |
| 1414 | widget.destroy() |
| 1415 | self.status = {} # Clear dictionary |
| 1416 | |
| 1417 | dsheet = self.datatop['data-sheet'] |
| 1418 | if 'global-conditions' in dsheet: |
| 1419 | globcond = dsheet['global-conditions'] |
| 1420 | else: |
| 1421 | globcond = [] |
| 1422 | |
| 1423 | # Add basic information at the top |
| 1424 | |
| 1425 | n = 0 |
| 1426 | dframe.cframe = ttk.Frame(dframe) |
| 1427 | dframe.cframe.grid(column = 0, row = n, sticky='ewns', columnspan = 10) |
| 1428 | |
| 1429 | dframe.cframe.plabel = ttk.Label(dframe.cframe, text = 'Project IP name:', |
| 1430 | style = 'italic.TLabel') |
| 1431 | dframe.cframe.plabel.grid(column = 0, row = n, sticky='ewns', ipadx = 5) |
| 1432 | dframe.cframe.pname = ttk.Label(dframe.cframe, text = dsheet['ip-name'], |
| 1433 | style = 'normal.TLabel') |
| 1434 | dframe.cframe.pname.grid(column = 1, row = n, sticky='ewns', ipadx = 5) |
| 1435 | dframe.cframe.fname = ttk.Label(dframe.cframe, text = dsheet['foundry'], |
| 1436 | style = 'normal.TLabel') |
| 1437 | dframe.cframe.fname.grid(column = 2, row = n, sticky='ewns', ipadx = 5) |
| 1438 | dframe.cframe.fname = ttk.Label(dframe.cframe, text = dsheet['node'], |
| 1439 | style = 'normal.TLabel') |
| 1440 | dframe.cframe.fname.grid(column = 3, row = n, sticky='ewns', ipadx = 5) |
| 1441 | if 'decription' in dsheet: |
| 1442 | dframe.cframe.pdesc = ttk.Label(dframe.cframe, text = dsheet['description'], |
| 1443 | style = 'normal.TLabel') |
| 1444 | dframe.cframe.pdesc.grid(column = 4, row = n, sticky='ewns', ipadx = 5) |
| 1445 | |
| 1446 | if 'UID' in self.datatop: |
| 1447 | n += 1 |
| 1448 | dframe.cframe.ulabel = ttk.Label(dframe.cframe, text = 'UID:', |
| 1449 | style = 'italic.TLabel') |
| 1450 | dframe.cframe.ulabel.grid(column = 0, row = n, sticky='ewns', ipadx = 5) |
| 1451 | dframe.cframe.uname = ttk.Label(dframe.cframe, text = self.datatop['UID'], |
| 1452 | style = 'normal.TLabel') |
| 1453 | dframe.cframe.uname.grid(column = 1, row = n, columnspan = 5, sticky='ewns', ipadx = 5) |
| 1454 | |
| 1455 | n = 1 |
| 1456 | ttk.Separator(dframe, orient='horizontal').grid(column=0, row=n, sticky='ewns', columnspan=10) |
| 1457 | |
| 1458 | # Title block |
| 1459 | n += 1 |
| 1460 | dframe.desc_title = ttk.Label(dframe, text = 'Parameter', style = 'title.TLabel') |
| 1461 | dframe.desc_title.grid(column = 0, row = n, sticky='ewns') |
| 1462 | dframe.method_title = ttk.Label(dframe, text = 'Method', style = 'title.TLabel') |
| 1463 | dframe.method_title.grid(column = 1, row = n, sticky='ewns') |
| 1464 | dframe.min_title = ttk.Label(dframe, text = 'Min', style = 'title.TLabel') |
| 1465 | dframe.min_title.grid(column = 2, row = n, sticky='ewns', columnspan = 2) |
| 1466 | dframe.typ_title = ttk.Label(dframe, text = 'Typ', style = 'title.TLabel') |
| 1467 | dframe.typ_title.grid(column = 4, row = n, sticky='ewns', columnspan = 2) |
| 1468 | dframe.max_title = ttk.Label(dframe, text = 'Max', style = 'title.TLabel') |
| 1469 | dframe.max_title.grid(column = 6, row = n, sticky='ewns', columnspan = 2) |
| 1470 | dframe.stat_title = ttk.Label(dframe, text = 'Status', style = 'title.TLabel') |
| 1471 | dframe.stat_title.grid(column = 8, row = n, sticky='ewns') |
| 1472 | |
| 1473 | if not self.sims_to_go: |
| 1474 | self.allsimbutton = ttk.Button(dframe, text='Simulate All', |
| 1475 | style = 'bluetitle.TButton', command = self.sim_all) |
| 1476 | else: |
| 1477 | self.allsimbutton = ttk.Button(dframe, text='Stop Simulations', |
| 1478 | style = 'redtitle.TButton', command = self.stop_sims) |
| 1479 | self.allsimbutton.grid(column = 9, row=n, sticky='ewns') |
| 1480 | |
| 1481 | tooltip.ToolTip(self.allsimbutton, text = "Simulate all electrical parameters") |
| 1482 | |
| 1483 | # Make all columns equally expandable |
| 1484 | for i in range(10): |
| 1485 | dframe.columnconfigure(i, weight = 1) |
| 1486 | |
| 1487 | # Parse the file for electrical parameters |
| 1488 | n += 1 |
| 1489 | binrex = re.compile(r'([0-9]*)\'([bodh])', re.IGNORECASE) |
| 1490 | paramstodo = [] |
| 1491 | if 'electrical-params' in dsheet: |
| 1492 | paramstodo.extend(dsheet['electrical-params']) |
| 1493 | if 'physical-params' in dsheet: |
| 1494 | paramstodo.extend(dsheet['physical-params']) |
| 1495 | |
| 1496 | if self.origin.get() == 'Schematic Capture': |
| 1497 | isschem = True |
| 1498 | else: |
| 1499 | isschem = False |
| 1500 | |
| 1501 | for param in paramstodo: |
| 1502 | # Fill frame with electrical parameter information |
| 1503 | if 'method' in param: |
| 1504 | p = param['method'] |
| 1505 | puniq = p + '.0' |
| 1506 | if puniq in self.status: |
| 1507 | # This method was used before, so give it a unique identifier |
| 1508 | j = 1 |
| 1509 | while True: |
| 1510 | puniq = p + '.' + str(j) |
| 1511 | if puniq not in self.status: |
| 1512 | break |
| 1513 | else: |
| 1514 | j += 1 |
| 1515 | else: |
| 1516 | j = 0 |
| 1517 | paramtype = 'electrical' |
| 1518 | else: |
| 1519 | paramtype = 'physical' |
| 1520 | p = param['condition'] |
| 1521 | puniq = paramtype + '.' + p |
| 1522 | j = 0 |
| 1523 | |
| 1524 | if 'editable' in param and param['editable'] == True: |
| 1525 | normlabel = 'hlight.TLabel' |
| 1526 | redlabel = 'rhlight.TLabel' |
| 1527 | greenlabel = 'ghlight.TLabel' |
| 1528 | normbutton = 'hlight.TButton' |
| 1529 | redbutton = 'rhlight.TButton' |
| 1530 | greenbutton = 'ghlight.TButton' |
| 1531 | else: |
| 1532 | normlabel = 'normal.TLabel' |
| 1533 | redlabel = 'red.TLabel' |
| 1534 | greenlabel = 'green.TLabel' |
| 1535 | normbutton = 'normal.TButton' |
| 1536 | redbutton = 'red.TButton' |
| 1537 | greenbutton = 'green.TButton' |
| 1538 | |
| 1539 | if 'display' in param: |
| 1540 | dtext = param['display'] |
| 1541 | else: |
| 1542 | dtext = p |
| 1543 | |
| 1544 | # Special handling: Change LVS_errors to "device check" when using |
| 1545 | # schematic netlist. |
| 1546 | if paramtype == 'physical': |
| 1547 | if isschem: |
| 1548 | if p == 'LVS_errors': |
| 1549 | dtext = 'Invalid device check' |
| 1550 | |
| 1551 | dframe.description = ttk.Label(dframe, text = dtext, style = normlabel) |
| 1552 | |
| 1553 | dframe.description.grid(column = 0, row=n, sticky='ewns') |
| 1554 | dframe.method = ttk.Label(dframe, text = p, style = normlabel) |
| 1555 | dframe.method.grid(column = 1, row=n, sticky='ewns') |
| 1556 | if 'plot' in param: |
| 1557 | status_style = normlabel |
| 1558 | dframe.plots = ttk.Frame(dframe) |
| 1559 | dframe.plots.grid(column = 2, row=n, columnspan = 6, sticky='ewns') |
| 1560 | plotrec = param['plot'] |
| 1561 | if 'status' in plotrec: |
| 1562 | status_value = plotrec['status'] |
| 1563 | else: |
| 1564 | status_value = '(not checked)' |
| 1565 | dframe_plot = ttk.Label(dframe.plots, text=plotrec['filename'], |
| 1566 | style = normlabel) |
| 1567 | dframe_plot.grid(column = j, row = n, sticky='ewns') |
| 1568 | else: |
| 1569 | # For schematic capture, mark physical parameters that can't and won't be |
| 1570 | # checked as "not applicable". |
| 1571 | status_value = '(not checked)' |
| 1572 | if paramtype == 'physical': |
| 1573 | if isschem: |
| 1574 | if p == 'area' or p == 'width' or p == 'height' or p == 'DRC_errors': |
| 1575 | status_value = '(N/A)' |
| 1576 | |
| 1577 | if 'min' in param: |
| 1578 | status_style = normlabel |
| 1579 | pmin = param['min'] |
| 1580 | if 'target' in pmin: |
| 1581 | if 'unit' in param and not binrex.match(param['unit']): |
| 1582 | targettext = pmin['target'] + ' ' + param['unit'] |
| 1583 | else: |
| 1584 | targettext = pmin['target'] |
| 1585 | # Hack for use of min to change method of scoring |
| 1586 | if not 'penalty' in pmin or pmin['penalty'] != '0': |
| 1587 | dframe.min = ttk.Label(dframe, text=targettext, style = normlabel) |
| 1588 | else: |
| 1589 | dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel) |
| 1590 | else: |
| 1591 | dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel) |
| 1592 | if 'score' in pmin: |
| 1593 | if pmin['score'] != 'fail': |
| 1594 | status_style = greenlabel |
| 1595 | if status_value != 'fail': |
| 1596 | status_value = 'pass' |
| 1597 | else: |
| 1598 | status_style = redlabel |
| 1599 | status_value = 'fail' |
| 1600 | if 'value' in pmin: |
| 1601 | if 'unit' in param and not binrex.match(param['unit']): |
| 1602 | valuetext = pmin['value'] + ' ' + param['unit'] |
| 1603 | else: |
| 1604 | valuetext = pmin['value'] |
| 1605 | dframe.value = ttk.Label(dframe, text=valuetext, style=status_style) |
| 1606 | dframe.value.grid(column = 3, row=n, sticky='ewns') |
| 1607 | else: |
| 1608 | dframe.min = ttk.Label(dframe, text='(no limit)', style = normlabel) |
| 1609 | dframe.min.grid(column = 2, row=n, sticky='ewns') |
| 1610 | if 'typ' in param: |
| 1611 | status_style = normlabel |
| 1612 | ptyp = param['typ'] |
| 1613 | if 'target' in ptyp: |
| 1614 | if 'unit' in param and not binrex.match(param['unit']): |
| 1615 | targettext = ptyp['target'] + ' ' + param['unit'] |
| 1616 | else: |
| 1617 | targettext = ptyp['target'] |
| 1618 | dframe.typ = ttk.Label(dframe, text=targettext, style = normlabel) |
| 1619 | else: |
| 1620 | dframe.typ = ttk.Label(dframe, text='(no target)', style = normlabel) |
| 1621 | if 'score' in ptyp: |
| 1622 | # Note: You can't fail a "typ" score, but there is only one "Status", |
| 1623 | # so if it is a "fail", it must remain a "fail". |
| 1624 | if ptyp['score'] != 'fail': |
| 1625 | status_style = greenlabel |
| 1626 | if status_value != 'fail': |
| 1627 | status_value = 'pass' |
| 1628 | else: |
| 1629 | status_style = redlabel |
| 1630 | status_value = 'fail' |
| 1631 | if 'value' in ptyp: |
| 1632 | if 'unit' in param and not binrex.match(param['unit']): |
| 1633 | valuetext = ptyp['value'] + ' ' + param['unit'] |
| 1634 | else: |
| 1635 | valuetext = ptyp['value'] |
| 1636 | dframe.value = ttk.Label(dframe, text=valuetext, style=status_style) |
| 1637 | dframe.value.grid(column = 5, row=n, sticky='ewns') |
| 1638 | else: |
| 1639 | dframe.typ = ttk.Label(dframe, text='(no target)', style = normlabel) |
| 1640 | dframe.typ.grid(column = 4, row=n, sticky='ewns') |
| 1641 | if 'max' in param: |
| 1642 | status_style = normlabel |
| 1643 | pmax = param['max'] |
| 1644 | if 'target' in pmax: |
| 1645 | if 'unit' in param and not binrex.match(param['unit']): |
| 1646 | targettext = pmax['target'] + ' ' + param['unit'] |
| 1647 | else: |
| 1648 | targettext = pmax['target'] |
| 1649 | # Hack for use of max to change method of scoring |
| 1650 | if not 'penalty' in pmax or pmax['penalty'] != '0': |
| 1651 | dframe.max = ttk.Label(dframe, text=targettext, style = normlabel) |
| 1652 | else: |
| 1653 | dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel) |
| 1654 | else: |
| 1655 | dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel) |
| 1656 | if 'score' in pmax: |
| 1657 | if pmax['score'] != 'fail': |
| 1658 | status_style = greenlabel |
| 1659 | if status_value != 'fail': |
| 1660 | status_value = 'pass' |
| 1661 | else: |
| 1662 | status_style = redlabel |
| 1663 | status_value = 'fail' |
| 1664 | if 'value' in pmax: |
| 1665 | if 'unit' in param and not binrex.match(param['unit']): |
| 1666 | valuetext = pmax['value'] + ' ' + param['unit'] |
| 1667 | else: |
| 1668 | valuetext = pmax['value'] |
| 1669 | dframe.value = ttk.Label(dframe, text=valuetext, style=status_style) |
| 1670 | dframe.value.grid(column = 7, row=n, sticky='ewns') |
| 1671 | else: |
| 1672 | dframe.max = ttk.Label(dframe, text='(no limit)', style = normlabel) |
| 1673 | dframe.max.grid(column = 6, row=n, sticky='ewns') |
| 1674 | |
| 1675 | if paramtype == 'electrical': |
| 1676 | if 'hints' in param: |
| 1677 | simtext = '\u2022Simulate' |
| 1678 | else: |
| 1679 | simtext = 'Simulate' |
| 1680 | else: |
| 1681 | simtext = 'Check' |
| 1682 | |
| 1683 | simbutton = ttk.Menubutton(dframe, text=simtext, style = normbutton) |
| 1684 | |
| 1685 | # Generate pull-down menu on Simulate button. Most items apply |
| 1686 | # only to electrical parameters (at least for now) |
| 1687 | simmenu = tkinter.Menu(simbutton) |
| 1688 | simmenu.add_command(label='Run', |
| 1689 | command = lambda puniq=puniq: self.sim_param(puniq)) |
| 1690 | simmenu.add_command(label='Stop', command = self.stop_sims) |
| 1691 | if paramtype == 'electrical': |
| 1692 | simmenu.add_command(label='Hints', |
| 1693 | command = lambda param=param, simbutton=simbutton: self.add_hints(param, simbutton)) |
| 1694 | simmenu.add_command(label='Edit', |
| 1695 | command = lambda param=param: self.edit_param(param)) |
| 1696 | simmenu.add_command(label='Copy', |
| 1697 | command = lambda param=param: self.copy_param(param)) |
| 1698 | if 'editable' in param and param['editable'] == True: |
| 1699 | simmenu.add_command(label='Delete', |
| 1700 | command = lambda param=param: self.delete_param(param)) |
| 1701 | |
| 1702 | # Attach the menu to the button |
| 1703 | simbutton.config(menu=simmenu) |
| 1704 | |
| 1705 | # simbutton = ttk.Button(dframe, text=simtext, style = normbutton) |
| 1706 | # command = lambda puniq=puniq: self.sim_param(puniq)) |
| 1707 | |
| 1708 | simbutton.grid(column = 9, row=n, sticky='ewns') |
| 1709 | |
| 1710 | if paramtype == 'electrical': |
| 1711 | tooltip.ToolTip(simbutton, text = "Simulate one electrical parameter") |
| 1712 | else: |
| 1713 | tooltip.ToolTip(simbutton, text = "Check one physical parameter") |
| 1714 | |
| 1715 | # If 'pass', then just display message. If 'fail', then create a button that |
| 1716 | # opens and configures the failure report window. |
| 1717 | if status_value == '(not checked)': |
| 1718 | bstyle=normbutton |
| 1719 | stat_label = ttk.Label(dframe, text=status_value, style=bstyle) |
| 1720 | else: |
| 1721 | if status_value == 'fail': |
| 1722 | bstyle=redbutton |
| 1723 | else: |
| 1724 | bstyle=greenbutton |
| 1725 | if paramtype == 'electrical': |
| 1726 | stat_label = ttk.Button(dframe, text=status_value, style=bstyle, |
| 1727 | command = lambda param=param, globcond=globcond: |
| 1728 | self.failreport.display(param, globcond, |
| 1729 | self.cur_datasheet)) |
| 1730 | elif p == 'LVS_errors': |
| 1731 | dspath = os.path.split(self.cur_datasheet)[0] |
| 1732 | datasheet = os.path.split(self.cur_datasheet)[1] |
| 1733 | dsheet = self.datatop['data-sheet'] |
| 1734 | designname = dsheet['ip-name'] |
| 1735 | if self.origin.get() == 'Schematic Capture': |
| 1736 | lvs_file = dspath + '/mag/precheck.log' |
| 1737 | else: |
| 1738 | lvs_file = dspath + '/mag/comp.out' |
| 1739 | if not os.path.exists(lvs_file): |
| 1740 | if os.path.exists(dspath + '/mag/precheck.log'): |
| 1741 | lvs_file = dspath + '/mag/precheck.log' |
| 1742 | elif os.path.exists(dspath + '/mag/comp.out'): |
| 1743 | lvs_file = dspath + '/mag/comp.out' |
| 1744 | |
| 1745 | stat_label = ttk.Button(dframe, text=status_value, style=bstyle, |
| 1746 | command = lambda lvs_file=lvs_file: self.textreport.display(lvs_file)) |
| 1747 | else: |
| 1748 | stat_label = ttk.Label(dframe, text=status_value, style=bstyle) |
| 1749 | tooltip.ToolTip(stat_label, |
| 1750 | text = "Show detail view of simulation conditions and results") |
| 1751 | stat_label.grid(column = 8, row=n, sticky='ewns') |
| 1752 | self.status[puniq] = stat_label |
| 1753 | n += 1 |
| 1754 | |
| 1755 | for child in dframe.winfo_children(): |
| 1756 | child.grid_configure(ipadx = 5, ipady = 1, padx = 2, pady = 2) |
| 1757 | |
| 1758 | # Check if a design submission and characterization may be in progress. |
| 1759 | # If so, add the progress bar at the bottom. |
| 1760 | self.check_ongoing_upload() |
| 1761 | |
| 1762 | if __name__ == '__main__': |
| 1763 | faulthandler.register(signal.SIGUSR2) |
| 1764 | options = [] |
| 1765 | arguments = [] |
| 1766 | for item in sys.argv[1:]: |
| 1767 | if item.find('-', 0) == 0: |
| 1768 | options.append(item) |
| 1769 | else: |
| 1770 | arguments.append(item) |
| 1771 | |
| 1772 | root = tkinter.Tk() |
Tim Edwards | 63dbde9 | 2023-03-07 20:58:29 -0500 | [diff] [blame] | 1773 | app = CACECharacterize(root) |
emayecs | 5966a53 | 2021-07-29 10:07:02 -0400 | [diff] [blame] | 1774 | if arguments: |
| 1775 | print('Calling set_datasheet with argument ' + arguments[0]) |
| 1776 | app.set_datasheet(arguments[0]) |
| 1777 | |
| 1778 | root.mainloop() |