Source code for qrisp.environments.quantum_inversion

"""
\********************************************************************************
* Copyright (c) 2023 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""


from qrisp.circuit import QubitAlloc, QubitDealloc
from qrisp.environments.quantum_environments import QuantumEnvironment


# Environment inheritor where the environment content is appended as the inverse
[docs] class InversionEnvironment(QuantumEnvironment): """ This QuantumEnvironment can be used to invert (i.e. "dagger") a block of operations. An alias for this is ``invert``. Examples -------- We increment a :ref:`QuantumFloat` and afterwards revert using the InversionEnvironment: :: from qrisp import QuantumFloat, invert qf = QuantumFloat(4) qf += 3 with invert(): qf += 3 >>> print(qf) {0: 1.0} >>> print(qf.qs) :: QuantumCircuit: -------------- ┌───────────┐┌──────────────┐ qf.0: ┤0 ├┤0 ├ │ ││ │ qf.1: ┤1 ├┤1 ├ │ __iadd__ ││ __iadd___dg │ qf.2: ┤2 ├┤2 ├ │ ││ │ qf.3: ┤3 ├┤3 ├ └───────────┘└──────────────┘ Live QuantumVariables: --------------------- QuantumFloat qf In the next example, we create a :ref:`QuantumFloat` and bring it into uniform superposition. We calculate the square and set a :ref:`QuantumBool` to ``True``, based on if the result is less than 10. Finally, we use the InversionEnvironment to uncompute the result of the multiplication. :: from qrisp import QuantumBool, h, q_mult, multi_measurement qf = QuantumFloat(3) h(qf) mult_res = q_mult(qf, qf) q_bool = QuantumBool() with mult_res < 10: q_bool.flip() with invert(): q_mult(qf, qf, target = mult_res) mult_res.delete(verify = True) >>> print(multi_measurement([qf, q_bool])) {(0, True): 0.125, (1, True): 0.125, (2, True): 0.125, (3, True): 0.125, (4, False): 0.125, (5, False): 0.125, (6, False): 0.125, (7, False): 0.125} .. note:: In many cases, this way of manually uncomputing only works if the uncomputed function (in this case ``q_mult``) allows specifying the target variable. Using the :meth:`redirect_qfunction <qrisp.redirect_qfunction>` decorator, you can turn any quantum function into it's target specifiable version. """ def __enter__(self): self.manual_allocation_management = True QuantumEnvironment.__enter__(self) def compile(self): # Save original circuit original_circuit = self.env_qs.copy() self.env_qs.clear_data() # Compile environment for instruction in self.env_data: # If the instruction is an environment, compile the environment if isinstance(instruction, QuantumEnvironment): instruction.compile() continue self.env_qs.append(instruction) # We are now faced with the challenge of handling the (De)Allocation gates. # Allocation gates are inverted to Deallocation and vice versa. # Implying no treatment at all can lead to the situation that a QuantumVariable # created inside this environment is unintentionally deallocated after # compilation and has no prior allocation. # from qrisp import invert, QuantumBool, QuantumFloat, cx # qf = QuantumFloat(3) # with invert(): # qf_res = qf*qf # print(qf.qs) # QuantumCircuit: # --------------- # ┌──────────┐┌──────────────┐ # qf.0: ┤ qb_alloc ├┤0 ├────────────── # ├──────────┤│ │ # qf.1: ┤ qb_alloc ├┤1 ├────────────── # ├──────────┤│ │ # qf.2: ┤ qb_alloc ├┤2 ├────────────── # └──────────┘│ │┌────────────┐ # mul_res_1.0: ────────────┤3 ├┤ qb_dealloc ├ # │ │├────────────┤ # mul_res_1.1: ────────────┤4 ├┤ qb_dealloc ├ # │ │├────────────┤ # mul_res_1.2: ────────────┤5 ├┤ qb_dealloc ├ # │ __mul___dg │├────────────┤ # mul_res_1.3: ────────────┤6 ├┤ qb_dealloc ├ # │ │├────────────┤ # mul_res_1.4: ────────────┤7 ├┤ qb_dealloc ├ # │ │├────────────┤ # mul_res_1.5: ────────────┤8 ├┤ qb_dealloc ├ # ┌──────────┐│ │├────────────┤ # sbp_anc_3.0: ┤ qb_alloc ├┤9 ├┤ qb_dealloc ├ # ├──────────┤│ │├────────────┤ # sbp_anc_4.0: ┤ qb_alloc ├┤10 ├┤ qb_dealloc ├ # ├──────────┤│ │├────────────┤ # sbp_anc_5.0: ┤ qb_alloc ├┤11 ├┤ qb_dealloc ├ # └──────────┘└──────────────┘└────────────┘ # Live QuantumVariables: # ---------------------- # QuantumFloat mul_res_1 # QuantumFloat qf # A naive solution would be to collect all (De)allocation gates and execute them # after and before the inverted instructions are appended. # This however results in the problem that automatic recomputation ancilla # management is no longer available within this environment because a function # using ancillae being uncomputed, will recompute it's ancillae on the same # qubits because there are no longer any (De)Allocation gates in between. # from qrisp import invert, QuantumBool, QuantumFloat, cx # qf = QuantumFloat(3) # with invert(): # qf_res = qf*qf # qb = QuantumBool() # cx(qf_res[0], qb) # qf_res.uncompute() # print(qf.qs) # QuantumCircuit: # --------------- # ┌──────────┐┌─────────────────┐ ┌──────────────┐ # qf.0: ┤ qb_alloc ├┤0 ├─────┤0 ├────────────── # ├──────────┤│ │ │ │ # qf.1: ┤ qb_alloc ├┤1 ├─────┤1 ├────────────── # ├──────────┤│ │ │ │ # qf.2: ┤ qb_alloc ├┤2 ├─────┤2 ├────────────── # ├──────────┤│ │ │ │┌────────────┐ # mul_res_1.0: ┤ qb_alloc ├┤3 ├──■──┤3 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # mul_res_1.1: ┤ qb_alloc ├┤4 ├──┼──┤4 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # mul_res_1.2: ┤ qb_alloc ├┤5 ├──┼──┤5 ├┤ qb_dealloc ├ # ├──────────┤│ __mul___dg_dg │ │ │ __mul___dg │├────────────┤ # mul_res_1.3: ┤ qb_alloc ├┤6 ├──┼──┤6 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # mul_res_1.4: ┤ qb_alloc ├┤7 ├──┼──┤7 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # mul_res_1.5: ┤ qb_alloc ├┤8 ├──┼──┤8 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # sbp_anc_3.0: ┤ qb_alloc ├┤9 ├──┼──┤9 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # sbp_anc_4.0: ┤ qb_alloc ├┤10 ├──┼──┤10 ├┤ qb_dealloc ├ # ├──────────┤│ │ │ │ │├────────────┤ # sbp_anc_5.0: ┤ qb_alloc ├┤11 ├──┼──┤11 ├┤ qb_dealloc ├ # ├──────────┤└─────────────────┘┌─┴─┐└──────────────┘└────────────┘ # qb.0: ┤ qb_alloc ├───────────────────┤ X ├────────────────────────────── # └──────────┘ └───┘ # Live QuantumVariables: # ---------------------- # QuantumBool qb # QuantumFloat qf # Our solution is therefore to collect all the INITAL and FINAL allocation gates # and execute them separately while keeping the inner (De)Allocation gates # where they are. # from qrisp import invert, QuantumBool, QuantumFloat, cx # qf = QuantumFloat(3) # with invert(): # qf_res = qf*qf # qb = QuantumBool() # cx(qf_res[0], qb) # qf_res.uncompute() # print(qf.qs) # QuantumCircuit: # --------------- # ┌──────────┐┌─────────────────┐ » # qf.0: ┤ qb_alloc ├┤0 ├───────────────────────────────» # ├──────────┤│ │ » # qf.1: ┤ qb_alloc ├┤1 ├───────────────────────────────» # ├──────────┤│ │ » # qf.2: ┤ qb_alloc ├┤2 ├───────────────────────────────» # ├──────────┤│ │ » # mul_res_2.0: ┤ qb_alloc ├┤3 ├────────────────■──────────────» # ├──────────┤│ │ │ » # mul_res_2.1: ┤ qb_alloc ├┤4 ├────────────────┼──────────────» # ├──────────┤│ │ │ » # mul_res_2.2: ┤ qb_alloc ├┤5 ├────────────────┼──────────────» # ├──────────┤│ __mul___dg_dg │ │ » # mul_res_2.3: ┤ qb_alloc ├┤6 ├────────────────┼──────────────» # ├──────────┤│ │ │ » # mul_res_2.4: ┤ qb_alloc ├┤7 ├────────────────┼──────────────» # ├──────────┤│ │ │ » # mul_res_2.5: ┤ qb_alloc ├┤8 ├────────────────┼──────────────» # ├──────────┤│ │┌────────────┐ │ ┌──────────┐» # sbp_anc_6.0: ┤ qb_alloc ├┤9 ├┤ qb_dealloc ├──┼──┤ qb_alloc ├» # ├──────────┤│ │├────────────┤ │ ├──────────┤» # sbp_anc_7.0: ┤ qb_alloc ├┤10 ├┤ qb_dealloc ├──┼──┤ qb_alloc ├» # ├──────────┤│ │├────────────┤ │ ├──────────┤» # sbp_anc_8.0: ┤ qb_alloc ├┤11 ├┤ qb_dealloc ├──┼──┤ qb_alloc ├» # ├──────────┤└─────────────────┘└────────────┘┌─┴─┐└──────────┘» # qb.0: ┤ qb_alloc ├─────────────────────────────────┤ X ├────────────» # └──────────┘ └───┘ » # « ┌──────────────┐ # « qf.0: ┤0 ├────────────── # « │ │ # « qf.1: ┤1 ├────────────── # « │ │ # « qf.2: ┤2 ├────────────── # « │ │┌────────────┐ # «mul_res_2.0: ┤3 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «mul_res_2.1: ┤4 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «mul_res_2.2: ┤5 ├┤ qb_dealloc ├ # « │ __mul___dg │├────────────┤ # «mul_res_2.3: ┤6 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «mul_res_2.4: ┤7 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «mul_res_2.5: ┤8 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «sbp_anc_6.0: ┤9 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «sbp_anc_7.0: ┤10 ├┤ qb_dealloc ├ # « │ │├────────────┤ # «sbp_anc_8.0: ┤11 ├┤ qb_dealloc ├ # « └──────────────┘└────────────┘ # « qb.0: ────────────────────────────── # « # Live QuantumVariables: # ---------------------- # QuantumBool qb # QuantumFloat qf initially_allocated_qubits = [] i = 0 while i < len(self.env_qs.data): instr = self.env_qs.data[i] if ( instr.op.name == "qb_alloc" and not instr.qubits[0] in initially_allocated_qubits ): initially_allocated_qubits.append(self.env_qs.data.pop(i).qubits[0]) continue i += 1 deallocated_qubits = [] i = 0 self.env_qs.data.reverse() while i < len(self.env_qs.data): instr = self.env_qs.data[i] if ( instr.op.name == "qb_dealloc" and not instr.qubits[0] in deallocated_qubits ): deallocated_qubits.append(self.env_qs.data.pop(i).qubits[0]) continue i += 1 deallocated_qubits = list(set(deallocated_qubits)) self.env_qs.data.reverse() # print(transpile(self.env_qs.inverse())) # print(transpile(self.env_qs)) # Merge the original circuit with the inverse of the environment content original_circuit.qubits = self.env_qs.qubits original_circuit.clbits = self.env_qs.clbits for qubit in initially_allocated_qubits: original_circuit.append(QubitAlloc(), [qubit]) original_circuit.extend(self.env_qs.inverse()) for qubit in deallocated_qubits: original_circuit.append(QubitDealloc(), [qubit]) # Reinstate the resulting circuit in the quantum session circuit self.env_qs.data = original_circuit.data
# Shortcut to quickly initiate inversion environments def invert(): return InversionEnvironment()