src/qutip_qip/circuit/text_renderer.py
"""Module for rendering a quantum circuit in text format.""" from math import ceilfrom typing import List from .base_renderer import BaseRenderer, StyleConfigfrom ..operations import Gate, Measurementfrom . import QubitCircuit __all__ = [ "TextRenderer",] class TextRenderer(BaseRenderer): """ A class to render a quantum circuit in text format. """ def __init__(self, qc: QubitCircuit, **style): # user defined style style = {} if style is None else style style["gate_margin"] = 0 self.style = StyleConfig(**style) super().__init__(self.style) self._qc = qc self._qwires = qc.N self._cwires = qc.num_cbits self._layer_list = [[] for _ in range(self._qwires + self._cwires)] self._render_strs = { "top_frame": [" "] * (self._qwires + self._cwires), "mid_frame": ["──"] * self._qwires + ["══"] * self._cwires, "bot_frame": [" "] * (self._qwires + self._cwires), } def _adjust_layer_pad(self, wire_list: List[int], xskip: int): """ Adjust the layers by filling the empty spaces with respective characters """ for wire in wire_list: self._render_strs["top_frame"][wire] += " " * ( xskip - len(self._render_strs["top_frame"][wire]) ) self._render_strs["bot_frame"][wire] += " " * ( xskip - len(self._render_strs["bot_frame"][wire]) ) if wire < self._qwires: self._render_strs["mid_frame"][wire] += "─" * ( xskip - len(self._render_strs["mid_frame"][wire]) ) else: self._render_strs["mid_frame"][wire] += "═" * ( xskip - len(self._render_strs["mid_frame"][wire]) ) def _add_wire_labels(self): """ Add wire labels to the circuit """ if self.style.wire_label is None: default_labels = [f"q{i}" for i in range(self._qwires)] + [ f"c{i}" for i in range(self._cwires) ] else: default_labels = ( self.style.wire_label[self._cwires :] + self.style.wire_label[: self._cwires] ) max_label_len = max([len(label) for label in default_labels]) for i, label in enumerate(default_labels): self._render_strs["mid_frame"][i] = ( f" {label} " + " " * (max_label_len - len(label)) + ":" + self._render_strs["mid_frame"][i] ) update_len = len(self._render_strs["mid_frame"][i]) self._render_strs["top_frame"][i] = " " * update_len self._render_strs["bot_frame"][i] = " " * update_len self._layer_list[i].append(update_len) def _draw_singleq_gate(self, gate_name: str): """ Draw a single qubit gate Parameters ---------- gate : Gate The gate to be drawn. Returns ------- tuple The parts of the gate to be drawn. The parts are the top_frame, mid_frame, and bot_frame. int The width of the gate. """ lid_seg = "─" * (ceil(self.style.gate_pad) * 2 + len(gate_name)) pad = " " * ceil(self.style.gate_pad) top_frame = f" ┌{lid_seg}┐ " mid_frame = f"─┤{pad}{gate_name}{pad}├─" bot_frame = f" └{lid_seg}┘ " # check for equal part lengths assert len(top_frame) == len(mid_frame) == len(bot_frame) return (top_frame, mid_frame, bot_frame), len(top_frame) def _draw_multiq_gate(self, gate: Gate, gate_text: str): """ Draw a multi qubit gate Parameters ---------- gate : Gate The gate to be drawn. Returns ------- tuple The parts of the gate to be drawn. i.e. the top_frame, mid_frame, mid_connect, mid_connect_label, and bot_frame. int The width of the gate. """ lid_seg = "─" * (ceil(self.style.gate_pad) * 2 + len(gate_text)) pad = " " * ceil(self.style.gate_pad) top_frame = f" ┌{lid_seg}┐ " bot_frame = f" └{lid_seg}┘ " mid_frame = f" │{pad}{' ' * len(gate_text)}{pad}│ " mid_connect = f"─┤{pad}{' ' * len(gate_text)}{pad}├─" mid_connect_label = f"─┤{pad}{gate_text}{pad}├─" # Adjust top_frame or bottom if there is a control wire if gate.controls: sorted_controls = sorted(gate.controls) sorted_targets = sorted(gate.targets) mid_index = len(bot_frame) // 2 top_frame = ( (top_frame[:mid_index] + "┴" + top_frame[mid_index + 1 :]) if sorted_controls[-1] > sorted_targets[0] else top_frame ) bot_frame = ( (bot_frame[:mid_index] + "┬" + bot_frame[mid_index + 1 :]) if sorted_controls[0] < sorted_targets[-1] else bot_frame ) # check for equal part lengths assert ( len(top_frame) == len(mid_frame) == len(bot_frame) == len(mid_connect) == len(mid_connect_label) ) return ( top_frame, mid_frame, mid_connect, mid_connect_label, bot_frame, ), len(top_frame) def _draw_measurement_gate(self, measurement: Measurement): """ Draw a measurement gate """ parts, width = self._draw_singleq_gate("M") top_frame, mid_frame, bot_frame = parts # adjust top_frame or bottom according the placement of the classical wire mid_index = len(bot_frame) // 2 if measurement.classical_store + self._qwires > measurement.targets[0]: bot_frame = ( bot_frame[:mid_index] + "╥" + bot_frame[mid_index + 1 :] ) else: top_frame = ( top_frame[:mid_index] + "╨" + top_frame[mid_index + 1 :] ) return (top_frame, mid_frame, bot_frame), width def _update_cbridge(self, gate: Gate, wire_list: List[int], width: int): """ Update the render strings for the control bridge Parameters ---------- wire_list : list The list of control wires the gate is acting on and all the wires in between. xskip : float The horizontal value for getting to requested layer. width : int The width of the gate. """ bar_conn = " " * (width // 2) + "║" + " " * (width // 2) mid_bar_conn = "─" * (width // 2) + "║" + "─" * (width // 2) mid_bar_classical_conn = "═" * (width // 2) + "║" + "═" * (width // 2) classical_conn = "═" * (width // 2) + "╩" + "═" * (width // 2) for wire in wire_list: if wire == gate.targets[0]: continue Similar blocks of code found in 2 locations. Consider refactoring. if wire == self._qwires + gate.classical_store: self._render_strs["top_frame"][wire] += bar_conn self._render_strs["mid_frame"][wire] += classical_conn self._render_strs["bot_frame"][wire] += " " * len(bar_conn) else: self._render_strs["top_frame"][wire] += bar_conn self._render_strs["bot_frame"][wire] += bar_conn # check if the non-store wire is a classical wire or not if wire > self._qwires: self._render_strs["mid_frame"][ wire ] += mid_bar_classical_conn else: self._render_strs["mid_frame"][wire] += mid_bar_conn def _update_singleq(self, wire_list, parts): """ Update the render strings for single qubit gates """ top_frame, mid_frame, bot_frame = partsSimilar blocks of code found in 4 locations. Consider refactoring. for wire in wire_list: self._render_strs["top_frame"][wire] += top_frame self._render_strs["mid_frame"][wire] += mid_frame self._render_strs["bot_frame"][wire] += bot_frame def _update_target_multiq( self, gate: Gate, wire_list: List[int], parts: List[str] ): """ Update the render strings for part of the multi qubit gate drawn on the target wires. Parameters ---------- gate : Gate The gate to be drawn. wire_list : list The list of target wires the gate is acting on and all the wires in between. xskip : float The horizontal value for getting to requested layer. parts : tuple The parts of the gate to be drawn. """ top_frame, mid_frame, mid_connect, mid_connect_label, bot_frame = parts for i, wire in enumerate(wire_list):Similar blocks of code found in 4 locations. Consider refactoring. if len(gate.targets) == 1: self._render_strs["top_frame"][wire] += top_frame self._render_strs["mid_frame"][wire] += mid_connect_label self._render_strs["bot_frame"][wire] += bot_frameSimilar blocks of code found in 4 locations. Consider refactoring. elif i == 0 and wire in gate.targets: self._render_strs["top_frame"][wire] += mid_frame self._render_strs["mid_frame"][wire] += mid_connect_label self._render_strs["bot_frame"][wire] += bot_frameSimilar blocks of code found in 4 locations. Consider refactoring. elif i == len(wire_list) - 1 and wire in gate.targets: self._render_strs["top_frame"][wire] += top_frame self._render_strs["mid_frame"][wire] += mid_connect self._render_strs["bot_frame"][wire] += mid_frame else: self._render_strs["top_frame"][wire] += mid_frame self._render_strs["mid_frame"][wire] += mid_frame self._render_strs["bot_frame"][wire] += mid_frame Function `_update_qbridge` has a Cognitive Complexity of 23 (exceeds 20 allowed). Consider refactoring. def _update_qbridge( self, gate: Gate, wire_list_control: List[int], width: int, is_top: bool, ): """ Update the render strings for part of the multi qubit gate drawn on the control wires. Parameters ---------- gate : Gate The gate to be drawn. wire_list_control : list The list of control wires the gate is acting on and all the wires in between. xskip : float The horizontal value for getting to requested layer. width : int The width of the gate. is_top_closer : bool If the top of the gate is closer to the first control wires. """ bar_conn = " " * (width // 2) + "│" + " " * (width // 2 - 1) mid_bar_conn = "─" * (width // 2) + "│" + "─" * (width // 2 - 1) node_conn = "─" * (width // 2) + "█" + "─" * (width // 2 - 1) for wire in wire_list_control: if wire not in gate.targets:Similar blocks of code found in 4 locations. Consider refactoring. if wire in gate.controls: # check if the control wire is the first or last control wire. # used in cases of multiple control wiresSimilar blocks of code found in 4 locations. Consider refactoring. if ( wire == wire_list_control[0] or wire == wire_list_control[-1] ): self._render_strs["mid_frame"][wire] += node_conn self._render_strs["top_frame"][wire] += ( bar_conn if not is_top else " " * len(bar_conn) ) self._render_strs["bot_frame"][wire] += ( bar_conn if is_top else " " * len(bar_conn) ) else: self._render_strs["top_frame"][wire] += bar_conn self._render_strs["mid_frame"][wire] += node_conn self._render_strs["bot_frame"][wire] += bar_conn else: self._render_strs["top_frame"][wire] += bar_conn self._render_strs["mid_frame"][wire] += mid_bar_conn self._render_strs["bot_frame"][wire] += bar_conn def _update_swap_gate(self, wire_list: List[int]): """ Update the render strings for the SWAP gate """ width = 4 * ceil(self.style.gate_pad) + 1 cross_conn = "─" * (width // 2) + "╳" + "─" * (width // 2) bar_conn = " " * (width // 2) + "│" + " " * (width // 2) mid_bar_conn = "─" * (width // 2) + "│" + "─" * (width // 2) for wire in wire_list: if wire == wire_list[-1]: self._render_strs["top_frame"][wire] += " " * len(bar_conn) self._render_strs["mid_frame"][wire] += cross_conn self._render_strs["bot_frame"][wire] += bar_connSimilar blocks of code found in 4 locations. Consider refactoring. elif wire == wire_list[0]: self._render_strs["top_frame"][wire] += bar_conn self._render_strs["mid_frame"][wire] += cross_conn self._render_strs["bot_frame"][wire] += " " * len(bar_conn) else: self._render_strs["top_frame"][wire] += bar_conn self._render_strs["mid_frame"][wire] += mid_bar_conn self._render_strs["bot_frame"][wire] += bar_conn Function `layout` has a Cognitive Complexity of 30 (exceeds 20 allowed). Consider refactoring. def layout(self): """ Layout the circuit """ self._add_wire_labels() for gate in self._qc.gates: if isinstance(gate, Gate): gate_text = ( gate.arg_label if gate.arg_label is not None else gate.name ) # generate the parts, width and wire_list for the gates if isinstance(gate, Measurement): wire_list = list(range(gate.targets[0] + 1)) + list( range( gate.classical_store + self._qwires, self._qwires + self._cwires, ) ) parts, width = self._draw_measurement_gate(gate) elif len(gate.targets) == 1 and gate.controls is None: wire_list = gate.targets parts, width = self._draw_singleq_gate(gate_text) elif gate.name == "SWAP": wire_list = list( range(min(gate.targets), max(gate.targets) + 1) ) width = 4 * ceil(self.style.gate_pad) + 1 else: merged_wire = sorted(gate.targets + (gate.controls or [])) wire_list = list(range(merged_wire[0], merged_wire[-1] + 1)) parts, width = self._draw_multiq_gate(gate, gate_text) # update the render strings for the gate layer = max(len(self._layer_list[i]) for i in wire_list) xskip = self._get_xskip(wire_list, layer) self._adjust_layer_pad(wire_list, xskip) self._manage_layers(width, wire_list, layer, xskip) if isinstance(gate, Measurement): self._update_singleq(gate.targets, parts) self._update_cbridge(gate, wire_list, width) elif len(gate.targets) == 1 and gate.controls is None: self._update_singleq(wire_list, parts) elif gate.name == "SWAP": self._update_swap_gate(wire_list) else: sorted_targets = sorted(gate.targets) self._update_target_multiq( gate, list(range(sorted_targets[0], sorted_targets[-1] + 1)), parts, ) if gate.controls: sorted_controls = sorted(gate.controls) # check if there is control wire above the gate top is_top = sorted_controls[-1] > sorted_targets[0] is_bot = sorted_controls[0] < sorted_targets[-1] if is_top: self._update_qbridge( gate, list( range( sorted_targets[0], sorted_controls[-1] + 1, ) ), width, is_top, ) if is_bot: self._update_qbridge( gate, list( range( sorted_controls[0], sorted_targets[-1] + 1, ) ), width, not is_bot, ) max_layer_len = max(sum(layer) for layer in self._layer_list) self._adjust_layer_pad( list(range(self._qwires + self._cwires)), max_layer_len + self.style.end_wire_ext, ) self.print_circuit() def print_circuit(self): """ Print the circuit """ Identical blocks of code found in 2 locations. Consider refactoring. for i in range(self._qwires - 1, -1, -1): print(self._render_strs["top_frame"][i]) print(self._render_strs["mid_frame"][i]) print(self._render_strs["bot_frame"][i]) Identical blocks of code found in 2 locations. Consider refactoring. for i in range(self._qwires + self._cwires - 1, self._qwires - 1, -1): print(self._render_strs["top_frame"][i]) print(self._render_strs["mid_frame"][i]) print(self._render_strs["bot_frame"][i]) def save(self, filename: str): """ Save the circuit to a file Parameters ---------- filename : str The name of the file to save the circuit to. """ if not filename.endswith(".txt"): filename += ".txt" with open(filename, "w", encoding="utf-8") as file:Identical blocks of code found in 2 locations. Consider refactoring. for i in range(self._qwires - 1, -1, -1): file.write(self._render_strs["top_frame"][i] + "\n") file.write(self._render_strs["mid_frame"][i] + "\n") file.write(self._render_strs["bot_frame"][i] + "\n") Identical blocks of code found in 2 locations. Consider refactoring. for i in range( self._qwires + self._cwires - 1, self._qwires - 1, -1 ): file.write(self._render_strs["top_frame"][i] + "\n") file.write(self._render_strs["mid_frame"][i] + "\n") file.write(self._render_strs["bot_frame"][i] + "\n")