qiskit_trebugger.views.cli.cli_view
1import curses 2from curses.textpad import Textbox 3 4import tabulate 5 6from qiskit.dagcircuit import DAGCircuit 7from qiskit.converters import dag_to_circuit 8 9 10from ...model.pass_type import PassType 11from .cli_pass_pad import TranspilerPassPad 12from .config import COLORS 13 14 15class CLIView: 16 """A class representing the CLI view for the Qiskit Transpiler Debugger.""" 17 18 def __init__(self): 19 """Initialize the CLIView object.""" 20 self._title = None 21 self._overview = None 22 23 self._all_passes_data = [] 24 self._all_passes_table = None 25 self._pass_table_headers = [ 26 "Pass Name", 27 "Pass Type", 28 "Runtime (ms)", 29 "Depth", 30 "Size", 31 "1q Gates", 32 "2q Gates", 33 "Width", 34 ] 35 # add the whitespace option 36 tabulate.PRESERVE_WHITESPACE = True 37 38 self._all_passes_pad = None 39 self._pass_pad_list = None 40 self._status_bar = None 41 self._title_string = "Qiskit Transpiler Debugger" 42 43 self._status_strings = { 44 "normal": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down | 'I': Index into a pass | 'H': Toggle overview | 'Q': Exit", 45 "index": " STATUS BAR | Enter the index of the pass you want to view : ", 46 "invalid": " STATUS BAR | Invalid input entered. Press Enter to continue.", 47 "out_of_bounds": " STATUS BAR | Number entered is out of bounds. Please Enter to continue.", 48 "pass": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down | 'N/P': Move to next/previous | 'I': Index into a pass | 'B': Back to home | 'Q': Exit", 49 } 50 51 self._colors = { 52 "title": None, 53 "status": None, 54 "base_pass_title": None, 55 "changing_pass": None, 56 } 57 58 # define status object 59 self._reset_view_params() 60 61 # add the transpilation sequence 62 self.transpilation_sequence = None 63 64 def _reset_view_params(self): 65 """Reset the view parameters to their default values.""" 66 67 self._view_params = { 68 "curr_row": 0, 69 "curr_col": 0, 70 "last_width": 0, 71 "last_height": 0, 72 "pass_id": -1, 73 "transpiler_pad_width": 800, 74 "transpiler_pad_height": 5000, 75 "transpiler_start_row": 6, 76 "transpiler_start_col": None, 77 "status_type": "normal", 78 "overview_visible": True, 79 "overview_change": False, 80 } 81 82 def _init_color(self): 83 """Initialize colors for the CLI interface.""" 84 # Start colors in curses 85 curses.start_color() 86 87 curses.init_pair(1, COLORS.TITLE["front"], COLORS.TITLE["back"]) 88 curses.init_pair(2, COLORS.STATUS["front"], COLORS.STATUS["back"]) 89 curses.init_pair( 90 3, COLORS.BASE_PASSES_TITLE["front"], COLORS.BASE_PASSES_TITLE["back"] 91 ) 92 curses.init_pair(4, COLORS.CHANGING_PASS["front"], COLORS.CHANGING_PASS["back"]) 93 94 self._colors["title"] = curses.color_pair(1) 95 self._colors["status"] = curses.color_pair(2) 96 self._colors["base_pass_title"] = curses.color_pair(3) 97 self._colors["changing_pass"] = curses.color_pair(4) 98 99 def _get_center(self, width, string_len, divisor=2): 100 """Calculate the starting position for centering a string. 101 102 Args: 103 width (int): Total width of the container. 104 string_len (int): Length of the string to be centered. 105 divisor (int, optional): Divisor for the centering calculation. Defaults to 2. 106 107 Returns: 108 int: Starting position for centering the string. 109 """ 110 return max(0, int(width // divisor - string_len // 2 - string_len % 2)) 111 112 def _handle_keystroke(self, key): 113 """Handle the keystrokes for navigation within the CLI interface. 114 115 Args: 116 key (int): The key pressed by the user. 117 118 Returns: 119 None 120 """ 121 if key == curses.KEY_UP: 122 self._view_params["curr_row"] -= 1 123 self._view_params["curr_row"] = max(self._view_params["curr_row"], 0) 124 elif key == curses.KEY_LEFT: 125 self._view_params["curr_col"] -= 1 126 elif key == curses.KEY_DOWN: 127 self._view_params["curr_row"] += 1 128 if self._view_params["status_type"] == "normal": 129 self._view_params["curr_row"] = min( 130 self._view_params["curr_row"], len(self._all_passes_table) - 1 131 ) 132 elif self._view_params["status_type"] in ["index", "pass"]: 133 self._view_params["curr_row"] = min( 134 self._view_params["curr_row"], 135 1999, 136 ) 137 138 elif key == curses.KEY_RIGHT: 139 self._view_params["curr_col"] += 1 140 141 if self._view_params["status_type"] == "normal": 142 self._view_params["curr_col"] = min( 143 self._view_params["curr_col"], len(self._all_passes_table[1]) - 1 144 ) 145 146 elif self._view_params["status_type"] in ["index", "pass"]: 147 self._view_params["curr_col"] = min( 148 self._view_params["curr_col"], 149 curses.COLS - self._view_params["transpiler_start_col"] - 1, 150 ) 151 elif key in [ord("u"), ord("U")]: 152 self._view_params["curr_row"] = max(self._view_params["curr_row"] - 10, 0) 153 154 elif key in [ord("d"), ord("D")]: 155 self._view_params["curr_row"] += 10 156 if self._view_params["status_type"] == "normal": 157 self._view_params["curr_row"] = min( 158 self._view_params["curr_row"], len(self._all_passes_table) - 1 159 ) 160 elif self._view_params["status_type"] in ["index", "pass"]: 161 self._view_params["curr_row"] = min( 162 self._view_params["curr_row"], 163 1999, 164 ) 165 166 elif key in [ord("i"), ord("I")]: 167 # user wants to index into the pass 168 self._view_params["status_type"] = "index" 169 170 elif key in [ord("n"), ord("N")]: 171 if self._view_params["status_type"] in ["index", "pass"]: 172 self._view_params["pass_id"] = min( 173 self._view_params["pass_id"] + 1, 174 len(self.transpilation_sequence.steps) - 1, 175 ) 176 self._view_params["status_type"] = "pass" 177 178 elif key in [ord("p"), ord("P")]: 179 if self._view_params["status_type"] in ["index", "pass"]: 180 self._view_params["pass_id"] = max(0, self._view_params["pass_id"] - 1) 181 self._view_params["status_type"] = "pass" 182 183 elif key in [ord("b"), ord("B")]: 184 # reset the required state variables 185 self._view_params["status_type"] = "normal" 186 self._view_params["pass_id"] = -1 187 self._view_params["curr_col"] = 0 188 self._view_params["curr_row"] = 0 189 190 elif key in [ord("h"), ord("H")]: 191 self._view_params["overview_visible"] = not self._view_params[ 192 "overview_visible" 193 ] 194 self._view_params["overview_change"] = True 195 self._view_params["curr_col"] = 0 196 self._view_params["curr_row"] = 0 197 if not self._view_params["overview_visible"]: 198 self._view_params["transpiler_start_col"] = 0 199 200 def _build_title_win(self, cols): 201 """Builds the title window for the debugger 202 203 Args: 204 cols (int): width of the window 205 206 Returns: 207 title_window (curses.window): title window object 208 """ 209 title_rows = 4 210 title_cols = cols 211 begin_row = 1 212 title_window = curses.newwin(title_rows, title_cols, begin_row, 0) 213 214 title_str = self._title_string[: title_cols - 1] 215 216 # Add title string to the title window 217 start_x_title = self._get_center(title_cols, len(title_str)) 218 title_window.bkgd(self._colors["title"]) 219 title_window.hline(0, 0, "-", title_cols) 220 title_window.addstr(1, start_x_title, title_str, curses.A_BOLD) 221 title_window.hline(2, 0, "-", title_cols) 222 223 # add Subtitle 224 subtitle = "| " 225 for key, value in self.transpilation_sequence.general_info.items(): 226 subtitle += f"{key}: {value} | " 227 228 subtitle = subtitle[: title_cols - 1] 229 start_x_subtitle = self._get_center(title_cols, len(subtitle)) 230 title_window.addstr(3, start_x_subtitle, subtitle) 231 232 return title_window 233 234 def _get_overview_stats(self): 235 """Get the overview statistics for the transpilation sequence. 236 237 Returns: 238 dict: A dictionary containing overview statistics for the transpilation sequence. 239 """ 240 init_step = self.transpilation_sequence.steps[0] 241 final_step = self.transpilation_sequence.steps[-1] 242 243 # build overview 244 overview_stats = { 245 "depth": {"init": 0, "final": 0}, 246 "size": {"init": 0, "final": 0}, 247 "width": {"init": 0, "final": 0}, 248 } 249 250 # get the depths, size and width 251 init_step_dict = init_step.circuit_stats.__dict__ 252 final_step_dict = final_step.circuit_stats.__dict__ 253 254 for prop in overview_stats: # prop should have same name as in CircuitStats 255 overview_stats[prop]["init"] = init_step_dict[prop] 256 overview_stats[prop]["final"] = final_step_dict[prop] 257 258 # get the op counts 259 overview_stats["ops"] = {"init": 0, "final": 0} 260 overview_stats["ops"]["init"] = ( 261 init_step.circuit_stats.ops_1q 262 + init_step.circuit_stats.ops_2q 263 + init_step.circuit_stats.ops_3q 264 ) 265 266 overview_stats["ops"]["final"] = ( 267 final_step.circuit_stats.ops_1q 268 + final_step.circuit_stats.ops_2q 269 + final_step.circuit_stats.ops_3q 270 ) 271 272 return overview_stats 273 274 def _build_overview_win(self, rows, cols): 275 """Build and return the overview window for the debugger. 276 277 Args: 278 rows (int): Height of the window. 279 cols (int): Width of the window. 280 281 Returns: 282 curses.window: The overview window object. 283 """ 284 begin_row = 6 285 overview_win = curses.newwin(rows, cols, begin_row, 0) 286 287 total_passes = {"T": 0, "A": 0} 288 for step in self.transpilation_sequence.steps: 289 if step.pass_type == PassType.TRANSFORMATION: 290 total_passes["T"] += 1 291 else: 292 total_passes["A"] += 1 293 294 total_pass_str = f"Total Passes : {total_passes['A'] + total_passes['T']}"[ 295 : cols - 1 296 ] 297 pass_categories_str = ( 298 f"Transformation : {total_passes['T']} | Analysis : {total_passes['A']}"[ 299 : cols - 1 300 ] 301 ) 302 303 start_x = 5 304 overview_win.addstr(5, start_x, "Pass Overview"[: cols - 1], curses.A_BOLD) 305 overview_win.addstr(6, start_x, total_pass_str) 306 overview_win.addstr(7, start_x, pass_categories_str) 307 308 # runtime 309 runtime_str = ( 310 f"Runtime : {round(self.transpilation_sequence.total_runtime,2)} ms"[ 311 : cols - 1 312 ] 313 ) 314 overview_win.addstr(9, start_x, runtime_str, curses.A_BOLD) 315 316 # circuit stats 317 headers = ["Property", "Initial", "Final"] 318 319 overview_stats = self._get_overview_stats() 320 rows = [] 321 for prop, value in overview_stats.items(): 322 rows.append([prop.capitalize(), value["init"], value["final"]]) 323 stats_table = tabulate.tabulate( 324 rows, 325 headers=headers, 326 tablefmt="simple_grid", 327 stralign=("center"), 328 numalign="center", 329 ).splitlines() 330 331 for row in range(12, 12 + len(stats_table)): 332 overview_win.addstr(row, start_x, stats_table[row - 12][: cols - 1]) 333 334 # for correct formatting of title 335 max_line_length = len(stats_table[0]) 336 337 # add titles 338 339 # stats header 340 stats_str = "Circuit Statistics"[: cols - 1] 341 stats_head_offset = self._get_center(max_line_length, len(stats_str)) 342 overview_win.addstr(11, start_x + stats_head_offset, stats_str, curses.A_BOLD) 343 344 # overview header 345 overview_str = "TRANSPILATION OVERVIEW"[: cols - 1] 346 start_x_overview = start_x + self._get_center( 347 max_line_length, len(overview_str) 348 ) 349 overview_win.hline(0, start_x, "_", min(cols, max_line_length)) 350 overview_win.addstr(2, start_x_overview, overview_str, curses.A_BOLD) 351 overview_win.hline(3, start_x, "_", min(cols, max_line_length)) 352 353 # update the dimensions 354 self._view_params["transpiler_start_col"] = start_x + max_line_length + 5 355 return overview_win 356 357 def _get_pass_title(self, cols): 358 """Get the window object for the title of the pass table. 359 360 Args: 361 cols (int): Width of the window. 362 363 Returns: 364 curses.window: The window object for the pass title. 365 """ 366 height = 4 367 368 width = max(5, cols - self._view_params["transpiler_start_col"] - 1) 369 pass_title = curses.newwin( 370 height, 371 width, 372 self._view_params["transpiler_start_row"], 373 self._view_params["transpiler_start_col"], 374 ) 375 # add the title of the table 376 transpiler_passes = "Transpiler Passes"[: cols - 1] 377 start_header = self._get_center(width, len(transpiler_passes)) 378 try: 379 pass_title.hline(0, 0, "_", width - 4) 380 pass_title.addstr(2, start_header, "Transpiler Passes", curses.A_BOLD) 381 pass_title.hline(3, 0, "_", width - 4) 382 except Exception as _: 383 pass_title = None 384 385 return pass_title 386 387 def _get_statusbar_win(self, rows, cols, status_type="normal"): 388 """Returns the status bar window object 389 390 Args: 391 rows (int): Current height of the terminal 392 cols (nt): Current width of the terminal 393 status_type (str, optional): Type of status of the debugger. Corresponds to 394 different view states of the debugger. 395 Defaults to "normal". 396 397 STATUS STATES 398 -normal : normal status bar 399 -index : index status bar - user is entering the numbers (requires input to be shown to user) 400 -invalid : error status bar - user has entered an invalid character 401 -out_of_bounds : out of bounds status bar - user has entered a number out of bounds 402 -pass : pass status bar - user has entered a valid number and is now viewing the pass details 403 404 NOTE : processing is done after the user presses enter. 405 This will only return a status bar window, TEXT processing is done within this function ONLY 406 Returns: 407 curses.window : Statusbar window object 408 """ 409 410 status_str = self._status_strings[status_type][: cols - 1] 411 412 statusbar_window = curses.newwin(1, cols, rows - 1, 0) 413 statusbar_window.bkgd(" ", self._colors["status"]) 414 415 offset = 0 416 statusbar_window.addstr(0, offset, status_str) 417 offset += len(status_str) 418 419 # now if index, enter a text box 420 if status_type == "index": 421 textbox = Textbox(statusbar_window) 422 textbox.edit() 423 str_value = ( 424 textbox.gather().split(":")[1].strip() 425 ) # get the value of the entered text 426 427 try: 428 num = int(str_value) 429 total_passes = len(self.transpilation_sequence.steps) 430 if num >= total_passes or num < 0: 431 status_str = self._status_strings["out_of_bounds"] 432 else: 433 status_str = self._status_strings["pass"] 434 self._view_params["pass_id"] = num 435 except ValueError as _: 436 # Invalid number entered 437 status_str = self._status_strings["invalid"] 438 status_str = status_str[: cols - 1] 439 440 # display the new string 441 statusbar_window.clear() 442 offset = 0 443 statusbar_window.addstr(0, 0, status_str) 444 offset += len(status_str) 445 446 statusbar_window.addstr(0, offset, " " * (cols - offset - 1)) 447 448 return statusbar_window 449 450 def _refresh_base_windows(self, resized, height, width): 451 """Refreshes the base windows of the debugger 452 453 Args: 454 width (int): Current width of the terminal 455 456 Returns: 457 None 458 """ 459 if resized: 460 self._title = self._build_title_win(width) 461 self._title.noutrefresh() 462 463 overview_toggle = ( 464 self._view_params["overview_visible"] 465 and self._view_params["overview_change"] 466 ) 467 if resized or overview_toggle: 468 try: 469 self._overview = self._build_overview_win(height, width) 470 self._overview.noutrefresh() 471 except: 472 # change the view param for overview 473 self._view_params["transpiler_start_col"] = 0 474 475 pass_title_window = self._get_pass_title(width) 476 if pass_title_window: 477 pass_title_window.noutrefresh() 478 479 def _get_pass_circuit(self, step): 480 if step.pass_type == PassType.TRANSFORMATION: 481 if step.circuit_stats.depth > 300: 482 # means it had depth > 300, so we can't show it 483 return None 484 return dag_to_circuit(step.dag) 485 idx = step.index 486 # Due to a bug in DAGCircuit.__eq__, we can not use ``step.dag != None`` 487 488 found_transform = False 489 while ( 490 not isinstance(self.transpilation_sequence.steps[idx].dag, DAGCircuit) 491 and idx > 0 492 ): 493 idx = idx - 1 494 if idx >= 0: 495 found_transform = ( 496 self.transpilation_sequence.steps[idx].pass_type 497 == PassType.TRANSFORMATION 498 ) 499 500 if found_transform is False: 501 return self.transpilation_sequence.original_circuit 502 503 return dag_to_circuit(self.transpilation_sequence.steps[idx].dag) 504 505 def _get_pass_property_set(self, step): 506 if step.property_set_index is not None: 507 return self.transpilation_sequence.steps[ 508 step.property_set_index 509 ].property_set 510 511 return {} 512 513 def _build_pass_pad(self, index): 514 step = self.transpilation_sequence.steps[index] 515 pad = curses.newpad( 516 self._view_params["transpiler_pad_height"], 517 self._view_params["transpiler_pad_width"], 518 ) 519 pass_pad = TranspilerPassPad( 520 step, 521 self._get_pass_circuit(step), 522 self._get_pass_property_set(step), 523 self._view_params["transpiler_pad_height"], 524 self._view_params["transpiler_pad_width"], 525 pad, 526 ) 527 pass_pad.build_pad() 528 self._pass_pad_list[index] = pass_pad.pad 529 530 def add_step(self, step): 531 """Adds a step to the transpilation sequence. 532 533 Args: 534 step (TranspilationStep): `TranspilationStep` object to be added to the transpilation sequence. 535 """ 536 self._all_passes_data.append( 537 [ 538 step.name, 539 step.pass_type.value, 540 step.duration, 541 step.circuit_stats.depth, 542 step.circuit_stats.size, 543 step.circuit_stats.ops_1q, 544 step.circuit_stats.ops_2q, 545 step.circuit_stats.width, 546 ] 547 ) 548 549 def _get_all_passes_table(self): 550 """Generate and return the table containing all the transpiler passes. 551 552 Returns: 553 list: The list representing the table of all transpiler passes. 554 """ 555 # build from the transpilation sequence 556 # make table 557 pass_table = tabulate.tabulate( 558 headers=self._pass_table_headers, 559 tabular_data=self._all_passes_data, 560 tablefmt="simple_grid", 561 stralign="center", 562 numalign="center", 563 showindex="always", 564 ).splitlines() 565 566 return pass_table 567 568 def _get_changing_pass_list(self): 569 """Get the list of indices of passes that caused a change in the circuit. 570 571 Returns: 572 list: A list containing the indices of changing passes. 573 """ 574 pass_id_list = [] 575 for i in range(1, len(self.transpilation_sequence.steps)): 576 prev_step = self.transpilation_sequence.steps[i - 1] 577 curr_step = self.transpilation_sequence.steps[i] 578 if prev_step.circuit_stats != curr_step.circuit_stats: 579 pass_id_list.append(i) 580 return pass_id_list 581 582 def _get_all_passes_pad(self): 583 """Generate and return the pad containing all the transpiler passes. 584 585 Returns: 586 curses.pad: The pad containing all the transpiler passes. 587 """ 588 start_x = 4 589 table_width = 500 # for now 590 table_height = len(self._all_passes_table) + 1 591 pass_pad = curses.newpad(table_height, table_width) 592 593 header_height = 3 594 595 # centering is required for each row 596 for row in range(header_height): 597 offset = self._get_center( 598 table_width, len(self._all_passes_table[row][: table_width - 1]) 599 ) 600 pass_pad.addstr( 601 row, 602 start_x + offset, 603 self._all_passes_table[row][: table_width - 1], 604 curses.A_BOLD | self._colors["base_pass_title"], 605 ) 606 607 # generate a changing pass set to see which pass 608 # changed the circuit and which didn't 609 changing_pass_list = set(self._get_changing_pass_list()) 610 611 def _is_changing_pass_row(row): 612 # dashes only at even rows 613 if row % 2 == 0: 614 return False 615 index = (row - header_height) // 2 616 if index in changing_pass_list: 617 return True 618 return False 619 620 # now start adding the passes 621 for row in range(header_height, len(self._all_passes_table)): 622 offset = self._get_center( 623 table_width, len(self._all_passes_table[row][: table_width - 1]) 624 ) 625 highlight = 0 626 627 if _is_changing_pass_row(row): 628 highlight = curses.A_BOLD | self._colors["changing_pass"] 629 630 pass_pad.addstr( 631 row, 632 start_x + offset, 633 self._all_passes_table[row][: table_width - 1], 634 highlight, 635 ) 636 637 # populated pad with passes 638 return pass_pad 639 640 def _render_transpilation_pad(self, pass_pad, curr_row, curr_col, rows, cols): 641 """Function to render the pass pad. 642 643 NOTE : this is agnostic of whether we are passing the base pad 644 or the individual transpiler pass pad. Why? 645 Because we are not shifting the pad, we are just refreshing it. 646 647 Args: 648 pass_pad (curses.pad): The pad containing the individual pass details. 649 curr_row (int): Current row position. 650 curr_col (int): Current column position. 651 rows (int): Total number of rows in the terminal. 652 cols (int): Total number of columns in the terminal. 653 654 Returns: 655 None 656 """ 657 if not pass_pad: 658 return 659 660 # 4 rows for the title + curr_row (curr_row is the row of the pass) 661 title_height = 5 662 start_row = self._view_params["transpiler_start_row"] + title_height 663 664 # if we don't have enough rows 665 if start_row >= rows - 2: 666 return 667 668 # if we don't have enough columns 669 if self._view_params["transpiler_start_col"] >= cols - 6: 670 return 671 672 actual_width = pass_pad.getmaxyx()[1] 673 window_width = cols - self._view_params["transpiler_start_col"] 674 col_offset = (actual_width - window_width) // 2 675 676 pass_pad.noutrefresh( 677 curr_row, 678 col_offset + curr_col, 679 start_row, 680 self._view_params["transpiler_start_col"], 681 rows - 2, 682 cols - 6, 683 ) 684 685 def _pre_input(self, height, width): 686 """Function to render the pad before any input is entered 687 by the user 688 689 Args: 690 height (int): Number of rows 691 width (int): Number of cols 692 """ 693 pad_to_render = None 694 695 if self._view_params["status_type"] == "index": 696 pass_id = self._view_params["pass_id"] 697 if pass_id == -1: 698 pad_to_render = self._all_passes_pad 699 else: 700 if self._pass_pad_list[pass_id] is None: 701 self._build_pass_pad(pass_id) 702 pad_to_render = self._pass_pad_list[pass_id] 703 704 self._render_transpilation_pad( 705 pad_to_render, 706 self._view_params["curr_row"], 707 self._view_params["curr_col"], 708 height, 709 width, 710 ) 711 712 def _post_input(self, height, width): 713 """Render the pad after user input is entered. 714 715 Args: 716 height (int): Number of rows in the terminal. 717 width (int): Number of columns in the terminal. 718 719 Returns: 720 None 721 """ 722 pad_to_render = None 723 if self._view_params["status_type"] == "normal": 724 pad_to_render = self._all_passes_pad 725 elif self._view_params["status_type"] in ["index", "pass"]: 726 # using zero based indexing 727 pass_id = self._view_params["pass_id"] 728 if pass_id >= 0: 729 self._view_params["status_type"] = "pass" 730 if self._pass_pad_list[pass_id] is None: 731 self._build_pass_pad(pass_id) 732 pad_to_render = self._pass_pad_list[pass_id] 733 734 self._render_transpilation_pad( 735 pad_to_render, 736 self._view_params["curr_row"], 737 self._view_params["curr_col"], 738 height, 739 width, 740 ) 741 742 def display(self, stdscr): 743 """Display the Qiskit Transpiler Debugger on the terminal. 744 745 Args: 746 stdscr (curses.window): The main window object provided by the curses library. 747 748 Returns: 749 None 750 """ 751 key = 0 752 753 # Clear and refresh the screen for a blank canvas 754 stdscr.clear() 755 stdscr.refresh() 756 757 # initiate color 758 self._init_color() 759 760 # hide the cursor 761 curses.curs_set(0) 762 763 # reset view params 764 self._reset_view_params() 765 766 height, width = stdscr.getmaxyx() 767 self._refresh_base_windows(True, height, width) 768 769 # build the base transpiler pad using the transpilation sequence 770 self._all_passes_table = self._get_all_passes_table() 771 self._all_passes_pad = self._get_all_passes_pad() 772 self._pass_pad_list = [None] * len(self.transpilation_sequence.steps) 773 774 # build the individual pass pad list 775 # done, via add_step 776 assert len(self._pass_pad_list) > 0 777 778 while key not in [ord("q"), ord("Q")]: 779 height, width = stdscr.getmaxyx() 780 781 # Check for clearing 782 panel_initiated = ( 783 self._view_params["last_height"] + self._view_params["last_width"] > 0 784 ) 785 panel_resized = ( 786 self._view_params["last_width"] != width 787 or self._view_params["last_height"] != height 788 ) 789 790 if panel_initiated and panel_resized: 791 stdscr.clear() 792 793 self._view_params["overview_change"] = False 794 self._handle_keystroke(key) 795 796 whstr = f"Width: {width}, Height: {height}" 797 stdscr.addstr(0, 0, whstr) 798 799 # refresh the screen and then the windows 800 stdscr.noutrefresh() 801 self._refresh_base_windows(panel_resized, height, width) 802 803 # pre input rendering 804 self._pre_input(height, width) 805 806 # render the status bar , irrespective of width / height 807 # and get the input (if any) 808 self._status_bar = self._get_statusbar_win( 809 height, width, self._view_params["status_type"] 810 ) 811 self._status_bar.noutrefresh() 812 813 # post input rendering 814 self._post_input(height, width) 815 816 self._view_params["last_width"] = width 817 self._view_params["last_height"] = height 818 819 curses.doupdate() 820 821 # wait for the next input 822 key = stdscr.getch()
class
CLIView:
16class CLIView: 17 """A class representing the CLI view for the Qiskit Transpiler Debugger.""" 18 19 def __init__(self): 20 """Initialize the CLIView object.""" 21 self._title = None 22 self._overview = None 23 24 self._all_passes_data = [] 25 self._all_passes_table = None 26 self._pass_table_headers = [ 27 "Pass Name", 28 "Pass Type", 29 "Runtime (ms)", 30 "Depth", 31 "Size", 32 "1q Gates", 33 "2q Gates", 34 "Width", 35 ] 36 # add the whitespace option 37 tabulate.PRESERVE_WHITESPACE = True 38 39 self._all_passes_pad = None 40 self._pass_pad_list = None 41 self._status_bar = None 42 self._title_string = "Qiskit Transpiler Debugger" 43 44 self._status_strings = { 45 "normal": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down | 'I': Index into a pass | 'H': Toggle overview | 'Q': Exit", 46 "index": " STATUS BAR | Enter the index of the pass you want to view : ", 47 "invalid": " STATUS BAR | Invalid input entered. Press Enter to continue.", 48 "out_of_bounds": " STATUS BAR | Number entered is out of bounds. Please Enter to continue.", 49 "pass": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down | 'N/P': Move to next/previous | 'I': Index into a pass | 'B': Back to home | 'Q': Exit", 50 } 51 52 self._colors = { 53 "title": None, 54 "status": None, 55 "base_pass_title": None, 56 "changing_pass": None, 57 } 58 59 # define status object 60 self._reset_view_params() 61 62 # add the transpilation sequence 63 self.transpilation_sequence = None 64 65 def _reset_view_params(self): 66 """Reset the view parameters to their default values.""" 67 68 self._view_params = { 69 "curr_row": 0, 70 "curr_col": 0, 71 "last_width": 0, 72 "last_height": 0, 73 "pass_id": -1, 74 "transpiler_pad_width": 800, 75 "transpiler_pad_height": 5000, 76 "transpiler_start_row": 6, 77 "transpiler_start_col": None, 78 "status_type": "normal", 79 "overview_visible": True, 80 "overview_change": False, 81 } 82 83 def _init_color(self): 84 """Initialize colors for the CLI interface.""" 85 # Start colors in curses 86 curses.start_color() 87 88 curses.init_pair(1, COLORS.TITLE["front"], COLORS.TITLE["back"]) 89 curses.init_pair(2, COLORS.STATUS["front"], COLORS.STATUS["back"]) 90 curses.init_pair( 91 3, COLORS.BASE_PASSES_TITLE["front"], COLORS.BASE_PASSES_TITLE["back"] 92 ) 93 curses.init_pair(4, COLORS.CHANGING_PASS["front"], COLORS.CHANGING_PASS["back"]) 94 95 self._colors["title"] = curses.color_pair(1) 96 self._colors["status"] = curses.color_pair(2) 97 self._colors["base_pass_title"] = curses.color_pair(3) 98 self._colors["changing_pass"] = curses.color_pair(4) 99 100 def _get_center(self, width, string_len, divisor=2): 101 """Calculate the starting position for centering a string. 102 103 Args: 104 width (int): Total width of the container. 105 string_len (int): Length of the string to be centered. 106 divisor (int, optional): Divisor for the centering calculation. Defaults to 2. 107 108 Returns: 109 int: Starting position for centering the string. 110 """ 111 return max(0, int(width // divisor - string_len // 2 - string_len % 2)) 112 113 def _handle_keystroke(self, key): 114 """Handle the keystrokes for navigation within the CLI interface. 115 116 Args: 117 key (int): The key pressed by the user. 118 119 Returns: 120 None 121 """ 122 if key == curses.KEY_UP: 123 self._view_params["curr_row"] -= 1 124 self._view_params["curr_row"] = max(self._view_params["curr_row"], 0) 125 elif key == curses.KEY_LEFT: 126 self._view_params["curr_col"] -= 1 127 elif key == curses.KEY_DOWN: 128 self._view_params["curr_row"] += 1 129 if self._view_params["status_type"] == "normal": 130 self._view_params["curr_row"] = min( 131 self._view_params["curr_row"], len(self._all_passes_table) - 1 132 ) 133 elif self._view_params["status_type"] in ["index", "pass"]: 134 self._view_params["curr_row"] = min( 135 self._view_params["curr_row"], 136 1999, 137 ) 138 139 elif key == curses.KEY_RIGHT: 140 self._view_params["curr_col"] += 1 141 142 if self._view_params["status_type"] == "normal": 143 self._view_params["curr_col"] = min( 144 self._view_params["curr_col"], len(self._all_passes_table[1]) - 1 145 ) 146 147 elif self._view_params["status_type"] in ["index", "pass"]: 148 self._view_params["curr_col"] = min( 149 self._view_params["curr_col"], 150 curses.COLS - self._view_params["transpiler_start_col"] - 1, 151 ) 152 elif key in [ord("u"), ord("U")]: 153 self._view_params["curr_row"] = max(self._view_params["curr_row"] - 10, 0) 154 155 elif key in [ord("d"), ord("D")]: 156 self._view_params["curr_row"] += 10 157 if self._view_params["status_type"] == "normal": 158 self._view_params["curr_row"] = min( 159 self._view_params["curr_row"], len(self._all_passes_table) - 1 160 ) 161 elif self._view_params["status_type"] in ["index", "pass"]: 162 self._view_params["curr_row"] = min( 163 self._view_params["curr_row"], 164 1999, 165 ) 166 167 elif key in [ord("i"), ord("I")]: 168 # user wants to index into the pass 169 self._view_params["status_type"] = "index" 170 171 elif key in [ord("n"), ord("N")]: 172 if self._view_params["status_type"] in ["index", "pass"]: 173 self._view_params["pass_id"] = min( 174 self._view_params["pass_id"] + 1, 175 len(self.transpilation_sequence.steps) - 1, 176 ) 177 self._view_params["status_type"] = "pass" 178 179 elif key in [ord("p"), ord("P")]: 180 if self._view_params["status_type"] in ["index", "pass"]: 181 self._view_params["pass_id"] = max(0, self._view_params["pass_id"] - 1) 182 self._view_params["status_type"] = "pass" 183 184 elif key in [ord("b"), ord("B")]: 185 # reset the required state variables 186 self._view_params["status_type"] = "normal" 187 self._view_params["pass_id"] = -1 188 self._view_params["curr_col"] = 0 189 self._view_params["curr_row"] = 0 190 191 elif key in [ord("h"), ord("H")]: 192 self._view_params["overview_visible"] = not self._view_params[ 193 "overview_visible" 194 ] 195 self._view_params["overview_change"] = True 196 self._view_params["curr_col"] = 0 197 self._view_params["curr_row"] = 0 198 if not self._view_params["overview_visible"]: 199 self._view_params["transpiler_start_col"] = 0 200 201 def _build_title_win(self, cols): 202 """Builds the title window for the debugger 203 204 Args: 205 cols (int): width of the window 206 207 Returns: 208 title_window (curses.window): title window object 209 """ 210 title_rows = 4 211 title_cols = cols 212 begin_row = 1 213 title_window = curses.newwin(title_rows, title_cols, begin_row, 0) 214 215 title_str = self._title_string[: title_cols - 1] 216 217 # Add title string to the title window 218 start_x_title = self._get_center(title_cols, len(title_str)) 219 title_window.bkgd(self._colors["title"]) 220 title_window.hline(0, 0, "-", title_cols) 221 title_window.addstr(1, start_x_title, title_str, curses.A_BOLD) 222 title_window.hline(2, 0, "-", title_cols) 223 224 # add Subtitle 225 subtitle = "| " 226 for key, value in self.transpilation_sequence.general_info.items(): 227 subtitle += f"{key}: {value} | " 228 229 subtitle = subtitle[: title_cols - 1] 230 start_x_subtitle = self._get_center(title_cols, len(subtitle)) 231 title_window.addstr(3, start_x_subtitle, subtitle) 232 233 return title_window 234 235 def _get_overview_stats(self): 236 """Get the overview statistics for the transpilation sequence. 237 238 Returns: 239 dict: A dictionary containing overview statistics for the transpilation sequence. 240 """ 241 init_step = self.transpilation_sequence.steps[0] 242 final_step = self.transpilation_sequence.steps[-1] 243 244 # build overview 245 overview_stats = { 246 "depth": {"init": 0, "final": 0}, 247 "size": {"init": 0, "final": 0}, 248 "width": {"init": 0, "final": 0}, 249 } 250 251 # get the depths, size and width 252 init_step_dict = init_step.circuit_stats.__dict__ 253 final_step_dict = final_step.circuit_stats.__dict__ 254 255 for prop in overview_stats: # prop should have same name as in CircuitStats 256 overview_stats[prop]["init"] = init_step_dict[prop] 257 overview_stats[prop]["final"] = final_step_dict[prop] 258 259 # get the op counts 260 overview_stats["ops"] = {"init": 0, "final": 0} 261 overview_stats["ops"]["init"] = ( 262 init_step.circuit_stats.ops_1q 263 + init_step.circuit_stats.ops_2q 264 + init_step.circuit_stats.ops_3q 265 ) 266 267 overview_stats["ops"]["final"] = ( 268 final_step.circuit_stats.ops_1q 269 + final_step.circuit_stats.ops_2q 270 + final_step.circuit_stats.ops_3q 271 ) 272 273 return overview_stats 274 275 def _build_overview_win(self, rows, cols): 276 """Build and return the overview window for the debugger. 277 278 Args: 279 rows (int): Height of the window. 280 cols (int): Width of the window. 281 282 Returns: 283 curses.window: The overview window object. 284 """ 285 begin_row = 6 286 overview_win = curses.newwin(rows, cols, begin_row, 0) 287 288 total_passes = {"T": 0, "A": 0} 289 for step in self.transpilation_sequence.steps: 290 if step.pass_type == PassType.TRANSFORMATION: 291 total_passes["T"] += 1 292 else: 293 total_passes["A"] += 1 294 295 total_pass_str = f"Total Passes : {total_passes['A'] + total_passes['T']}"[ 296 : cols - 1 297 ] 298 pass_categories_str = ( 299 f"Transformation : {total_passes['T']} | Analysis : {total_passes['A']}"[ 300 : cols - 1 301 ] 302 ) 303 304 start_x = 5 305 overview_win.addstr(5, start_x, "Pass Overview"[: cols - 1], curses.A_BOLD) 306 overview_win.addstr(6, start_x, total_pass_str) 307 overview_win.addstr(7, start_x, pass_categories_str) 308 309 # runtime 310 runtime_str = ( 311 f"Runtime : {round(self.transpilation_sequence.total_runtime,2)} ms"[ 312 : cols - 1 313 ] 314 ) 315 overview_win.addstr(9, start_x, runtime_str, curses.A_BOLD) 316 317 # circuit stats 318 headers = ["Property", "Initial", "Final"] 319 320 overview_stats = self._get_overview_stats() 321 rows = [] 322 for prop, value in overview_stats.items(): 323 rows.append([prop.capitalize(), value["init"], value["final"]]) 324 stats_table = tabulate.tabulate( 325 rows, 326 headers=headers, 327 tablefmt="simple_grid", 328 stralign=("center"), 329 numalign="center", 330 ).splitlines() 331 332 for row in range(12, 12 + len(stats_table)): 333 overview_win.addstr(row, start_x, stats_table[row - 12][: cols - 1]) 334 335 # for correct formatting of title 336 max_line_length = len(stats_table[0]) 337 338 # add titles 339 340 # stats header 341 stats_str = "Circuit Statistics"[: cols - 1] 342 stats_head_offset = self._get_center(max_line_length, len(stats_str)) 343 overview_win.addstr(11, start_x + stats_head_offset, stats_str, curses.A_BOLD) 344 345 # overview header 346 overview_str = "TRANSPILATION OVERVIEW"[: cols - 1] 347 start_x_overview = start_x + self._get_center( 348 max_line_length, len(overview_str) 349 ) 350 overview_win.hline(0, start_x, "_", min(cols, max_line_length)) 351 overview_win.addstr(2, start_x_overview, overview_str, curses.A_BOLD) 352 overview_win.hline(3, start_x, "_", min(cols, max_line_length)) 353 354 # update the dimensions 355 self._view_params["transpiler_start_col"] = start_x + max_line_length + 5 356 return overview_win 357 358 def _get_pass_title(self, cols): 359 """Get the window object for the title of the pass table. 360 361 Args: 362 cols (int): Width of the window. 363 364 Returns: 365 curses.window: The window object for the pass title. 366 """ 367 height = 4 368 369 width = max(5, cols - self._view_params["transpiler_start_col"] - 1) 370 pass_title = curses.newwin( 371 height, 372 width, 373 self._view_params["transpiler_start_row"], 374 self._view_params["transpiler_start_col"], 375 ) 376 # add the title of the table 377 transpiler_passes = "Transpiler Passes"[: cols - 1] 378 start_header = self._get_center(width, len(transpiler_passes)) 379 try: 380 pass_title.hline(0, 0, "_", width - 4) 381 pass_title.addstr(2, start_header, "Transpiler Passes", curses.A_BOLD) 382 pass_title.hline(3, 0, "_", width - 4) 383 except Exception as _: 384 pass_title = None 385 386 return pass_title 387 388 def _get_statusbar_win(self, rows, cols, status_type="normal"): 389 """Returns the status bar window object 390 391 Args: 392 rows (int): Current height of the terminal 393 cols (nt): Current width of the terminal 394 status_type (str, optional): Type of status of the debugger. Corresponds to 395 different view states of the debugger. 396 Defaults to "normal". 397 398 STATUS STATES 399 -normal : normal status bar 400 -index : index status bar - user is entering the numbers (requires input to be shown to user) 401 -invalid : error status bar - user has entered an invalid character 402 -out_of_bounds : out of bounds status bar - user has entered a number out of bounds 403 -pass : pass status bar - user has entered a valid number and is now viewing the pass details 404 405 NOTE : processing is done after the user presses enter. 406 This will only return a status bar window, TEXT processing is done within this function ONLY 407 Returns: 408 curses.window : Statusbar window object 409 """ 410 411 status_str = self._status_strings[status_type][: cols - 1] 412 413 statusbar_window = curses.newwin(1, cols, rows - 1, 0) 414 statusbar_window.bkgd(" ", self._colors["status"]) 415 416 offset = 0 417 statusbar_window.addstr(0, offset, status_str) 418 offset += len(status_str) 419 420 # now if index, enter a text box 421 if status_type == "index": 422 textbox = Textbox(statusbar_window) 423 textbox.edit() 424 str_value = ( 425 textbox.gather().split(":")[1].strip() 426 ) # get the value of the entered text 427 428 try: 429 num = int(str_value) 430 total_passes = len(self.transpilation_sequence.steps) 431 if num >= total_passes or num < 0: 432 status_str = self._status_strings["out_of_bounds"] 433 else: 434 status_str = self._status_strings["pass"] 435 self._view_params["pass_id"] = num 436 except ValueError as _: 437 # Invalid number entered 438 status_str = self._status_strings["invalid"] 439 status_str = status_str[: cols - 1] 440 441 # display the new string 442 statusbar_window.clear() 443 offset = 0 444 statusbar_window.addstr(0, 0, status_str) 445 offset += len(status_str) 446 447 statusbar_window.addstr(0, offset, " " * (cols - offset - 1)) 448 449 return statusbar_window 450 451 def _refresh_base_windows(self, resized, height, width): 452 """Refreshes the base windows of the debugger 453 454 Args: 455 width (int): Current width of the terminal 456 457 Returns: 458 None 459 """ 460 if resized: 461 self._title = self._build_title_win(width) 462 self._title.noutrefresh() 463 464 overview_toggle = ( 465 self._view_params["overview_visible"] 466 and self._view_params["overview_change"] 467 ) 468 if resized or overview_toggle: 469 try: 470 self._overview = self._build_overview_win(height, width) 471 self._overview.noutrefresh() 472 except: 473 # change the view param for overview 474 self._view_params["transpiler_start_col"] = 0 475 476 pass_title_window = self._get_pass_title(width) 477 if pass_title_window: 478 pass_title_window.noutrefresh() 479 480 def _get_pass_circuit(self, step): 481 if step.pass_type == PassType.TRANSFORMATION: 482 if step.circuit_stats.depth > 300: 483 # means it had depth > 300, so we can't show it 484 return None 485 return dag_to_circuit(step.dag) 486 idx = step.index 487 # Due to a bug in DAGCircuit.__eq__, we can not use ``step.dag != None`` 488 489 found_transform = False 490 while ( 491 not isinstance(self.transpilation_sequence.steps[idx].dag, DAGCircuit) 492 and idx > 0 493 ): 494 idx = idx - 1 495 if idx >= 0: 496 found_transform = ( 497 self.transpilation_sequence.steps[idx].pass_type 498 == PassType.TRANSFORMATION 499 ) 500 501 if found_transform is False: 502 return self.transpilation_sequence.original_circuit 503 504 return dag_to_circuit(self.transpilation_sequence.steps[idx].dag) 505 506 def _get_pass_property_set(self, step): 507 if step.property_set_index is not None: 508 return self.transpilation_sequence.steps[ 509 step.property_set_index 510 ].property_set 511 512 return {} 513 514 def _build_pass_pad(self, index): 515 step = self.transpilation_sequence.steps[index] 516 pad = curses.newpad( 517 self._view_params["transpiler_pad_height"], 518 self._view_params["transpiler_pad_width"], 519 ) 520 pass_pad = TranspilerPassPad( 521 step, 522 self._get_pass_circuit(step), 523 self._get_pass_property_set(step), 524 self._view_params["transpiler_pad_height"], 525 self._view_params["transpiler_pad_width"], 526 pad, 527 ) 528 pass_pad.build_pad() 529 self._pass_pad_list[index] = pass_pad.pad 530 531 def add_step(self, step): 532 """Adds a step to the transpilation sequence. 533 534 Args: 535 step (TranspilationStep): `TranspilationStep` object to be added to the transpilation sequence. 536 """ 537 self._all_passes_data.append( 538 [ 539 step.name, 540 step.pass_type.value, 541 step.duration, 542 step.circuit_stats.depth, 543 step.circuit_stats.size, 544 step.circuit_stats.ops_1q, 545 step.circuit_stats.ops_2q, 546 step.circuit_stats.width, 547 ] 548 ) 549 550 def _get_all_passes_table(self): 551 """Generate and return the table containing all the transpiler passes. 552 553 Returns: 554 list: The list representing the table of all transpiler passes. 555 """ 556 # build from the transpilation sequence 557 # make table 558 pass_table = tabulate.tabulate( 559 headers=self._pass_table_headers, 560 tabular_data=self._all_passes_data, 561 tablefmt="simple_grid", 562 stralign="center", 563 numalign="center", 564 showindex="always", 565 ).splitlines() 566 567 return pass_table 568 569 def _get_changing_pass_list(self): 570 """Get the list of indices of passes that caused a change in the circuit. 571 572 Returns: 573 list: A list containing the indices of changing passes. 574 """ 575 pass_id_list = [] 576 for i in range(1, len(self.transpilation_sequence.steps)): 577 prev_step = self.transpilation_sequence.steps[i - 1] 578 curr_step = self.transpilation_sequence.steps[i] 579 if prev_step.circuit_stats != curr_step.circuit_stats: 580 pass_id_list.append(i) 581 return pass_id_list 582 583 def _get_all_passes_pad(self): 584 """Generate and return the pad containing all the transpiler passes. 585 586 Returns: 587 curses.pad: The pad containing all the transpiler passes. 588 """ 589 start_x = 4 590 table_width = 500 # for now 591 table_height = len(self._all_passes_table) + 1 592 pass_pad = curses.newpad(table_height, table_width) 593 594 header_height = 3 595 596 # centering is required for each row 597 for row in range(header_height): 598 offset = self._get_center( 599 table_width, len(self._all_passes_table[row][: table_width - 1]) 600 ) 601 pass_pad.addstr( 602 row, 603 start_x + offset, 604 self._all_passes_table[row][: table_width - 1], 605 curses.A_BOLD | self._colors["base_pass_title"], 606 ) 607 608 # generate a changing pass set to see which pass 609 # changed the circuit and which didn't 610 changing_pass_list = set(self._get_changing_pass_list()) 611 612 def _is_changing_pass_row(row): 613 # dashes only at even rows 614 if row % 2 == 0: 615 return False 616 index = (row - header_height) // 2 617 if index in changing_pass_list: 618 return True 619 return False 620 621 # now start adding the passes 622 for row in range(header_height, len(self._all_passes_table)): 623 offset = self._get_center( 624 table_width, len(self._all_passes_table[row][: table_width - 1]) 625 ) 626 highlight = 0 627 628 if _is_changing_pass_row(row): 629 highlight = curses.A_BOLD | self._colors["changing_pass"] 630 631 pass_pad.addstr( 632 row, 633 start_x + offset, 634 self._all_passes_table[row][: table_width - 1], 635 highlight, 636 ) 637 638 # populated pad with passes 639 return pass_pad 640 641 def _render_transpilation_pad(self, pass_pad, curr_row, curr_col, rows, cols): 642 """Function to render the pass pad. 643 644 NOTE : this is agnostic of whether we are passing the base pad 645 or the individual transpiler pass pad. Why? 646 Because we are not shifting the pad, we are just refreshing it. 647 648 Args: 649 pass_pad (curses.pad): The pad containing the individual pass details. 650 curr_row (int): Current row position. 651 curr_col (int): Current column position. 652 rows (int): Total number of rows in the terminal. 653 cols (int): Total number of columns in the terminal. 654 655 Returns: 656 None 657 """ 658 if not pass_pad: 659 return 660 661 # 4 rows for the title + curr_row (curr_row is the row of the pass) 662 title_height = 5 663 start_row = self._view_params["transpiler_start_row"] + title_height 664 665 # if we don't have enough rows 666 if start_row >= rows - 2: 667 return 668 669 # if we don't have enough columns 670 if self._view_params["transpiler_start_col"] >= cols - 6: 671 return 672 673 actual_width = pass_pad.getmaxyx()[1] 674 window_width = cols - self._view_params["transpiler_start_col"] 675 col_offset = (actual_width - window_width) // 2 676 677 pass_pad.noutrefresh( 678 curr_row, 679 col_offset + curr_col, 680 start_row, 681 self._view_params["transpiler_start_col"], 682 rows - 2, 683 cols - 6, 684 ) 685 686 def _pre_input(self, height, width): 687 """Function to render the pad before any input is entered 688 by the user 689 690 Args: 691 height (int): Number of rows 692 width (int): Number of cols 693 """ 694 pad_to_render = None 695 696 if self._view_params["status_type"] == "index": 697 pass_id = self._view_params["pass_id"] 698 if pass_id == -1: 699 pad_to_render = self._all_passes_pad 700 else: 701 if self._pass_pad_list[pass_id] is None: 702 self._build_pass_pad(pass_id) 703 pad_to_render = self._pass_pad_list[pass_id] 704 705 self._render_transpilation_pad( 706 pad_to_render, 707 self._view_params["curr_row"], 708 self._view_params["curr_col"], 709 height, 710 width, 711 ) 712 713 def _post_input(self, height, width): 714 """Render the pad after user input is entered. 715 716 Args: 717 height (int): Number of rows in the terminal. 718 width (int): Number of columns in the terminal. 719 720 Returns: 721 None 722 """ 723 pad_to_render = None 724 if self._view_params["status_type"] == "normal": 725 pad_to_render = self._all_passes_pad 726 elif self._view_params["status_type"] in ["index", "pass"]: 727 # using zero based indexing 728 pass_id = self._view_params["pass_id"] 729 if pass_id >= 0: 730 self._view_params["status_type"] = "pass" 731 if self._pass_pad_list[pass_id] is None: 732 self._build_pass_pad(pass_id) 733 pad_to_render = self._pass_pad_list[pass_id] 734 735 self._render_transpilation_pad( 736 pad_to_render, 737 self._view_params["curr_row"], 738 self._view_params["curr_col"], 739 height, 740 width, 741 ) 742 743 def display(self, stdscr): 744 """Display the Qiskit Transpiler Debugger on the terminal. 745 746 Args: 747 stdscr (curses.window): The main window object provided by the curses library. 748 749 Returns: 750 None 751 """ 752 key = 0 753 754 # Clear and refresh the screen for a blank canvas 755 stdscr.clear() 756 stdscr.refresh() 757 758 # initiate color 759 self._init_color() 760 761 # hide the cursor 762 curses.curs_set(0) 763 764 # reset view params 765 self._reset_view_params() 766 767 height, width = stdscr.getmaxyx() 768 self._refresh_base_windows(True, height, width) 769 770 # build the base transpiler pad using the transpilation sequence 771 self._all_passes_table = self._get_all_passes_table() 772 self._all_passes_pad = self._get_all_passes_pad() 773 self._pass_pad_list = [None] * len(self.transpilation_sequence.steps) 774 775 # build the individual pass pad list 776 # done, via add_step 777 assert len(self._pass_pad_list) > 0 778 779 while key not in [ord("q"), ord("Q")]: 780 height, width = stdscr.getmaxyx() 781 782 # Check for clearing 783 panel_initiated = ( 784 self._view_params["last_height"] + self._view_params["last_width"] > 0 785 ) 786 panel_resized = ( 787 self._view_params["last_width"] != width 788 or self._view_params["last_height"] != height 789 ) 790 791 if panel_initiated and panel_resized: 792 stdscr.clear() 793 794 self._view_params["overview_change"] = False 795 self._handle_keystroke(key) 796 797 whstr = f"Width: {width}, Height: {height}" 798 stdscr.addstr(0, 0, whstr) 799 800 # refresh the screen and then the windows 801 stdscr.noutrefresh() 802 self._refresh_base_windows(panel_resized, height, width) 803 804 # pre input rendering 805 self._pre_input(height, width) 806 807 # render the status bar , irrespective of width / height 808 # and get the input (if any) 809 self._status_bar = self._get_statusbar_win( 810 height, width, self._view_params["status_type"] 811 ) 812 self._status_bar.noutrefresh() 813 814 # post input rendering 815 self._post_input(height, width) 816 817 self._view_params["last_width"] = width 818 self._view_params["last_height"] = height 819 820 curses.doupdate() 821 822 # wait for the next input 823 key = stdscr.getch()
A class representing the CLI view for the Qiskit Transpiler Debugger.
CLIView()
19 def __init__(self): 20 """Initialize the CLIView object.""" 21 self._title = None 22 self._overview = None 23 24 self._all_passes_data = [] 25 self._all_passes_table = None 26 self._pass_table_headers = [ 27 "Pass Name", 28 "Pass Type", 29 "Runtime (ms)", 30 "Depth", 31 "Size", 32 "1q Gates", 33 "2q Gates", 34 "Width", 35 ] 36 # add the whitespace option 37 tabulate.PRESERVE_WHITESPACE = True 38 39 self._all_passes_pad = None 40 self._pass_pad_list = None 41 self._status_bar = None 42 self._title_string = "Qiskit Transpiler Debugger" 43 44 self._status_strings = { 45 "normal": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down | 'I': Index into a pass | 'H': Toggle overview | 'Q': Exit", 46 "index": " STATUS BAR | Enter the index of the pass you want to view : ", 47 "invalid": " STATUS BAR | Invalid input entered. Press Enter to continue.", 48 "out_of_bounds": " STATUS BAR | Number entered is out of bounds. Please Enter to continue.", 49 "pass": " STATUS BAR | Arrow keys: Scrolling | 'U/D': Page up/down | 'N/P': Move to next/previous | 'I': Index into a pass | 'B': Back to home | 'Q': Exit", 50 } 51 52 self._colors = { 53 "title": None, 54 "status": None, 55 "base_pass_title": None, 56 "changing_pass": None, 57 } 58 59 # define status object 60 self._reset_view_params() 61 62 # add the transpilation sequence 63 self.transpilation_sequence = None
Initialize the CLIView object.
def
add_step(self, step):
531 def add_step(self, step): 532 """Adds a step to the transpilation sequence. 533 534 Args: 535 step (TranspilationStep): `TranspilationStep` object to be added to the transpilation sequence. 536 """ 537 self._all_passes_data.append( 538 [ 539 step.name, 540 step.pass_type.value, 541 step.duration, 542 step.circuit_stats.depth, 543 step.circuit_stats.size, 544 step.circuit_stats.ops_1q, 545 step.circuit_stats.ops_2q, 546 step.circuit_stats.width, 547 ] 548 )
Adds a step to the transpilation sequence.
Args:
step (TranspilationStep): TranspilationStep
object to be added to the transpilation sequence.
def
display(self, stdscr):
743 def display(self, stdscr): 744 """Display the Qiskit Transpiler Debugger on the terminal. 745 746 Args: 747 stdscr (curses.window): The main window object provided by the curses library. 748 749 Returns: 750 None 751 """ 752 key = 0 753 754 # Clear and refresh the screen for a blank canvas 755 stdscr.clear() 756 stdscr.refresh() 757 758 # initiate color 759 self._init_color() 760 761 # hide the cursor 762 curses.curs_set(0) 763 764 # reset view params 765 self._reset_view_params() 766 767 height, width = stdscr.getmaxyx() 768 self._refresh_base_windows(True, height, width) 769 770 # build the base transpiler pad using the transpilation sequence 771 self._all_passes_table = self._get_all_passes_table() 772 self._all_passes_pad = self._get_all_passes_pad() 773 self._pass_pad_list = [None] * len(self.transpilation_sequence.steps) 774 775 # build the individual pass pad list 776 # done, via add_step 777 assert len(self._pass_pad_list) > 0 778 779 while key not in [ord("q"), ord("Q")]: 780 height, width = stdscr.getmaxyx() 781 782 # Check for clearing 783 panel_initiated = ( 784 self._view_params["last_height"] + self._view_params["last_width"] > 0 785 ) 786 panel_resized = ( 787 self._view_params["last_width"] != width 788 or self._view_params["last_height"] != height 789 ) 790 791 if panel_initiated and panel_resized: 792 stdscr.clear() 793 794 self._view_params["overview_change"] = False 795 self._handle_keystroke(key) 796 797 whstr = f"Width: {width}, Height: {height}" 798 stdscr.addstr(0, 0, whstr) 799 800 # refresh the screen and then the windows 801 stdscr.noutrefresh() 802 self._refresh_base_windows(panel_resized, height, width) 803 804 # pre input rendering 805 self._pre_input(height, width) 806 807 # render the status bar , irrespective of width / height 808 # and get the input (if any) 809 self._status_bar = self._get_statusbar_win( 810 height, width, self._view_params["status_type"] 811 ) 812 self._status_bar.noutrefresh() 813 814 # post input rendering 815 self._post_input(height, width) 816 817 self._view_params["last_width"] = width 818 self._view_params["last_height"] = height 819 820 curses.doupdate() 821 822 # wait for the next input 823 key = stdscr.getch()
Display the Qiskit Transpiler Debugger on the terminal.
Args: stdscr (curses.window): The main window object provided by the curses library.
Returns: None