Getting Started (RREF) ====================== This page details how to get started with photonic_circuit_solver. This page covers the RREF function and the photonic circuit solving algorithm. This is entire codebase relies on the Stabilizer class, so if you haven't already read that page first. Installation ------------ To install photonic_circuit_solver and use the rref functions and supplemental functions, you will need photonic_circuit_solver installed in an environment with the following packages: * Python 3.11 or superior * NumPy * Qiskit (optional) * Matplotlib (optional) * SymPy (optional) As in Stabilizer, the core computation is all done using NumPy as native python data types, all optional packages are for visual and style utility. Qiskit is needed for any functions that output qiskit circuits, Matplotlib is needed for any plotting functions, and SymPy is needed for functions in the supplemental functions. RREF Gauge Choice ------------------- The RREF Gauge ``````````````` In the Stabilizer section, we noted that the choice of generators for representing a stabilizer state is not unique. However, given a choice of generators, there is a way to bring it into a form that is similar to the upper triangular form of the RREF linear operation on matrices. This protocol is called the RREF algorithm, and further details can be found at https://doi.org/10.1088/1367-2630/7/1/170. Note that even fixing a set of generators, the RREF gauge is still not unique. However, it does have some useful properties for our purposes. First, fix some order of your generators. This is done inherently by the ``Stabilizer`` class since the tableau is a matrix with ordered rows. To bring a set of stabilizers into RREF gauge, we note that we have N columns and K rows. For the purposes of this package, since the Stabilizer class enforces pure states, K = N, although this is not necessarily true for broader uses of the algorithm. We also have an active region, denoted by the column NL and the row KU. We start by setting NL = 1 and inspect the first column, and look at all the Paulis in the first column, i.e. inspecting the first entry in every generator. There should be one of two cases, there are only identities and one specific Pauli, or identities and at least two Paulis. If we're in case 1, reorder the generators such that a generator with a non-identity Pauli are in row 1, and set KU = 1. Then, for all rows k such that k>KU, if the first entry of that generator is a non-identity Pauli, multiply that generator by the first generator and replace it. In the code, this is done via the ``row_add`` method of the Stabilizer class. If we're in case 2, reorder the generators such that a generator with the non-identity Paulis are in rows 1 and 2, and ensure that those Paulis differ. Set KU = 2, and for all k>KU, inspect the corresponding generator. If the first Pauli is an identity, do nothing. If it matches the Pauli in row 1, multiply them together and replace the kth generator with that, and similar with row 2. If it is the third pauli, multiply by both row1 and row2 and replace the kth generator with the new generator. Then set NL=1, and follow the same steps (only inspecting Paulis for rows greater than the previous KU). If the column has only identities (which wasn't possible for column 1 but is possible afterwards), don't alter KU and just set NL+=1 Let's see the package implement this. .. code-block:: python from photonic_circuit_solver import * state = Stabilizer(edgelist = [[0,1],[1,3],[3,2],[2,0],[1,2]]) state.stabilizers() returns :: ['XZZI', 'ZXZZ', 'ZZXZ', 'IZZX'] Now lets implement the RREF procedure. If you do the RREF procedure by hand, you should notice all we need to do is multiply row 2 into row 3, and then the generators will be in RREF gauge. And as expected, following the above code with .. code-block:: python rref(state) state.stabilizers() returns :: ['XZZI', 'ZXZZ', 'IYYI', 'IZZX'] As expected. Equivalently, RREF is a method implemented in the Stabilizer class. To replicate the above, one can also do. .. code-block:: python from photonic_circuit_solver import * state = Stabilizer(edgelist = [[0,1],[1,3],[3,2],[2,0],[1,2]]) state.rref() state.stabilizers() For the sake of all future work, we are going to emit the hexagonal ring graph from the previous section. .. image:: Plot5.jpg :width: 400 :alt: Hexagonal Ring graph As a reminder, this state can be generated by .. code-block:: state = Stabilizer(edgelist = [[i,(i+1)%6] for i in range(6)]) Entanglement Entropy and Required Emitters ``````````````````````````````````````````` The reason RREF is so useful is it lets us compute entanglement entropy by having an efficient way to compute the partial trace if we remove a selection of qubits. However, since the actual partial trace isn't as important to us as the entropy, this package doesn't come with the algorithm PTRACE. Details for that algorithm can be found in the above url for RREF. We proxy the entanglement entropy by the so-called height function in (equation 1 in doi.org/10.1038/s41534-022-00522-6). .. math:: h(x) = n - x - |\{g_i:l(g_i)>x\}| :math:`l(g_i)` is the leftmost index of a generator that contains a Pauli that isn't the identity. Henceforth, we will simply call these nontrivial Paulis. This function effectively tells us how many generators have a nontrivial pauli further left than anticipated. This is a sign of entanglement, and thus requires an increase in the height function. For the sake of clarity, :math:`h(x)` proxies the entanglement entropy of tracing out the qubits starting from 0 up to and including :math:`x-1`. From this, we see that :math:`h(0)` and :math:`h(n)` trace out no qubits and thus has no entanglement entropy. It turns out that, upon fixing an ordering of photons to be emitted, the minimal number of emitters required is just the maximal of this height function. We can plot this using the ``plot_height()`` function .. code-block:: python from photonic_circuit_solver import * state = Stabilizer(edgelist = [[i,(i+1)%6] for i in range(6)]) plot_height(state) .. image:: Plot6.png :width: 400 :alt: Height function associated with the hexagonal ring with normal ordering, [0,1,2,2,2,1,0] From this, we note that we need at least 2 emitters to generate this state from solely emitters. If we just want that number, we can call the ``num_emitters`` function. .. code-block:: python from photonic_circuit_solver import * state = Stabilizer(edgelist = [[i,(i+1)%6] for i in range(6)]) print(num_emitters(state)) The above code returns :: 2 Circuit Solving Algorithm -------------------------- Overview ````````` This section outlines the circuit solving algorithm found here: doi.org/10.1038/s41534-022-00522-6 This section is a surface-level overview of the algorithm. I will make statements without proofs, since those proofs are found in the link above. We want to generate this circuit from solely emitters. We're first going to establish some rules that our scheme follows. * No photon-photon interactions allowed * Photons only directly interact with emitters during the act of emission * Single qubit (Clifford) gates are allowed on photons after emission and on emitters at any time * Entangling (Clifford) gates are only allowed between emitters For clarity, emitters are indexed by :math:`i_k` and the photon we're preparing to absorb is indexed by :math:`j` We're going to generate the scheme backwards, starting from the graph state and zeroed out emitters and working backwards to the computational zero state. We do this because we model unabsorbed photons as being in the :math:`|0\rangle` state. We start by emitting photon :math:`j = n_p-1` and work backwards till :math:`j = 0` We mimic photon absorptions computationally via a CNOT gate. If :math:`h(j+1)\geq h(j)`, one can verify that due to the RREF gauge there exists a generator whose leading nontrivial Pauli is at site :math:`j`. Meanwhile, by design of the algorithm (which we will get to a bit later), all the other photon sites after :math:`j` until the emitters are also trivial. One thing to note, the original paper uses the quantity :math:`h(j)- h(j-1)` to determine this. This discrepancy is due to differences in photon indexing. The original paper indexes photons starting from 1, but to stay consistent with default python indexing this package index photons starting from 0. However, if the above condition is satsified, we're guaranteed the existance of generator of the form :math:`g = \sigma_j^\alpha\sigma_{i_1}^{\beta_1}\ldots \sigma_{i_e}^{\beta_e}` where :math:`\alpha\in \{X,Y,Z\}` and :math:`\beta\in \{I,X,Y,Z\}`. This is important, because the existance of such a stabilizer implies that this photon is entangled solely with the emitters, and not with the other photons. This generator can, through single qubit gates on the photon and through gates on the emitters, be transformed to :math:`g = \sigma^Z_j\sigma^Z_i`, where :math:`i` is the chosen emitter. Then, noting that :math:`\text{CNOT}_{i,j}\sigma^Z_j\sigma^Z_i = \sigma^Z_j\text{CNOT}_{i,j}`, upon absorbing the photon, we have disentangled the photon. We then take that generator to clear the column so that for all other generators, :math:`\sigma_j = I`. This is why we could assume that on that generator, the paulis at all the sites after :math:`j` until the emitters are trivial, because those photons have already been absorbed and disentangled. However, what if :math:`h(j+1)