<jupyter_start><jupyter_text>---- Getting started with Intel Quantum Simulator----Tutorial on the basic use of Intel QS through its Python interface.**NOTE:**Currently, the Python implementation only allows for single-core execution and does not take advantages of the MPI protocol.However the user can familiarize with the same functionalities available in the distributed implementation (only C++ at the moment) and the transition should be relatively straighforward since all methods maintain name and effect.  Import Intel QS libraryLet's start by importing the Python library with the class and methods defined in the C++ implementation.<jupyter_code># Import the Python library with the C++ class and methods of Intel Quantum Simulator.
# If the library is not contained in the same folder of this notebook, its path has to be added.
import sys
sys.path.insert(0, '../build/lib')
import intelqs_py as simulator

# import numPy
import numpy as np

# Import graphical library for plots.
import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>Initialize the state of the quantum registerIQS  stores a full representation of the quantum state in the computational basis.In practice, the quantum state of $N$ qubits is represented as a complex vector with $2^N$ components.Each component corresponds to the probability amplitude of a specific computational basis state:$$\psi(k) = \langle k | \psi \rangle$$with the index $k$ corresponding to the $N$-bit integer in decimal representation, and $k\in\{0,1,2,\dots,2^N-1\}$.----- First of all, one needs to allocate the memory to contain the state representation.- Then the quantum register has to be initialized, either to a specific computational basis state (using the keyword "base") or to a random state (using the keyword "rand").----NOTE: the random number generator is able to generate three different kinds of random numbers:- *local* --> different for each pool rank- *state* --> common to all ranks of the same state- *pool*  --> common to all ranks of the pool<jupyter_code># Number of qubits.
num_qubits = 2;
# Index of the computational basis state corresponding to |00>.
index = 0;

# Allocate memory for the quantum register's state and initialize it to |00>.
psi = simulator.QubitRegister(num_qubits, "base", index, 0);

# To initialize the state to a random vector, one first need a random number generator.
# Create the random number generator, set its seed and then associate it to the IQS object 'psi'.
rng = simulator.RandomNumberGenerator();
rng_seed = 7777;
rng.SetSeedStreamPtrs( rng_seed );
psi.SetRngPtr(rng);

# Initialize the state to a random state, this can be achieved with the codeword "rand" followed by 0
# if we desire to use *local* random numbers (this speed up the process  of generating the random numbers).
psi.Initialize("rand", 0);<jupyter_output><empty_output><jupyter_text>Display the quantum stateIt is important to be able to access and visualize the quantum state.IQS allows to access the single components of the state or to print a comprehensive description.What index is associated to state $|1011\rangle$?In decimal representation one has:$$1011 \rightarrow 1\times2^0 + 0\times2^1 + 1\times2^2 + 1\times2^3 = 1+4+8 = 13$$**NOTE:** contrary to what is adopted in decimal notation, our binary representation must be read from left to right (from least significant to most significant bit).<jupyter_code># Initialize the state to |10>.
# The index of |10> in decimal representation is 1.
index = 1;
psi.Initialize("base", index);

# There are for amplitudes, corresponding to |00>, |10>, |01>, |11>.
for index in range(0,2**num_qubits):
    amplitude = psi[index]
    print("psi({}) = <{}|psi> = {}".format(index,index,amplitude))

# A complete description of the state is provided by the method Print().
print("----")
label = "Computational basis state |10>"
psi.Print(label)<jupyter_output>psi(0) = <0|psi> = 0j
psi(1) = <1|psi> = (1+0j)
psi(2) = <2|psi> = 0j
psi(3) = <3|psi> = 0j
----
<<the output has been redirected to the terminal>><jupyter_text>One-qubit gatesIn the gate-model of quantum computation, one manipulates the quantum state by means of unitary transformations acting on one or two qubits. Let us apply a few of the standard one-qubit gates.<jupyter_code># State was |10>. Let us re-prepare it:
psi = simulator.QubitRegister(2, "base", 1, 0);
# Flip the qubit 1 by applying the Pauli X gate: |10> ==> |11>
qubit = 1;
psi.ApplyPauliX(qubit);

# Display all amplitudes.
print("Currently, |psi>=|11>:");
for index in range(0,2**num_qubits):
    print("  psi({}) = <{}|psi> = {}".format(index,index,psi[index]))

print("----")
    
# Apply the Hadamard gate on qubit 0: |11> ==> |-1> ~ |01>-|11>
qubit = 0;
psi.ApplyHadamard(qubit);

# Display all amplitudes.
print("Currently, |psi>=|-1>:");
for index in range(0,2**num_qubits):
    print("  psi({}) = <{}|psi> = {}".format(index,index,psi[index]))

# Apply Pauli Z gate on qubit 1: |-1> ==> -|-1>
psi.ApplyPauliZ(1);
# Apply Pauli X gate on qubit 0: -|-1> ==> |-1>
psi.ApplyPauliX(0);<jupyter_output>Currently, |psi>=|11>:
  psi(0) = <0|psi> = 0j
  psi(1) = <1|psi> = 0j
  psi(2) = <2|psi> = 0j
  psi(3) = <3|psi> = (1+0j)
----
Currently, |psi>=|-1>:
  psi(0) = <0|psi> = 0j
  psi(1) = <1|psi> = 0j
  psi(2) = <2|psi> = (0.7071067811865475+0j)
  psi(3) = <3|psi> = (-0.7071067811865475+0j)<jupyter_text>Two-qubit gatesTo achieve universal quantum computation, it is enought to implement one-qubit gates and a single type of two-qubit gate.The essential requirement is that such two-qubit gate is able to generate entanglement. Usually the controlled-not gate (CNOT in the following) is the operation of choice.IQS provides built-in methods to implement a much broader variety of two-qubit gates.<jupyter_code># Currently, state is |-1>.
# Apply a CNOT(1,0): flip qubit 0 conditioned on the state of qubit 1.
# |-1> ==> -|-1>
control = 1;
target = 0;
psi.ApplyCPauliX(control, target);

# Display all amplitudes.
print("Currently, |psi>=-|-1>:");
for index in range(0,2**num_qubits):
    print("  psi({}) = <{}|psi> = {}".format(index,index,psi[index]))

print("----")

# The application of the previous CNOT did not create any entanglement.
# This is achieved by exchanging the role of control and target qubits.
# Apply a CNOT(0,1): flip qubit 1 conditioned on the state of qubit 0.
# -|-1> ~ -|01>+|11> ==> -|01>+|10>
control = 0;
target = 1;
psi.ApplyCPauliX(control, target);

# Display all amplitudes.
print("Currently, |psi>=(|10>-|01>)/sqrt(2):");
for index in range(0,2**num_qubits):
    print("  psi({}) = <{}|psi> = {}".format(index,index,psi[index]))<jupyter_output>Currently, |psi>=-|-1>:
  psi(0) = <0|psi> = 0j
  psi(1) = <1|psi> = 0j
  psi(2) = <2|psi> = (-0.7071067811865475+0j)
  psi(3) = <3|psi> = (0.7071067811865475+0j)
----
Currently, |psi>=(|10>-|01>)/sqrt(2):
  psi(0) = <0|psi> = 0j
  psi(1) = <1|psi> = (0.7071067811865475+0j)
  psi(2) = <2|psi> = (-0.7071067811865475+0j)
  psi(3) = <3|psi> = 0j<jupyter_text>Custom gatesIf IQS does not provide the gates needed in your circuit, it is possible to implement custom one-qubit gates and controlled gates.<jupyter_code># Define an arbitrary single qubit gate.
# The quantum gate G is given by a 2x2 unitary matrix, here using a bi-dimensional NumPy array.
G = np.zeros((2,2),dtype=np.complex_);
G[0,0] =  0.592056606032915 + 0.459533060553574j;
G[0,1] = -0.314948020757856 - 0.582328159830658j;
G[1,0] =  0.658235557641767 + 0.070882241549507j;
G[1,1] =  0.649564427121402 + 0.373855203932477j;

# To verify that G is unitary, we will compute the norm of psi before and after the application of G.
initial_norm = psi.ComputeNorm();
if initial_norm != 1:
    print("Even before the application of G, state psi had normalization {}".format(initial_norm));
# Apply the custom gate G to qubit 0.
qubit = 0;
psi.Apply1QubitGate(qubit,G);
final_norm = psi.ComputeNorm();
if initial_norm != final_norm:
    print("The application of G changed the norm of state psi: from {} to {}".format(initial_norm,final_norm));
else:
    print("Sanity check: norm was unchanged by G.");
# It is also possible to apply the arbitrary gate specified by G conditioned on the state of another qubit.
# G is applied only when the control qubit is in |1>.
control = 1;
target = 0;
psi.ApplyControlled1QubitGate( control, target, G);

# Notice that this output is directed to the terminal and not re-directed to the iPython notebook.
psi.Print("State of the quantum register after all gates.")
print()

# To display the amplitudes in the iPython notebook:
for index in range(0,2**num_qubits):
    print("psi({}) = <{}|psi> = {}".format(index,index,psi[index]))<jupyter_output><<the output has been redirected to the terminal>>

psi(0) = <0|psi> = (-0.22270188119916148-0.41176819069214193j)
psi(1) = <1|psi> = (0.4593114112350983+0.2643555498825341j)
psi(2) = <2|psi> = (0.018860567095173378-0.09793842271642633j)
psi(3) = <3|psi> = (-0.5361330884127283-0.45012626658938304j)<jupyter_text>Single-qubit measurementsTo extract information from the quantum register, one can obtain the probability of measuring a certain qubit in the computational basis and obtaining the outcome "1" (i.e. the state is in $|1\rangle$).Once the probability is known, one can draw a random number to simulate the stochastic outcome of the measurement and collapse the wavefunction accordingly.**NOTE:**Computing the probability of a certain outcome does not collapse automatically the wavefunction. This is helpful when the probabilities of multiple measurements have to be computed without re-executing the quantum simulation.<jupyter_code># Compute the probability of qubit 1 being in state |1>.
measured_qubit = 1;
prob = psi.GetProbability( measured_qubit );

print("Probability that qubit {} is in state |1> is {}\n".format(measured_qubit, prob));

# Draw random number in [0,1)
r = np.random.rand()
if r < prob:
    # Collapse the wavefunction according to qubit 1 being in |1>.
    print("Simulated outcome is 1. Collapse the function accordingly.")
    psi.CollapseQubit(measured_qubit,True);
else:
    # Collapse the wavefunction according to qubit 1 being in |0>
    print("Simulated outcome is 0. Collapse the function accordingly.")
    psi.CollapseQubit(measured_qubit,False);

# In both cases one needs to re-normalize the wavefunction:
psi.Normalize();<jupyter_output>Probability that qubit 1 is in state |1> is 0.4999999999999997

Simulated outcome is 0. Collapse the function accordingly.<jupyter_text>Expectation value of products of Pauli matricesTo extract information from the quantum register, one can obtain the expectation value of Pauli strings.For example, consider the Pauli string given by: $$X_0 \otimes id_1 \otimes Z_2 \otimes Z_3$$Such observable is defined by:- the position of the non-trivial Pauli matrices, in this case {0,2,3}- the corresponding Pauli matrices ($X$=1, $Y$=2, $Z$=3).To facilitate the verification of the expectation value, we reinitialize the quantum state to $|+-01\rangle$.We also consider the Pauli string $$X_0 \otimes id_1 \otimes Z_2 \otimes Y_3$$.<jupyter_code># Prepare the state |+-01>
num_qubits = 4;
index = 0;
psi = simulator.QubitRegister(num_qubits, "base", index, 0);
psi.ApplyPauliX(1);
psi.ApplyPauliX(3);
psi.ApplyHadamard(0);
psi.ApplyHadamard(1);
print("psi is in state |+-01>\n");

# The Pauli string given by:  X_0 . id_1 . Z_2 . Z_3
# Such observable is defined by the position of the non-trivial Pauli matrices:
qubits_to_be_measured = [0,2,3]

# And by the corresponding Pauli matrices (X=1, Y=2, Z=3)
observables = [1,3,3]

# The expectation value <psi|X_0.id_1.Z_2.Z_3|psi> is obtained via:
average = psi.ExpectationValue(qubits_to_be_measured, observables, 1.);
print("Expectation value <psi|X_0.id_1.Z_2.Z_3|psi> = {}  <== it should be -1\n".format(average));

# The expectation value <psi|X_0.id_1.Z_2.Y_3|psi> is obtained via:
observables = [1,3,2]
average = psi.ExpectationValue(qubits_to_be_measured, observables, 1.);
print("Expectation value <psi|X_0.id_1.Z_2.Y_3|psi> = {}  <== it should be  0\n".format(average));<jupyter_output>psi is in state |+-01>

Expectation value <psi|X_0.id_1.Z_2.Z_3|psi> = -0.9999999999999996  <== it should be -1

Expectation value <psi|X_0.id_1.Z_2.Y_3|psi> = 0.0  <== it should be  0<jupyter_text>Examples of state preparationLet us prepare the state $|+-01\rangle$.<jupyter_code># Method A:
# Prepare the state |0000>, flip qubits {1,3}, change basis to qubits {0,1}.
num_qubits = 4;
index = 0;
psi = simulator.QubitRegister(num_qubits, "base", index, 0);
psi.ApplyPauliX(1);
psi.ApplyPauliX(3);
psi.ApplyHadamard(0);
psi.ApplyHadamard(1);

# Method B:
# Prepare the state |0000>, change basis to qubits {0,1}, flip qubit {3}, flip in X qubit {1}.
index = 0;
psi.Initialize("base", index);
psi.ApplyHadamard(0);
psi.ApplyHadamard(1);
psi.ApplyPauliZ(1);
psi.ApplyPauliX(3);

# Method C:
# Prepare the computational state |0101>, change basis to qubits {0,1}.
index = 2+8 ;
psi.Initialize("base", index);
# Notice that GetProbability() does not change the state.
print("Verify that the state is now |0101>.\n")
for qubit in range(0,num_qubits):
    prob = psi.GetProbability( qubit );
    print("Probability that qubit {}, if measured, is in state |1> = {}".format(qubit, prob));
psi.ApplyHadamard(0);
psi.ApplyHadamard(1);
print("\nNow the state is |+-01>.\n")

# The expectation value <psi|X_0.X_1.Z_2.Z_3|psi> is obtained via:
qubits_to_be_measured = [0,1,2,3]
observables = [1,1,3,3]
average = psi.ExpectationValue(qubits_to_be_measured, observables, 1.);
print("Expectation value <psi|X_0.X_1.Z_2.Z_3|psi> = {}  <== it should be +1\n".format(average));<jupyter_output>Verify that the state is now |0101>.

Probability that qubit 0, if measured, is in state |1> = 0.0
Probability that qubit 1, if measured, is in state |1> = 1.0
Probability that qubit 2, if measured, is in state |1> = 0.0
Probability that qubit 3, if measured, is in state |1> = 1.0

Now the state is |+-01>.

Expectation value <psi|X_0.X_1.Z_2.Z_3|psi> = 0.9999999999999993  <== it should be +1
StarcoderdataJupyterStructuredCleanDedup