#!/usr/bin/env python3

# Arline Benchmarks
# Copyright (C) 2019-2020 Turation Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.


# Importing pylatex package - tool for automated LaTeX generation.
from pylatex import (
    Document,
    Section,
    Subsection,
    Command,
    Itemize,
    Enumerate,
    Math,
    Alignat,
    Hyperref,
    Figure,
    LineBreak,
    NewPage,
    NewLine,
    TextColor,
    FootnoteText,
    Label,
    PageStyle,
    Head,
    Foot,
    simple_page_number,
)
from pylatex.section import Chapter
from pylatex.utils import italic, NoEscape
from importlib_metadata import version
from psutil import virtual_memory
from shutil import rmtree
from os import makedirs, path
import arline_benchmarks

import platform
import os
import cpuinfo
import pandas as pd
import numpy as np
import re
import argparse


class LatexReport:
    r"""Latex Report Analyser Class

    **Description:**

        Calculates additional metrics based on .csv report file.
        These metrics are required for automated analytics and text generation in .tex report file.
    """

    def __init__(self, *args, **kwargs):
        self.df = None

    def read_report_file(self, fname):
        if not os.path.isfile(fname):
            raise FileNotFoundError("Benchmarking report file {:} is not found".format(fname))

        self.df = pd.read_csv(fname)

    def get_hardware_list(self):
        hardw_names = self.df["Pipeline Output Hardware Name"]
        hardw_list = set(hardw_names)
        return hardw_list

    def get_kak_hardware_list(self):
        hardw_names = self.df["Pipeline Output Hardware Name"]
        hardw_qubits = self.df["Pipeline Output Number of Qubits"]
        filter = hardw_qubits == 2
        hardw_names = hardw_names[filter]
        hardw_list = set(hardw_names)
        return hardw_list

    def get_not_kak_hardware_list(self):
        hardw_names = self.df["Pipeline Output Hardware Name"]
        hardw_qubits = self.df["Pipeline Output Number of Qubits"]
        filter = hardw_qubits != 2
        hardw_names = hardw_names[filter]
        hardw_list = set(hardw_names)
        return hardw_list

    def get_target_list(self):
        targ_list = self.df["Test Target Generator Name"].unique()
        return targ_list

    def get_not_kak_target_list(self):
        num_qubs = self.df["Pipeline Output Number of Qubits"]
        targ_list = self.df["Test Target Generator Name"][num_qubs != 2].unique()
        return targ_list

    def get_kak_target_list(self):
        num_qubs = self.df["Pipeline Output Number of Qubits"]
        targ_list = self.df["Test Target Generator Name"][num_qubs == 2].unique()
        return targ_list

    def get_frameworks_list(self):
        frameworks_list = self.df["Pipeline ID"].unique()
        return frameworks_list

    def get_qubit_number(self, hardware_name):
        idx = self.df.index[self.df["Pipeline Output Hardware Name"] == hardware_name][0]
        qub_number = self.df["Gate Chain Number of Qubits"][idx]
        return qub_number

    def get_sum_agg_data(self, hardware, target):
        filter1 = self.df["Test Target Generator Name"] == target
        filter2 = self.df["Pipeline Output Hardware Name"] == hardware
        df = self.df[filter1][filter2]
        sum_data = df.groupby(by=["Pipeline ID"]).aggregate(np.sum)
        sum_data = sum_data.reset_index()
        return sum_data

    def get_min_exec_time_framework(self, hardware, target):
        df = self.get_sum_agg_data(hardware, target)
        idx = df["Execution Time"].idxmin()
        framework = df["Pipeline ID"][idx]
        return framework

    def get_max_exec_time_framework(self, hardware, target):
        df = self.get_sum_agg_data(hardware, target)
        idx = df["Execution Time"].idxmax()
        framework = df["Pipeline ID"][idx]
        return framework

    def get_pipelines_summary(self):
        pipelines_summary = []
        frameworks = self.get_frameworks_list()
        for framework in frameworks:
            _df = self.df[self.df["Pipeline ID"] == framework]["Stage ID"]
            stages = list(_df.unique())
            stages.remove("target_analysis")
            pipelines_summary.append({"pipeline": framework, "stages": stages})
        return pipelines_summary

    def target_class_display_name(self, target):
        split_names = target.split("_")
        if "random" in split_names:
            if "CliffordTAll2All" in split_names:
                targ_name = "Random Circuits from [Clifford + T] Gate Set"
                targ_name_lower_case = "random circuits from [Clifford + T] gate set"
            elif "chain" and "cnot" and "u3" in split_names:
                targ_name = "Random Circuits from [CNOT, U$_3$] Gate Set"
                targ_name_lower_case = "random circuits from [CNOT, U$_3$] gate set"
            else:
                targ_name = "Some Random Circuits"
                targ_name_lower_case = "some random circuits"

        elif target == "QASM":
            targ_name = "QASM Circuits for Arithmetic Blocks"
            targ_name_lower_case = "QASM circuits for arithmetic blocks"
        else:
            targ_name = target
            targ_name_lower_case = target
        return targ_name, targ_name_lower_case

    def get_target_summary(self, target, hardware=None, stage_id="target_analysis", framework=None):
        # filter by particular target class
        filter = self.df["Test Target Generator Name"] == target
        df = self.df[filter]
        if hardware is not None:
            filter = df["Pipeline Output Hardware Name"] == hardware
            df = df[filter]
        if framework is not None:
            filter = df["Pipeline ID"] == framework
            df = df[filter]
        num_targets = len(df["Test Target ID"].unique())
        targ_df = df[df["Stage ID"] == stage_id]

        gates_list = targ_df.filter(regex=("Count of .* Gates"), axis=1)
        g_count_sum = gates_list.sum()
        g_count_sum = g_count_sum[g_count_sum != 0]
        g_frequencies = g_count_sum / g_count_sum.sum()

        g_names = g_frequencies.index.to_list()

        g_names = [re.search("Count of (.*) Gates", g).group(1) for g in g_names]

        # Assumes that all targets for given class have the same number of qubits!
        mean_num_qubits = targ_df["Gate Chain Number of Qubits"].mean()
        mean_depth = targ_df["Depth"].mean()
        mean_1q_gate_count = targ_df["Single-Qubit Gate Count"].mean()
        mean_2q_gate_count = targ_df["Two-Qubit Gate Count"].mean()
        mean_total_gate_count = targ_df["Total Gate Count"].mean()
        target_summary = {
            "gate_frequencies": g_frequencies,
            "gate_names": g_names,
            "mean_num_qubits": mean_num_qubits,
            "num_targets": num_targets,
            "mean_depth": mean_depth,
            "mean_total_gate_count": mean_total_gate_count,
            "mean_two_qubit_gate_count": mean_2q_gate_count,
            "mean_one_qubit_gate_count": mean_1q_gate_count,
        }

        return target_summary

    def max_result_by_feature(self, feature, stage_id, hardware, target):
        """ Examples of features: Depth,
                                  Sngle-Qubit Gate Count,
                                  Two-Qubit Gate Count,
                                  Total Gate Count,
                                  Execution time """
        df = self.df
        conditions = {
            "Pipeline Output Hardware Name": hardware,
            "Stage ID": stage_id,
            "Test Target Generator Name": target,
        }
        for key in conditions.keys():
            cond = conditions[key]
            df = df.loc[df[key] == cond]

        max_feature_val = df[feature].max()
        idx = df[feature].idxmax()
        circuit_id = df["Test Target ID"][idx]
        pipeline_name = df["Pipeline ID"][idx]
        return max_feature_val, circuit_id, pipeline_name

    def min_result_by_feature(self, feature, stage_id, hardware, targets_class):
        """ Examples of features: Depth,
                                  1-Qubit Gate Count,
                                  2-Qubit Gate Count,
                                  Total Gate Count,
                                  Execution time """
        df = self.df
        conditions = {
            "Pipeline Output Hardware Name": hardware,
            "Stage ID": stage_id,
            "Test Target Generator Name": target,
        }

        for key in conditions.keys():
            cond = conditions[key]
            df = df.loc[df[key] == cond]

        min_feature_val = df[feature].min()
        idx = df[feature].idxmin()
        circuit_id = df["Test Target ID"][idx]
        pipeline_name = df["Pipeline ID"][idx]

        return min_feature_val, circuit_id, pipeline_name

    def groupby_framework_mean(self, stage_id, hardware, targets_class):
        df = self.df
        conditions = {
            "Pipeline Output Hardware Name": hardware,
            "Stage ID": stage_id,
            "Test Target Generator Name": target,
        }
        for key in conditions.keys():
            cond = conditions[key]
            df = df.loc[df[key] == cond]

        df_mean = df.groupby(by=["Pipeline ID"]).mean()
        return df_mean

    def get_df_mean_by_hardware(self, feature, hardw_list):
        df = self.df
        df = df[df["Pipeline Output Hardware Name"].isin(hardw_list)]
        df_mean_by_hardware = df.groupby(by=["Pipeline ID", "Pipeline Output Hardware Name"]).aggregate(np.mean)
        df_mean_by_hardware = df_mean_by_hardware.reset_index()
        heat_df = df_mean_by_hardware.pivot(
            index="Pipeline Output Hardware Name", columns="Pipeline ID", values=feature
        )
        return heat_df


geometry_options = {
    "bindingoffset": "0.2in",
    "left": "1in",
    "right": "1in",
    "top": "1in",
    "bottom": "1in",
    "footskip": "0.25in",
}


if __name__ == "__main__":
    # Generates .tex report based on .csv report file. Then compiles .tex into .pdf report.
    parser = argparse.ArgumentParser(description="Plot curves", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("--input_data_dir", "-i", type=str, help="Directory with generated benchmark data")
    parser.add_argument("--output_data_dir", "-o", type=str, help="Directory for Latex output")
    args = parser.parse_args()

    print("Generating LaTeX report, please wait ...")
    latex_report = LatexReport()
    fname = os.path.join(args.input_data_dir, "output/gate_chain_report.csv")
    fname = os.path.abspath(fname)

    latex_report.read_report_file(fname)

    frameworks = latex_report.get_frameworks_list()
    contains_qiskit = "QiskitPl" in frameworks  # Pipeline Name
    contains_cirq = "CirqPl" in frameworks  # Pipeline Name

    final_stage = "compression"

    hardware_list = latex_report.get_hardware_list()
    # Document with `\maketitle` command activated
    output_dir = path.join(args.output_data_dir, "latex")
    rmtree(output_dir, ignore_errors=True)
    makedirs(output_dir)
    filepath = os.path.join(output_dir, "benchmark_report")
    filepath = os.path.abspath(filepath)
    doc = Document(documentclass="report", geometry_options=geometry_options, default_filepath=filepath)
    doc.preamble.append(NoEscape(r"\linespread{1.5}"))

    doc.preamble.append(
        Command(
            "title",
            NoEscape(
                r"""\textbf{Arline: Benchmarks} \\
                Automated benchmarking platform for quantum compilers, quantum hardware and quantum algorithms \\
                \vspace{2cm}
                \normalsize{\textbf{Automatically generated report}} \\
                \vspace{0.5cm}
                \normalsize{\textbf{by Arline Benchmarks}}
                \vspace{2cm}
                """
            ),
        )
    )
    doc.preamble.append(Command("author", "https://github.com/ArlineQ/ArlineBenchmarks"))
    doc.preamble.append(Command("date", NoEscape(r"\today")))
    doc.append(NoEscape(r"\maketitle"))
    doc.append(NoEscape(r"\tableofcontents"))

    header = PageStyle("header")
    # Create right header
    with header.create(Head("R")):
        header.append(NoEscape(r"""\leftmark"""))
    with header.create(Foot("R")):
        header.append(simple_page_number())
    with header.create(Foot("L")):
        header.append(
            NoEscape(
                r"""\footnotesize{\textbf{Automatically generated report by Arline Benchmarks \\
                https://github.com/ArlineQ/ArlineBenchmarks}}"""
            )
        )
    doc.preamble.append(header)
    doc.change_document_style("header")

    # Add stuff to the document
    with doc.create(Chapter(r"""Overview""")):
        doc.append(
            NoEscape(
                r"""Quantum compilation is a problem of translating quantum algorithm to the set of low-level
                hardware instructions to be executed on the quantum processor. Efficient compilation and circuit
                optimisation (finding an optimal sequence of gates for the desired quantum computation) is immense
                importance for practical applications and is necessary for further progress towards scalable quantum
                computation.\bigskip

                \noindent{The importance of optimal quantum compilation stems from the fact that noisy
                intermediate-scale quantum (NISQ) devices suffer from unavoidable noise caused by individual gates. 
                Extreme susceptibility of quantum computation to noise is the crucial problem that hinders
                the development of large-scale quantum computers. By the means of optimising gate count in the quantum
                circuit, it is possible to significantly reduce hardware errors.} \bigskip

                \noindent{Optimal (or near-optimal) circuit compilation is an extremely challenging task due to
                additional constraints imposed by hardware configuration, such as restricted qubit connectivity
                and hardware-native gate set. Finding optimal gate sequences for a given quantum circuit with all
                imposed constraints is an open problem. For two-qubit circuits, the KAK algorithm is an example
                of the efficient compilation algorithm, that is used in practical applications.} \bigskip

                \noindent{Subroutines for quantum circuit optimisation, mapping and routing for particular
                hardware connectivity are an integral part of existing quantum software frameworks. The complexity 
                and diversity of various quantum compilation algorithms create a necessity of cross-benchmarking and 
                comparison between different compiler frameworks. It worth to note, that the problem of hardware 
                benchmarking often arises in classical computing, where hardware is compared based on their 
                performance on a set of predefined tests.} \\ \\ \bigskip
                
                \noindent{Arline Benchmarks}\footnotemark"""
            )
        )
        doc.append(
            NoEscape(
                r"""\footnotetext{\textbf{Disclaimer.} This report was prepared using open source program Arline
                Benchmarks and is provided "AS IS", "AS AVAILABLE", and all warranties, express or implied,
                are disclaimed.\bigskip

                Neither the authors of the Arline Benchmarks program, nor any of their employees, nor any of
                their contractors, subcontractors or their employees, makes any warranty, express or implied, or
                assumes any legal liability or responsibility for the accuracy, completeness, or any third party's
                use or the results of such use of any information, apparatus, product, or process disclosed,
                or represents that its use would not infringe privately owned rights.\bigskip

                Readers are cautioned that information contained in this report is only current as of the
                respective dates of such information. The technical condition, analyzed program code,
                comparative results may have changed since those dates. Readers are advised to review only the most
                recent document for the most current information. \\ \bigskip}
                """
            )
        )
        doc.append(
            NoEscape(
                r"""platform is created to solve benchmarking problem in the quantum world and aims to provide a fair
                 comparison between compilers for various quantum hardware and quantum algorithms.
                """
            )
        )

        # Description of frameworks involved in benchmarking
        with doc.create(Section("""Frameworks """)):
            doc.append(NoEscape("Below we list compilation frameworks used in the benchmarking run:"))
            if contains_qiskit:
                with doc.create(Itemize()) as itemize:
                    itemize.add_item(
                        NoEscape(
                            r"""\textbf{IBM Qiskit}\footnotemark
                            """
                        )
                    )
                    doc.append(
                        NoEscape(
                            r"""\footnotetext{IBM and Qiskit are trademarks of International
                            Business Machines Corporation, registered in many jurisdictions worldwide.}
                            """
                        )
                    )
                    doc.append(
                        NoEscape(
                            f"""v{version('qiskit')} (open-source).""" + r"""
                            \textbf{Qiskit} is an open-source framework for working with quantum computers at the 
                            level of circuits, pulses, and algorithms. Qiskit transpiler combines mapping and 
                            compression subroutines. The transpiler has three levels of optimization, in our report, 
                            we invoke the most advanced optimization level (3)."""
                        )
                    )
                    with doc.create(Itemize()) as itemize2:
                        itemize2.add_item(
                            NoEscape(
                                r"""Compression/optimization algorithm relies on \textbf{commutative
                                cancellations of gates, aggregation of single-qubit gates, removal
                                of diagonal gates before measurement.}"""
                            )
                        )
                        itemize2.add_item(
                            NoEscape(
                                r"""Routing and mapping algorithms are based on construction of a 
                                \textbf{basic, stochastic or lookahead swap network.}"""
                            )
                        )
                        itemize2.add_item("""The output circuit gate set: """)
                        doc.append(NoEscape(r"$U_1$, $U_2$, $U_3$, $CNOT$, $I$."))

            if contains_cirq:
                with doc.create(Itemize()) as itemize:
                    itemize.add_item(NoEscape(r"""\textbf{Google Cirq library} """))
                    doc.append(
                        NoEscape(
                            f"""v{version('cirq')} (open-source). """ +
                            r"""\textbf{Cirq} has been developed by Google AI Quantum Team.
                            Cirq supports mapping/routing operations and tailored
                            towards specific grid-like qubit coupling topologies."""
                        )
                    )
                    with doc.create(Itemize()) as itemize2:
                        itemize2.add_item(
                            NoEscape(
                                r"""The compression subroutines include \textbf{pushing Pauli gates and phased
                                Pauli gates to the end of the circuit and merging single-qubit gates}."""
                            )
                        )
                        itemize2.add_item(
                            NoEscape(
                                r"""Routing and mapping methods are based on a \textbf{greedy swap insertion 
                                strategy.}"""
                            )
                        )
                        itemize2.add_item(NoEscape(r"""The output circuit gate set: $R_x$, $R_z$, $CZ$."""))

        with doc.create(Section("Definitions")):
            doc.append(
                NoEscape(
                    """We define compression factor ($CF$) for a particular class of gates ($G$)
                    (e.g. single-qubit gates, two-qubit gates) averaged over circuits ($C$) as:"""
                )
            )

            with doc.create(Itemize()) as itemize:
                itemize.add_item(
                    NoEscape(
                        r"""Compression factor greater then unity ($CF(G)>1$) corresponds to
                        a successful compression (the gate count of the gate
                        ($G$) in the output circuit is less compared to the input circuit).
                        Similarly, compression factor less then unity ($CF(G)<1$) corresponds to
                        unsuccessful compression (the output circuit contains more gates of type ($G$)
                        compared to the input circuit)."""
                    )
                )

                with doc.create(Alignat(numbering=False, escape=False)) as agn:
                    agn.append(
                        r"""CF(G) =\left \langle  \frac{\textrm{gate count}
                        (G, C_i)_{\textrm{before}}}{\textrm{gate count}(G, C_i)_{\textrm{after}}} \right\rangle_{C}."""
                    )

                itemize.add_item(
                    NoEscape(
                        r"""Circuits \textbf{before} and \textbf{after} correspond to initial and
                        final compilation stages. In the present report, we usually assume that the initial stage is
                        target generation, the final stage is typically either \textbf{compression} or
                        \textbf{rebase} to the output hardware gate set.
                        """
                    )
                )
            with doc.create(Section("Metrics")):
                with doc.create(Itemize()) as itemize:
                    itemize.add_item(NoEscape(r"""\textbf{Circuit depth}"""))
                    itemize.add_item(NoEscape(r"""\textbf{Single-qubit gate count}"""))
                    itemize.add_item(NoEscape(r"""\textbf{Two-qubit gate count}"""))
                    itemize.add_item(NoEscape(r"""\textbf{Total gate count}"""))
                    itemize.add_item(NoEscape(r"""\textbf{Compression factor per gate type}"""))
                    itemize.add_item(NoEscape(r"""\textbf{Execution time}"""))
                doc.append(
                    NoEscape(
                        r"""Note that in most cases single-qubit gate count and single-qubit gate compression factor
                        have only limited meaning as a metric of compiler performance.
                        This is because single-qubit gate count is very sensitive to the choice single-qubit basis gates
                        (e.g. $U_3$ is equivalent to a combination of 3 rotation gates $R_x$, $R_y$ and $R_z$)."""
                    )
                )

        with doc.create(Section("Hardware Backends ")):
            doc.append(NoEscape(r"""Hardware backends are defined in open-source \textbf{Arline Quantum} library """))
            doc.append(LineBreak())
            doc.append(Hyperref(marker="", text="""https://github.com/ArlineQ/ArlineQuantum"""))
            doc.append(
                NoEscape(
                    r""". Names of hardware backends reflect gate set and connectivity. E.g. IbmALl2All16Q
                    corresponds to a 16-qubit fully-connected mock backend with the native gate set of the IBM
                    quantum hardware. \bigskip
                    """
                )
            )
            with doc.create(Itemize()) as itemize:
                for hardw in hardware_list:
                    itemize.add_item(NoEscape(r"\textbf{" + hardw + r"}"))

        doc.append(Section("Compilation Pipelines"))
        doc.append(
            NoEscape(
                r"""\textbf{Compilation pipeline} is a sequence of compilation routines,
                which typically consist of three main stages:"""
            )
        )
        with doc.create(Enumerate()) as enumerate_tex:
            enumerate_tex.add_item(
                NoEscape(
                    r"""\textbf{Mapping} and \textbf{routing} of the original circuit to
                    a hardware topology, further \textbf{mapping};"""
                )
            )
            enumerate_tex.add_item(
                NoEscape(r"""\textbf{Compression} of the circuit that reduces the number of gates involved;""")
            )

        pipelines = latex_report.get_pipelines_summary()
        doc.append(
            NoEscape(
                r"""Summary of compilation pipelines settings used in the current report is presented below. \\ """
            )
        )
        doc.append(
            NoEscape(
                f"""Number of pipelines for benchmarking: """ + r"\textbf{" + "{:}".format(len(pipelines)) + r"}"
            )
        )
        with doc.create(Itemize()) as itemize:
            for pip_i in range(len(pipelines)):
                pipeline_name = pipelines[pip_i]["pipeline"]
                itemize.add_item(NoEscape(r"Pipeline name: \textbf{" + pipeline_name + r"}"))
                with doc.create(Enumerate()) as enumerate_tex:
                    stages = pipelines[pip_i]["stages"]
                    for stage in stages:
                        enumerate_tex.add_item(NoEscape(r"Stage name: \textbf{" + stage + r"}"))

        with doc.create(Section("System Info")):
            with doc.create(Itemize()) as itemize:
                itemize.add_item(f"Platform: {platform.platform(aliased=1)}")
                itemize.add_item(f"Processor: {cpuinfo.get_cpu_info()['brand']}")
                memory = virtual_memory().total / 2 ** 30
                itemize.add_item(f"Memory: {round(memory, 1)} Gb")

        with doc.create(Section("Targets")):
            target_list_full = latex_report.get_target_list()
            doc.append("Target is a quantum circuit subject to compilation. List of target generators: ")
            for targ in target_list_full:
                with doc.create(Itemize()) as itemize:
                    target_summary = latex_report.get_target_summary(targ)
                    targ_name, targ_name_lower_case = latex_report.target_class_display_name(targ)
                    itemize.add_item(NoEscape(rf"Target generator type: {targ_name_lower_case}:"))
                    if targ_name == "QASM Circuits for Arithmetic Blocks":
                        with doc.create(Itemize()) as itemize3:
                            itemize3.add_item(
                                NoEscape(
                                    r"""This QASM dataset contains circuits relevant for arithmetic
                                    operations in quantum algorithms such as Shor's algorithm and many other. """
                                )
                            )
                            doc.append(NoEscape(r"""More info can be found on D. Maslov's  website """))
                            doc.append(LineBreak())
                            doc.append(Hyperref(marker="", text="""http://webhome.cs.uvic.ca/~dmaslov/"""))
                            doc.append(NoEscape(r"""."""))
                            itemize3.add_item(
                                NoEscape(
                                    r"""Some examples of frequently used arithmetic block circuits:
                                    """
                                )
                            )

                            with doc.create(Itemize()) as itemize4:
                                itemize4.add_item(r"""ALU (arithmetic logical unit) [e.g. mini-alu_167.qasm]""")
                                itemize4.add_item(r"""Symmetric functions [e.g. sym9_146.qasm]""")
                                itemize4.add_item(r"""RD-Input weight functions [e.g. rd53_135.qasm]""")

                    g_names = target_summary["gate_names"]
                    g_frequencies = target_summary["gate_frequencies"]

                    s = "[" + ", ".join([g + ": " + str(round(f, 3)) for g, f in zip(g_names, g_frequencies)]) + "]"
                    with doc.create(Itemize()) as itemize2:
                        itemize2.add_item(f"Number of circuits: {target_summary['num_targets']}")
                        itemize2.add_item(f"Gates frequencies: {s}")
                        itemize2.add_item(f"Mean number of qubits: {target_summary['mean_num_qubits']}")
                        itemize2.add_item(f"Mean circuit depth per circuit: {round(target_summary['mean_depth'], 1)}")
                        itemize2.add_item(
                            f"Mean single-qubit gate count per circuit: "
                            f"{round(target_summary['mean_one_qubit_gate_count'], 1)}"
                        )
                        itemize2.add_item(
                            f"Mean two-qubit gate count per circuit: "
                            f"{round(target_summary['mean_two_qubit_gate_count'], 1)}"
                        )
                        itemize2.add_item(
                            f"Mean total gate count per circuit: {round(target_summary['mean_total_gate_count'], 1)}"
                        )

    # Separate chapter for KAK analysis (two-qubit hardware)
    kak_hardware_list = latex_report.get_kak_hardware_list()
    is_kak_hardware = len(kak_hardware_list) > 0
    # Display KAK results only for one hardware!

    kak_target_list = latex_report.get_kak_target_list()
    if is_kak_hardware:
        for hardware in kak_hardware_list:
            with doc.create(Chapter("Comparison with KAK Decomposition: Two-Qubit Circuits")):
                with doc.create(Section("Cartan decomposition")):
                    doc.append(
                        NoEscape(
                            r"""Cartan decomposition (KAK decomposition) provides theoretical
                            $CNOT$-optimal schemes for two-qubit circuits (Figure \ref{fig:KAK}).\bigskip"""
                        )
                    )
                    with doc.create(Figure(position="h")) as fig:
                        kak_scheme_path = path.join(os.path.dirname(os.path.abspath(arline_benchmarks.__file__)), "reports")
                        filename = "./kak_scheme.png"
                        filename = os.path.join(kak_scheme_path, filename)

                        fig.add_image(filename, width=NoEscape(r"0.7\textwidth"))
                        fig.add_caption(NoEscape(r"""\label{fig:KAK}KAK decomposition scheme for two-qubit circuits."""))
                    doc.append(
                        NoEscape(
                            r"""\noindent{Arbitrary two-qubit unitary can be represented using $U_3$ and $CNOT$ gates
                            with no more than: }"""
                        )
                    )
                    with doc.create(Itemize()) as itemize:
                        itemize.add_item(NoEscape(r"""$CNOT$ count: \textbf{3}"""))
                        itemize.add_item(NoEscape(r"""$U_3$ count: \textbf{8}"""))
                        itemize.add_item(NoEscape(r"""Total depth: \textbf{7}"""))
                        itemize.add_item(NoEscape(r"""Total gate count: \textbf{11}"""))

                for target in kak_target_list:
                    targ_name = "Random Circuits from [Clifford + T] Gate Set"
                    targ_name_lower_case = "random circuits from [Clifford + T] gate set"
                    with doc.create(Section(NoEscape(rf"Target: {targ_name}"))):
                        doc.append(
                            Subsection(
                                r"""Metrics for each stage of compilation pipeline""",
                                numbering=False,
                            )
                        )
                        doc.append(
                            NoEscape(
                                r"""Note that in most cases single-qubit gate count and single-qubit gate compression 
                                factor have only limited meaning as a metric of compiler performance.
                                This is because single-qubit gate count is very sensitive to the choice single-qubit
                                basis gates (e.g. $U_3$ is equivalent to a combination of 3 rotation gates $R_x$, 
                                $R_y$ and $R_z$)."""
                            )
                        )
                        with doc.create(Figure(position="h!")) as fig:
                            filename = rf"output/figures/2qubit/{target}/{hardware}/bars_depth.png"
                            filename = os.path.join(args.input_data_dir, filename)
                            filename = os.path.abspath(filename)
                            fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                            filename = rf"output/figures/2qubit/{target}/{hardware}/bars_single_qubit_gate_count.png"
                            filename = os.path.join(args.input_data_dir, filename)
                            filename = os.path.abspath(filename)
                            fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                            doc.append(LineBreak())
                            filename = rf"output/figures/2qubit/{target}/{hardware}/bars_two_qubit_gate_count.png"
                            filename = os.path.join(args.input_data_dir, filename)
                            filename = os.path.abspath(filename)
                            fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                            filename = rf"output/figures/2qubit/{target}/{hardware}/bars_total_gate_count.png"
                            filename = os.path.join(args.input_data_dir, filename)
                            filename = os.path.abspath(filename)
                            fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                            fig.add_caption(
                                NoEscape(
                                    rf"""Compilation of two-qubit {targ_name_lower_case} and comparison with
                                    KAK algorithm for {hardware}. The red line corresponds to KAK baseline."""
                                )
                            )

                    doc.append(Subsection(r"""Summary stats by pipeline (after final compilation stage)""",
                                          numbering=False))
                    # Here we show aggregated information on single-qubit, two-qubit gate count and total
                    # gate count before/after compression.
                    with doc.create(Itemize()) as itemize:
                        stage_id = final_stage
                        # TODO: Check that the output gate set = U1, U2, U3, CNOT (IbmAll2all)
                        df_mean = latex_report.groupby_framework_mean(stage_id, hardware, target)
                        itemize.add_item(
                            NoEscape(
                                r"Min depth: \textbf{"
                                + f"{df_mean['Depth'].idxmin()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Max depth: \textbf{"
                                + f"{df_mean['Depth'].idxmax()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Min single-qubit gate count: \textbf{"
                                + f"{df_mean['Single-Qubit Gate Count'].idxmin()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Max single-qubit gate count: \textbf{"
                                + f"{df_mean['Single-Qubit Gate Count'].idxmax()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Min two-qubit gate count: \textbf{"
                                + f"{df_mean['Two-Qubit Gate Count'].idxmin()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Max two-qubit gate count: \textbf{"
                                + f"{df_mean['Two-Qubit Gate Count'].idxmax()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Min total gate count: \textbf{"
                                + f"{df_mean['Total Gate Count'].idxmin()}" + r"}"
                            )
                        )
                        itemize.add_item(
                            NoEscape(
                                r"Max total gate count: \textbf{"
                                + f"{df_mean['Total Gate Count'].idxmax()}" + r"}"
                            )
                        )
    hardware_list = latex_report.get_not_kak_hardware_list()
    target_list = latex_report.get_not_kak_target_list()

    with doc.create(Chapter(rf"Quick Summary")):
        with doc.create(Section("Aggregate Multi-Factor Comparison")):
            doc.append(
                NoEscape(
                    r"""This analysis is performed across all compilation frameworks, target circuits and hardware.
                    From this result, we can deduce the degree of compressibility of different target 
                    circuits. \bigskip
                    
                    \noindent{Note that in most cases single-qubit gate count and single-qubit gate compression 
                    factor have only limited meaning as a metric of compiler performance. This is because 
                    single-qubit gate count is very sensitive to the choice single-qubit basis gates
                    (e.g. $U_3$ is equivalent to a combination of 3 rotation gates $R_x$, $R_y$ and $R_z$).}
                    """
                )
            )

            with doc.create(Figure(position="h!")) as fig:
                filename = f"output/figures/multiqubit/comp_radar_target_analysis_to_{final_stage}_grid.png"
                filename = os.path.join(args.input_data_dir, filename)
                filename = os.path.abspath(filename)
                fig.add_image(filename, width=NoEscape(r"1.\textwidth"))
                fig.add_caption(
                    NoEscape(
                        rf"""Aggregate multi-factor comparison of compilation frameworks: compression factor ($CF$)
                        across various features. The x-axis corresponds to target, y-axis - hardware.
                        Better performance corresponds to a larger polygon area."""
                    )
                )

    for target in target_list:
        targ_name, targ_name_lower_case = latex_report.target_class_display_name(target)

        with doc.create(Chapter(NoEscape(rf"Target: {targ_name}"))):
            for i_hardw, hardware in enumerate(hardware_list):
                if i_hardw > 0:
                    doc.append(NoEscape(r"\clearpage"))

                plot_group = "2qubit" if "2Q" in hardware else "multiqubit"

                frmwrk_min_exec_time = latex_report.get_min_exec_time_framework(hardware, target)
                frmwrk_max_exec_time = latex_report.get_max_exec_time_framework(hardware, target)
                doc.append(Section(r"Hardware: {:}".format(hardware)))
                doc.append(
                    Subsection(
                        NoEscape(
                            r"""Metrics for each stage of compilation pipeline and aggregate compression factor 
                            (initial/final)"""
                        ),
                        numbering=False,
                    )
                )
                doc.append(
                    NoEscape(
                        r"""Note that in most cases single-qubit gate count and single-qubit gate compression factor
                        have only limited meaning as a metric of compiler performance.
                        This is because single-qubit gate count is very sensitive to the choice single-qubit basis gates
                        (e.g. $U_3$ is equivalent to a combination of 3 rotation gates $R_x$, $R_y$ and $R_z$)."""
                    )
                )
                with doc.create(Figure(position="h!")) as fig:
                    filename = rf"output/figures/{plot_group}/{target}/{hardware}/bars_depth.png"
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    filename = rf"output/figures/{plot_group}/{target}/{hardware}/bars_single_qubit_gate_count.png"
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    doc.append(LineBreak())
                    filename = rf"output/figures/{plot_group}/{target}/{hardware}/bars_two_qubit_gate_count.png"
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    filename = rf"output/figures/{plot_group}/{target}/{hardware}/bars_total_gate_count.png"
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    fig.add_caption(
                        NoEscape(
                            rf"""Circuits metrics for each compilation pipeline stage for {hardware}.
                            """
                        )
                    )
                with doc.create(Figure(position="h!")) as fig:

                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"compression_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.45\textwidth"))

                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"comp_radar_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))

                    fig.add_caption(
                        NoEscape(
                            rf"""Compression factor ($CF$) between target and final compilation stage for {hardware}
                            (histogram and radar plot).
                            """
                        )
                    )

                doc.append(NoEscape(r"\clearpage"))

                doc.append(Subsection(r"""Gate composition for each compilation pipeline stage""", numbering=False))
                with doc.create(Figure(position="h!")) as fig:
                    frameworks = latex_report.get_frameworks_list()

                    for i, framework in enumerate(frameworks):
                        filename = (
                            f"output/figures/{plot_group}/{target}/{hardware}/"
                            f"gate_composition_heatmap_{framework.lower()}.png"
                        )
                        filename = os.path.join(args.input_data_dir, filename)
                        filename = os.path.abspath(filename)
                        fig.add_image(filename, width=NoEscape(r"0.35\textwidth"))

                        # Arrange figures so that there are 2 figures in a row (insert LineBreak)
                        if i % 2 == 0 and i > 0:
                            doc.append(LineBreak())

                    fig.add_caption(NoEscape(rf"""Gate frequencies in each pipeline stage for {hardware}."""))

                doc.append(Subsection(r"""Execution time stats """, numbering=False))
                doc.append(
                    NoEscape(
                        """Here we present stats about execution time (in seconds)
                        spent by frameworks for each compilation stage."""
                    )
                )
                with doc.create(Figure(position="h!")) as fig:
                    filename = rf"output/figures/{plot_group}/{target}/{hardware}/bars_execution_time.png"
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.45\textwidth"))
                    fig.add_caption(NoEscape(f"""Mean execution time of each compilation stage for {hardware}."""))
                doc.append(Subsection(r"Summary stats (averaged over target circuits) by"
                                      r" pipeline", numbering=False))
                # Here we show aggregated information on single-qubit, two-qubit gate count
                # and total gate count before/after compression.
                with doc.create(Itemize()) as itemize:
                    stage_id = final_stage
                    df_mean = latex_report.groupby_framework_mean(stage_id, hardware, target)
                    itemize.add_item(
                        NoEscape(
                            r"Min depth: \textbf{"
                            + f"{df_mean['Depth'].idxmin()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Max depth: \textbf{"
                            + f"{df_mean['Depth'].idxmax()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Min single-qubit gate count: \textbf{"
                            + f"{df_mean['Single-Qubit Gate Count'].idxmin()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Max single-qubit gate count: \textbf{"
                            + f"{df_mean['Single-Qubit Gate Count'].idxmax()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Min two-qubit gate count: \textbf{"
                            + f"{df_mean['Two-Qubit Gate Count'].idxmin()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Max two-qubit gate count: \textbf{"
                            + f"{df_mean['Two-Qubit Gate Count'].idxmax()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Min total gate count: \textbf{"
                            + f"{df_mean['Total Gate Count'].idxmin()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Max total gate count: \textbf{"
                            + f"{df_mean['Total Gate Count'].idxmax()}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Min execution time: \textbf{"
                            + f"{frmwrk_min_exec_time}" + r"}"
                        )
                    )
                    itemize.add_item(
                        NoEscape(
                            r"Max execution time: \textbf{"
                            + f"{frmwrk_max_exec_time}" + r"}"
                        )
                    )
                doc.append(Subsection(r"""Cluster analytics """, numbering=False))
                doc.append(
                    NoEscape(
                        r"""Scatter plots with axes representing: depth, (input/output) single-qubit gate count,
                        (input/output) two-qubit gate count, (input/output) total gate count and execution time."""
                    )
                )
                with doc.create(Figure(position="h!")) as fig:
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"scatter_two_qubit_gate_count_vs_single_qubit_gate_count.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"scatter_two_qubit_gate_count_vs_depth.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    doc.append(LineBreak())
                    filename = (
                        rf"output/figures/{plot_group}/{target}/"
                        rf"{hardware}/scatter_total_gate_count_vs_depth.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"scatter_execution_time_vs_input_depth.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.4\textwidth"))
                    fig.add_caption(
                        NoEscape(
                            rf"""Cluster analytics for {hardware}.
                            Each point corresponds to an individual target
                            quantum circuit from the target generator."""
                        )
                    )
                doc.append(Subsection(r"""Breakdown by individual circuits """, numbering=False))
                with doc.create(Figure(position="h!")) as fig:
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"comp_heatmap_depth_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.35\textwidth"))
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"comp_heatmap_single_qubit_gate_count_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.35\textwidth"))
                    doc.append(LineBreak())
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"comp_heatmap_two_qubit_gate_count_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.35\textwidth"))
                    filename = (
                        rf"output/figures/{plot_group}/{target}/{hardware}/"
                        rf"comp_heatmap_total_gate_count_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.35\textwidth"))
                    fig.add_caption(
                        NoEscape(
                            rf"""Compression factor ($CF$) vs circuit for {hardware}."""
                        )
                    )
                with doc.create(Itemize()) as itemize:
                    stage_id = final_stage

                    val, circ, pip = latex_report.min_result_by_feature(
                        "Depth", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with min final depth: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.max_result_by_feature(
                        "Depth", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with max final depth: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.min_result_by_feature(
                        "Single-Qubit Gate Count", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with min final single-qubit gate count: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.max_result_by_feature(
                        "Single-Qubit Gate Count", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with max final single-qubit gate count: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.min_result_by_feature(
                        "Two-Qubit Gate Count", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with min final two-qubit gate count: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.max_result_by_feature(
                        "Two-Qubit Gate Count", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with max final two-qubit gate count: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.min_result_by_feature(
                        "Total Gate Count", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with min final total gate count: \textbf{" + f"{val}" + r"}"))

                    val, circ, pip = latex_report.max_result_by_feature(
                        "Total Gate Count", stage_id, hardware, target
                    )
                    itemize.add_item(f"Circuit id: {circ} ")
                    doc.append(NoEscape(r"with max final total gate count: \textbf{" + f"{val}" + r"}"))

            doc.append(NoEscape(r"\clearpage"))
            with doc.create(Section("Hardware Comparison")):
                doc.append("This analysis is performed across all classes of target circuit.")
                with doc.create(Itemize()) as itemize:
                    heat_df = latex_report.get_df_mean_by_hardware("Depth", hardware_list)
                    idx = heat_df.idxmin()
                    itemize.add_item(NoEscape(r"Min average depth: " + r"\textbf{" + idx[0] + r"}"))

                    heat_df = latex_report.get_df_mean_by_hardware("Depth", hardware_list)
                    idx = heat_df.idxmax()
                    itemize.add_item(NoEscape(r"Max average depth: " + r"\textbf{" + idx[0] + r"}"))

                    heat_df = latex_report.get_df_mean_by_hardware("Single-Qubit Gate Count", hardware_list)
                    idx = heat_df.idxmin()
                    itemize.add_item(NoEscape(r"Min average single-qubit gate count: " + r"\textbf{" + idx[0] + r"}"))

                    heat_df = latex_report.get_df_mean_by_hardware("Single-Qubit Gate Count", hardware_list)
                    idx = heat_df.idxmax()
                    itemize.add_item(NoEscape(r"Max average single-qubit gate count: " + r"\textbf{" + idx[0] + r"}"))

                    heat_df = latex_report.get_df_mean_by_hardware("Two-Qubit Gate Count", hardware_list)
                    idx = heat_df.idxmin()
                    itemize.add_item(NoEscape(r"Min average two-qubit gate count: " + r"\textbf{" + idx[0] + r"}"))

                    heat_df = latex_report.get_df_mean_by_hardware("Two-Qubit Gate Count", hardware_list)
                    idx = heat_df.idxmax()
                    itemize.add_item(NoEscape(r"Max average two-qubit gate count: " + r"\textbf{" + idx[0] + r"}"))

                with doc.create(Figure(position="h!")) as fig:
                    filename = (
                        f"output/figures/multiqubit/{target}/comp_heatmap_depth_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.5\textwidth"))
                    filename = (
                        f"output/figures/multiqubit/{target}/"
                        f"comp_heatmap_single_qubit_gate_count_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.5\textwidth"))
                    doc.append(LineBreak())
                    filename = (
                        f"output/figures/multiqubit/{target}/"
                        f"comp_heatmap_two_qubit_gate_count_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.5\textwidth"))
                    filename = (
                        f"output/figures/multiqubit/{target}/"
                        f"comp_heatmap_total_gate_count_target_analysis_to_{final_stage}.png"
                    )
                    filename = os.path.join(args.input_data_dir, filename)
                    filename = os.path.abspath(filename)
                    fig.add_image(filename, width=NoEscape(r"0.5\textwidth"))
                    fig.add_caption(
                        NoEscape(
                            r"""Final circuit metrics after compilation vs hardware backend."""
                        )
                    )
    doc.append(NewPage())
    doc.append(Chapter("References", numbering=False))
    with doc.create(Itemize()) as itemize:
        if is_kak_hardware:
            itemize.add_item(
                NoEscape(
                    r""" G. Vidal and C.M. Dawson, “A universal quantum circuit for two-qubit 
                    transformations with three CNOT gates”, Phys. Rev. A 69, 010301 (2004),  """
                )
            )
            doc.append(Hyperref(marker="", text="""https://arxiv.org/pdf/quant-ph/0307177.pdf"""))
        if contains_qiskit:
            itemize.add_item(r"""IBM Qiskit repository """)
            doc.append(Hyperref(marker="", text="""https://github.com/Qiskit"""))
        if contains_cirq:
            itemize.add_item(r"""Google Quantum AI Lab repository """)
            doc.append(Hyperref(marker="", text="""https://github.com/quantumlib/Cirq"""))

    doc.generate_tex()  # Generate .tex file from the text provided above
    doc.generate_pdf(clean_tex=False)  # Converts .tex file to .pdf
    tex = doc.dumps()  # The document as string in LaTeX syntax
    print(f"PDF report has been successfully generated from LaTeX in {filepath}")
