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.

transpilation_sequence
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