[docs]classIterationEnvironment(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):ifiteration_amount<1:raiseException("Tried to create IterationEnvironment with < 1 iterations")self.iteration_amount=iteration_amountself.precompile=precompileQuantumEnvironment.__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=Truedef__enter__(self):self.inital_qvs=set(self.env_qs.qv_list)QuantumEnvironment.__enter__(self)def__exit__(self,exception_type,exception_value,traceback):ifset(self.env_qs.qv_list)!=self.inital_qvsandself.iteration_amount>1:ifexception_valueisNone:raiseException("Tried to invoke IterationEnvironment with code creating/deleting QuantumVariables")QuantumEnvironment.__exit__(self,exception_type,exception_value,traceback)defcompile(self):# Stow away the environment data to facicility environment compilationtemp_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.ifself.precompile:# Compile the quantum environment to retrieve the compiled dataQuantumEnvironment.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]foriinrange(len(anc_qv))}anc_qv.qs.data=[]# We append the previously executed allocation calls such that# the compile method knows which variables are allocatedforqbinself.env_qs.qubits:ifqb.allocated:anc_qv.qs.append(QubitAlloc(),[translation_dic[qb]])anc_qv.qs.barrier()# Convert the compiled data to the new quantum sessionretarget_instructions(compiled_data,self.env_qs.qubits,anc_qv.reg)# Append the data to the new QuantumSessionanc_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 circuitcompiled_data=[]forinstrincompiled_qc.data:if"alloc"notininstr.op.nameorinstr.op.name=="barrier":compiled_data.append(instr)# Retarget the resulting instructions from the compilationretarget_instructions(compiled_data,anc_qv.reg,self.env_qs.qubits)# Reinstate the original dataself.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 qcworkspace_qubits=list(set(compiled_qc.qubits)-set(anc_qv.reg))iflen(workspace_qubits):# Allocate a QuantumVariable that will hold the workspaceworkspace_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 functionsource_qubits=workspace_qubitstarget_qubits=list(workspace_var)# Perform instruction retargetingretarget_instructions(compiled_data,source_qubits,target_qubits)# Perform iterated instruction executionforiinrange(self.iteration_amount):compiled_data=[instr.copy()forinstrincompiled_data]self.env_qs.data.extend(compiled_data)# Delete workspace variableifisinstance(workspace_var,QuantumVariable):workspace_var.delete()# The non-precompiled case is much simplerelse:QuantumEnvironment.compile(self)compiled_data=list(self.env_qs.data)self.env_qs.data=temp_qs_dataforiinrange(self.iteration_amount):compiled_data=[instr.copy()forinstrincompiled_data]self.env_qs.data.extend(compiled_data)
Get in touch!
If you are interested in Qrisp or high-level quantum algorithm research in general connect with us on our
Slack workspace.