Quantum Environments#

class QuantumEnvironment[source]#

QuantumEnvironments are blocks of code, that undergo some user-specified compilation process. They can be entered using the with statement

from qrisp import QuantumEnvironment, QuantumVariable, x

qv = QuantumVariable(5)

with QuantumEnvironment():
   x(qv)

In this case we have no special compilation technique, since the abstract baseclass simply returns it’s content:

>>> print(qv.qs)
QuantumCircuit:
--------------
      ┌───┐
qv.0: ┤ X ├
      ├───┤
qv.1: ┤ X ├
      ├───┤
qv.2: ┤ X ├
      ├───┤
qv.3: ┤ X ├
      ├───┤
qv.4: ┤ X ├
      └───┘
Live QuantumVariables:
---------------------
QuantumVariable qv

More advanced environments allow for a large variety of features and can significantly simplify code development and maintainance.

The most important built-in QuantumEnvironments are:

Due to sophisticated condition evaluation of nested ConditionEnvironment and ControlEnvironment, using QuantumEnvironments even can bring an increase in performance, compared to the control method which is commonly implemented by QuantumCircuit-based approaches.

Uncomputation within QuantumEnvironments

Uncomputation via the uncompute method is possible only if the QuantumVariable has been created within the same or a sub-environment:

from qrisp import QuantumVariable, QuantumEnvironment, cx

a = QuantumVariable(1)

with QuantumEnvironment():

    b = QuantumVariable(1)

    cx(a,b)

    with QuantumEnvironment():

        c = QuantumVariable(1)

        cx(b,c)

    c.uncompute() # works because c was created in a sub environment
    b.uncompute() # works because b was created in the same environment
    # a.uncompute() # doesn't work because a was created outside this
    environment.
>>> print(a.qs)
QuantumCircuit:
--------------
a.0: ──■──────────────■──
     ┌─┴─┐          ┌─┴─┐
b.0: ┤ X ├──■────■──┤ X ├
     └───┘┌─┴─┐┌─┴─┐└───┘
c.0: ─────┤ X ├┤ X ├─────
          └───┘└───┘
Live QuantumVariables:
---------------------
QuantumVariable a

Visualisation within QuantumEnvironments

Calling print on a QuantumSession inside a QuantumEnvironment will display only the instructions, that have been performed within this environment.

from qrisp import x, y, z
a = QuantumVariable(3)

x(a[0])

with QuantumEnvironment():

    y(a[1])

    with QuantumEnvironment():

        z(a[2])

        print(a.qs)

    print(a.qs)

print(a.qs)

Executing this snippet yields

QuantumCircuit:
--------------
a.0: ─────

a.1: ─────
     ┌───┐
a.2: ┤ Z ├
     └───┘
QuantumEnvironment Stack:
------------------------
Level 0: QuantumEnvironment
Level 1: QuantumEnvironment

Live QuantumVariables:
---------------------
QuantumVariable a
QuantumCircuit:
--------------
a.0: ─────
     ┌───┐
a.1: ┤ Y ├
     ├───┤
a.2: ┤ Z ├
     └───┘
QuantumEnvironment Stack:
------------------------
Level 0: QuantumEnvironment

Live QuantumVariables:
---------------------
QuantumVariable a
QuantumCircuit:
--------------
     ┌───┐
a.0: ┤ X ├
     ├───┤
a.1: ┤ Y ├
     ├───┤
a.2: ┤ Z ├
     └───┘
Live QuantumVariables:
---------------------
QuantumVariable a

Warning

Calling print within a QuantumEnvironment causes all sub environments to be compiled. While this doesn’t change the semantics of the resulting circuit, especially nested Condition- and ControlEnvironments lose a lot of efficiency if compiled prematurely. Therefore, print-calls within QuantumEnvironments are usefull for debugging purposes but should be removed, if efficiency is a concern.

Creating custom QuantumEnvironments

More interesting QuantumEnvironments can be created by inheriting and modifying the compile method. In the following code snippet, we will demonstrate how to set up a QuantumEnvironment, that skips every second instruction. We do this by inheriting from the QuantumEnvironment class. This will provide us with the necessary attributes for writing the compile method:

#. .env_data, which is the list of instructions, that have been appended in this environment. Note that child environments append themselves in this list upon exiting.

#. .env_qs which is a QuantumSession, where all QuantumVariables, that operated inside this environment, are registered.

The compile method is then called once all environments of .env_qs have been exited. Note that this doesn’t neccessarily imply that all QuantumEnvironments have been left. For more information about the interplay between QuantumSessions and QuantumEnvironments check the session merging documentation.

class ExampleEnvironment(QuantumEnvironment):

   def compile(self):

      for i in range(len(self.env_data)):

         #This line makes sure every second instruction is skipped
         if i%2:
            continue

         instruction = self.env_data[i]

         #If the instruction is an environment, we compile this environment
         if isinstance(instruction, QuantumEnvironment):
            instruction.compile()
         #Otherwise we append
         else:
             self.env_qs.append(instruction)

Check the result:

from qrisp import x, y, z, t, s, h
qv = QuantumVariable(6)

with ExampleEnvironment():
    x(qv[0])
    y(qv[1])
    with ExampleEnvironment():
        z(qv[2])
        t(qv[3])
    with ExampleEnvironment():
        s(qv[4])
    h(qv[5])
>>> print(qv.qs)
QuantumCircuit:
--------------
      ┌───┐
qv.0: ┤ X ├
      └───┘
qv.1: ─────
      ┌───┐
qv.2: ┤ Z ├
      └───┘
qv.3: ─────

qv.4: ─────
      ┌───┐
qv.5: ┤ H ├
      └───┘
Live QuantumVariables:
---------------------
QuantumVariable qv