src/qutip_qip/circuit/texrenderer.py
import osimport sysimport shutilimport tempfileimport warningsimport functoolsimport subprocessimport collections from ..operations import Gate __all__ = ["TeXRenderer", "CONVERTERS"] class TeXRenderer: """ Class to render the circuit in latex format. """ def __init__(self, qc): self.qc = qc self.N = qc.N self.num_cbits = qc.num_cbits self.gates = qc.gates self.input_states = qc.input_states self.reverse_states = qc.reverse_states self._latex_template = r""" \documentclass[border=3pt]{standalone} \usepackage[braket]{qcircuit} \begin{document} \Qcircuit @C=1cm @R=1cm { %s} \end{document} """ self._pdflatex = self._find_system_command(["pdflatex"]) self._pdfcrop = self._find_system_command(["pdfcrop"]) def _gate_label(self, gate): gate_label = gate.latex_str if gate.arg_label is not None: return r"%s(%s)" % (gate_label, gate.arg_label) return r"%s" % gate_label Function `latex_code` has a Cognitive Complexity of 95 (exceeds 20 allowed). Consider refactoring. def latex_code(self): """ Generate the latex code for the circuit. Returns ------- code: str The latex code for the circuit. """ rows = [] ops = self.gates col = [] for op in ops: if isinstance(op, Gate): gate = op col = [] _swap_processing = False for n in range(self.N + self.num_cbits): if gate.targets and n in gate.targets:Avoid deeply nested control flow statements. if len(gate.targets) > 1: if gate.name == "SWAP": if _swap_processing: col.append(r" \qswap \qw") continue distance = abs( gate.targets[1] - gate.targets[0] ) if self.reverse_states: distance = -distance col.append(r" \qswap \qwx[%d] \qw" % distance) _swap_processing = True elif ( self.reverse_states and n == max(gate.targets) ) or ( not self.reverse_states and n == min(gate.targets) ): col.append( r" \multigate{%d}{%s} " % ( len(gate.targets) - 1, self._gate_label(gate), ) ) else: col.append( r" \ghost{%s} " % (self._gate_label(gate)) ) elif gate.name == "CNOT": col.append(r" \targ ") elif gate.name == "CY": col.append(r" \targ ") elif gate.name == "CZ": col.append(r" \targ ") elif gate.name == "CS": col.append(r" \targ ") elif gate.name == "CT": col.append(r" \targ ") elif gate.name == "TOFFOLI": col.append(r" \targ ") else: col.append(r" \gate{%s} " % self._gate_label(gate)) elif gate.controls and n in gate.controls: control_tag = (-1 if self.reverse_states else 1) * ( gate.targets[0] - n ) col.append(r" \ctrl{%d} " % control_tag) elif ( gate.classical_controls and (n - self.N) in gate.classical_controls ): control_tag = (-1 if self.reverse_states else 1) * ( gate.targets[0] - n ) col.append(r" \ctrl{%d} " % control_tag) elif not gate.controls and not gate.targets: # global gateAvoid deeply nested control flow statements. if (self.reverse_states and n == self.N - 1) or ( not self.reverse_states and n == 0 ): col.append( r" \multigate{%d}{%s} " % ( self.N - 1, self._gate_label(gate), ) ) else: col.append( r" \ghost{%s} " % (self._gate_label(gate)) ) else: col.append(r" \qw ") else: measurement = op col = [] for n in range(self.N + self.num_cbits): if n in measurement.targets: col.append(r" \meter") elif (n - self.N) == measurement.classical_store: sgn = 1 if self.reverse_states else -1 store_tag = sgn * (n - measurement.targets[0]) col.append(r" \qw \cwx[%d] " % store_tag) else: col.append(r" \qw ") col.append(r" \qw ") rows.append(col) input_states_quantum = [ r"\lstick{\ket{" + x + "}}" if x is not None else "" for x in self.input_states[: self.N] ] input_states_classical = [ r"\lstick{" + x + "}" if x is not None else "" for x in self.input_states[self.N :] ] input_states = input_states_quantum + input_states_classical code = "" n_iter = ( reversed(range(self.N + self.num_cbits)) if self.reverse_states else range(self.N + self.num_cbits) ) for n in n_iter: code += r" & %s" % input_states[n] for m in range(len(ops)): code += r" & %s" % rows[m][n] code += r" & \qw \\ " + "\n" return self._latex_template % code def raw_img(self, file_type="png", dpi=100): return self.image_from_latex(self.latex_code(), file_type, dpi) @classmethod def _run_command(self, command, *args, **kwargs): """ Run a command with stdout explicitly thrown away, raising `RuntimeError` with the system error message if the command returned a non-zero exit code. """ try: return subprocess.run( command, *args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, **kwargs, ) except subprocess.CalledProcessError as e: raise RuntimeError(e.stderr.decode(sys.stderr.encoding)) from None def _force_remove(self, *filenames): """`rm -f`: try to remove a file, ignoring errors if it doesn't exist.""" for filename in filenames: try: os.remove(filename) except FileNotFoundError: pass @classmethod def _test_convert_is_imagemagick(self): """ Test to see if the `convert` command behaves like we'd expect ImageMagick to. On Windows if ImageMagick is not installed then `convert` may refer to a system utility. """ try: # Don't use `capture_output` because we're still supporting Python 3.6 process = subprocess.run( ("convert", "-version"), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) return "imagemagick" in process.stdout.decode("utf-8").lower() except FileNotFoundError: return False @staticmethod def _find_system_command(names): """ Given a list of possible system commands (as strings), return the first one which has a locatable executable form, or `None` if none of them do. We also check some special cases of shadowing (e.g. ImageMagick 6's `convert` is also a Windows system utility) to try and catch false-positives. """ for name in names: if shutil.which(name) is not None: is_valid = _SPECIAL_CASES.get(name, lambda: True)() if is_valid: return name return None def _crop_pdf(self, filename): if self._pdfcrop is not None: """Crop the pdf file `filename` in place.""" temporary = ".tmp." + filename self._run_command((self._pdfcrop, filename, temporary)) # Windows does not allow renaming to an existing file (but unix does). self._force_remove(filename) os.rename(temporary, filename) else: # Warn, but do not raise - we can recover from a failed crop. warnings.warn( "Could not locate system 'pdfcrop':" " image output may have additional margins." ) @staticmethod def _convert_pdf(file_stem, dpi=None): """ 'Convert' to pdf: since LaTeX outputs a PDF file, there's nothing to do. """ if dpi is not None: warnings.warn("argument dpi is ignored for pdf output.") with open(file_stem + ".pdf", "rb") as file: return file.read() @classmethod def _make_converter(self, configuration): """ Create the actual conversion function of signature file_stem: str -> 'T, where 'T is data in the format to be converted to. """ which = self._find_system_command(configuration.executables) if which is None: return None mode = "rb" if configuration.binary else "r" def converter(file_stem, dpi=100): """ Convert a file located in the current directory named `<file_stem>.pdf` to an image format with the name `<file_stem>.xxx`, where `xxx` is converter-dependent. Parameters ---------- file_stem : str The basename of the PDF file to be converted. dpi : int/float Image density in dots per inch. Ignored for SVG. """ in_file = file_stem + ".pdf" out_file = file_stem + "." + configuration.file_type if "-density" in configuration.arguments: arguments = list(configuration.arguments) arguments[arguments.index("-density") + 1] = str(dpi) else: arguments = configuration.arguments self._run_command((which, *arguments, in_file, out_file)) with open(out_file, mode) as file: return file.read() return converter def image_from_latex(self, code, file_type="png", dpi=100): """ Convert the LaTeX `code` into an image format, defined by the `file_type`. Returns a string or bytes object, depending on whether the requested type is textual (e.g. svg) or binary (e.g. png). The known file types are in keys in this module's `CONVERTERS` dictionary. Parameters ---------- code: str LaTeX code representing the circuit to be converted. file_type: str ("png") The file type that the image should be returned in. Returns ------- image: str or bytes An encoded version of the image. Whether the output type is str or bytes depends on whether the requested image format is textual or binary. """ if self._pdflatex is not None: filename = "qcirc" # Arbitrary and internal. # We do all the image conversion in a temporary directory to prevent # leftover files if something goes wrong (or we get a # KeyboardInterrupt) during conversion. previous_dir = os.getcwd() with tempfile.TemporaryDirectory() as temporary_dir: try: os.chdir(temporary_dir) with open(filename + ".tex", "w") as file: file.write(code) try: self._run_command( ( self._pdflatex, "-interaction", "batchmode", filename, ) ) except RuntimeError as e: message = ( "pdflatex failed." " Perhaps you do not have it installed, or you are" " missing the LaTeX package 'qcircuit'." ) message += ( "The latex code is printed below. " "Please try to compile locally using pdflatex:\n" + code ) raise RuntimeError(message) from e self._crop_pdf(filename + ".pdf") if file_type in _MISSING_CONVERTERS: dependency = _MISSING_CONVERTERS[file_type] message = "".join( [ "Could not find system ", dependency, ".", " Image conversion to '", file_type, "'", " is not available.", ] ) raise RuntimeError(message) if file_type not in CONVERTERS: raise ValueError( "".join( ["Unknown output format: '", file_type, "'."] ) ) out = CONVERTERS[file_type](filename, dpi) finally: # Leave the temporary directory before it is removed (necessary # on Windows, but it doesn't hurt on POSIX). os.chdir(previous_dir) return out else: raise RuntimeError("Could not find system 'pdflatex'.") # Record type to hold definitions of possible conversions - this is just for# reading convenience._ConverterConfiguration = collections.namedtuple( "_ConverterConfiguration", ["file_type", "dependency", "executables", "arguments", "binary"],)CONVERTERS = {"pdf": TeXRenderer._convert_pdf}_MISSING_CONVERTERS = {}_CONVERTER_CONFIGURATIONS = [ _ConverterConfiguration( "png", "ImageMagick", ["magick", "convert"], arguments=("-density", "100"), binary=True, ), _ConverterConfiguration( "svg", "pdf2svg", ["pdf2svg"], arguments=(), binary=False ),]_SPECIAL_CASES = { "convert": TeXRenderer._test_convert_is_imagemagick,}for configuration in _CONVERTER_CONFIGURATIONS: # Make the converter using a higher-order function, because if we defined a # function in the loop, it would be easy to later introduce bugs due to # leaky closures over loop variables. converter = TeXRenderer._make_converter(configuration) if converter: CONVERTERS[configuration.file_type] = converter else: _MISSING_CONVERTERS[configuration.file_type] = configuration.dependency