Source code for qrisp.environments.iteration_environment
# -*- coding: utf-8 -*-
"""
Created on Mon Jul 3 17:37:05 2023
@author: sea
"""
from qrisp.environments import QuantumEnvironment, GateWrapEnvironment
from qrisp.core.quantum_variable import QuantumVariable
from qrisp.core.compilation import qompiler
from qrisp.misc.utility import retarget_instructions
from qrisp.circuit import QubitAlloc, transpile
[docs]
class IterationEnvironment(QuantumEnvironment):
"""
This QuantumEnvironment can be used for reducing bottlenecks in compilation time.
Many algorithms such as Grover or QPE require repeated execution of the same
quantum circuit. When scaling up complex algorithms that perform a lot of
non-trivial logic many iterations can significantly slow down the compilation
speed. The ``IterationEnvironment`` remedies this flaw by recording the circuit
of a single iteration and then duplicating the instructions the required
amount of times.
Another bottleneck that can appear is the :meth:`compile <qrisp.QuantumSession.compile>`
method as the qubit allocation algorithm can also scale bad for really large
algorithms. For this problem the ``IterationEnvironment`` exposes the ``precompile``
keyword. Setting this keyword to ``True`` will perform the Qubit allocation algorithm
on the QuantumEnvironments content and then (if necessary) allocate another
:ref:`QuantumVariable` to accomodate the workspace qubits of the compilation
result. This way there is only a single (de)allocation per
``IterationEnvironment``.
.. note::
Code that is executed within a ``IterationEnvironment`` may not
:meth:`delete <qrisp.QuantumVariable.delete>`
previously created ``QuantumVariable`` and every created ``QuantumVariable``
inside this :ref:`QuantumEnvironment` has to be deleted before exit.
Parameters
----------
qs : QuantumSession
The ``QuantumSession`` in which the iterated code should be performed.
QuantumVariables that have been created outside this ``QuantumEnvironment``
need to be :ref:`registered <SessionMerging>` in this ``QuantumSession``.
iteration_amount : integer
The amount of iterations to perform.
precompile : bool
If set to ``True``, the qubit allocation algorithm will be run, once this
``QuantumEnvironment`` is compiled. This can significantly reduce the
workload for a later compile call as there is only a single allocation
per ``IterationEnvironment`` to handle. The default is False.
Examples
--------
We perform a simple addition circuit multiple times:
::
from qrisp import QuantumFloat, IterationEnvironment
a = QuantumFloat(5)
with IterationEnvironment(a.qs, iteration_amount = 10):
a += 1
Evaluate the result
>>> print(a)
{10: 1.0}
**Precompilation**
Squaring a :ref:`QuantumFloat` uses the :meth:`qrisp.sbp_mult` function,
which has a high demand of ancilla qubits.
Therefore many iterations can quickly overload the allocation algorithm.
::
def benchmark_function(use_iter_env = False):
qf = QuantumFloat(5)
if use_iter_env:
with IterationEnvironment(qf.qs, 20, precompile = True):
temp = qf*qf
temp.uncompute()
else:
for i in range(20):
temp = qf*qf
temp.uncompute()
compiled_qc = qf.qs.compile()
Benchmark results:
::
import time
start_time = time.time()
benchmark_function(False)
print("Time taken without IterationEnvironment: ", time.time() - start_time)
#Takes 55s
start_time = time.time()
benchmark_function(True)
print("Time taken with IterationEnvironment: ", time.time() - start_time)
#Takes 6s
"""
def __init__(self, qs, iteration_amount, precompile=False):
if iteration_amount < 1:
raise Exception(
"Tried to create IterationEnvironment with < 1 iterations")
self.iteration_amount = iteration_amount
self.precompile = precompile
QuantumEnvironment.__init__(self)
self.env_qs = qs
# Manual allocation management = False means that the compile function
# pulls out all allocation gates to the front and all deallocation gates
# to the back. This way the user doesn't have to worry about the (sometimes)
# complicated logic of arranging them.
# In this case we enable manual allocation management because the compilation
# function is simple enough that we can ignore the allocation logic.
self.manual_allocation_management = True
def __enter__(self):
self.inital_qvs = set(self.env_qs.qv_list)
QuantumEnvironment.__enter__(self)
def __exit__(self, exception_type, exception_value, traceback):
if set(self.env_qs.qv_list) != self.inital_qvs and self.iteration_amount > 1:
if exception_value is None:
raise Exception(
"Tried to invoke IterationEnvironment with code creating/deleting QuantumVariables")
QuantumEnvironment.__exit__(
self, exception_type, exception_value, traceback)
def compile(self):
# Stow away the environment data to facicility environment compilation
temp_qs_data = list(self.env_qs.data)
self.env_qs.data = []
# If the code executed in the Iteration environment contains many
# (de)allocations the allocation algorithm in the compile method can
# be a bottleneck.
# The precompilation feature calls the compile method on the quantum session
# and appends the compiled data instead. This way there is only a single
# (de)allocation for an arbitrary amount of iterations.
# This comes at the cost that the allocation algorithm might find better
# ways if it has insight into the internal allocation structure.
if self.precompile:
# Compile the quantum environment to retrieve the compiled data
QuantumEnvironment.compile(self)
compiled_data = list(self.env_qs.data)
self.env_qs.data = []
# The idea is now to create a new quantum session, convert the collected
# data to this quantum session and compile this quantum session.
# This gives us a quantum circuit whose data we again convert to the
# original QuantumSession.
anc_qv = QuantumVariable(len(self.env_qs.qubits))
translation_dic = {self.env_qs.qubits[i] : anc_qv[i] for i in range(len(anc_qv))}
anc_qv.qs.data = []
# We append the previously executed allocation calls such that
# the compile method knows which variables are allocated
for qb in self.env_qs.qubits:
if qb.allocated:
anc_qv.qs.append(QubitAlloc(), [translation_dic[qb]])
anc_qv.qs.barrier()
# Convert the compiled data to the new quantum session
retarget_instructions(compiled_data, self.env_qs.qubits, anc_qv.reg)
# Append the data to the new QuantumSession
anc_qv.qs.data.extend(compiled_data)
compiled_qc = qompiler(anc_qv.qs, cancel_qfts = False, use_dirty_anc_for_mcx_recomp = False)
# Remove previously added allocation calls from the compiled quantum circuit
compiled_data = []
for instr in compiled_qc.data:
if "alloc" not in instr.op.name or instr.op.name == "barrier":
compiled_data.append(instr)
# Retarget the resulting instructions from the compilation
retarget_instructions(compiled_data, anc_qv.reg, self.env_qs.qubits)
# Reinstate the original data
self.env_qs.data = temp_qs_data
# We now need to locate ancilla qubits of the compilation result and
# create a new ancilla variable to hold these qubits
# Determine the workspace qubits from the compiled qc
workspace_qubits = list(
set(compiled_qc.qubits) - set(anc_qv.reg))
if len(workspace_qubits):
# Allocate a QuantumVariable that will hold the workspace
workspace_var = QuantumVariable(
len(workspace_qubits), qs=self.env_qs, name="workspace_var*")
else:
workspace_var = []
# We now prepare the qubit lists for the retarget_instructions function
source_qubits = workspace_qubits
target_qubits = list(workspace_var)
# Perform instruction retargeting
retarget_instructions(compiled_data, source_qubits, target_qubits)
# Perform iterated instruction execution
for i in range(self.iteration_amount):
compiled_data = [instr.copy() for instr in compiled_data]
self.env_qs.data.extend(compiled_data)
# Delete workspace variable
if isinstance(workspace_var, QuantumVariable):
workspace_var.delete()
# The non-precompiled case is much simpler
else:
QuantumEnvironment.compile(self)
compiled_data = list(self.env_qs.data)
self.env_qs.data = temp_qs_data
for i in range(self.iteration_amount):
compiled_data = [instr.copy() for instr in compiled_data]
self.env_qs.data.extend(compiled_data)