Introduction to Qiskit
In this notebook we will explore how we can program quantum gates and quantum circuits with Qiskit and even how we can execute them on simulators and real quantum computers using Qiskit patterns. Later we will introduce different ways of encoding information and we will finish with a bonus example of Quantum Teleportation.
Before you begin
Follow the Install and set up instructions if you haven't already, including the steps to Set up to use IBM Quantum™ Platform.
It is recommended that you use the Jupyter development environment to interact with quantum computers. Be sure to install the recommended extra visualization support ('qiskit[visualization]'). You'll also need the matplotlib package for the second part of this example.
To learn about quantum computing in general, visit the Basics of quantum information course in IBM Quantum Learning
Imports
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Import necessary modules for this notebook
import time
import qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_state_qsphere
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
print(qiskit.__version__)
2.3.1
To execute your quantum circuits on hardware you need to first set up your account. You can do it as follows:
- Go to the upgraded IBM Quantum® Platform.
- Go to the top right corner (as shown in the above picture), create your API token, and copy it to a secure location.
- In the next cell, replace
deleteThisAndPasteYourAPIKeyHerewith your API key. - Go to the bottom left corner (as shown in the above picture) and create your instance. Make sure to choose the open plan.
- After the instance is created, copy its associated CRN code. You may need to refresh to see the instance.
- In the cell below, replace
deleteThisAndPasteYourCRNHerewith your CRN code.
See this guide for more details on how to set up your IBM Cloud® account.
⚠️ Note: Treat your API key as you would a secure password. See the Cloud setup guide for more information about using your API key in both secure and untrusted environments.
#your_api_key = "deleteThisAndPasteYourAPIKeyHere"
#your_crn = "deleteThisAndPasteYourCRNHere"
QiskitRuntimeService.save_account(
channel="ibm_quantum_platform",
token=your_api_key,
instance=your_crn,
overwrite=True
)
1. Quantum Gates and Quantum Circuits
Quantum circuits are models for quantum computation in which a computation is a sequence of quantum gates. Let's take a look at some of the popular quantum gates.
X Gate
An X gate equates to a rotation around the X-axis of the Bloch sphere by radians. It maps to and to . It is the quantum equivalent of the NOT gate for classical computers and is sometimes called a bit-flip.
# Let's apply an X-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.draw(output='mpl')

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

H Gate
A Hadamard gate represents a rotation of about the axis that is in the middle of the -axis and -axis.
It maps the basis state to , which means that a measurement will have equal probabilities of being 1 or 0, creating a 'superposition' of states. This state is also written as .
# Let's apply an H-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.h(0)
qc.draw(output='mpl')
# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)
CX Gate (CNOT Gate)
The controlled NOT (or CNOT or CX) gate acts on two qubits. It performs the NOT operation (equivalent to applying an X gate) on the second qubit only when the first qubit is and otherwise leaves it unchanged. Note: Qiskit numbers the bits in a string from right to left.
# Let's apply a CX-gate on |11>
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
qc.cx(0,1)
qc.draw(output='mpl')
sv=Statevector(qc)
plot_state_qsphere(sv)

Create the first Bell state
# Create a Bell state circuit
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)
# Draw the circuit
qc.draw("mpl")

# Plot the state using q-sphere visualization
sv = Statevector(qc)
plot_state_qsphere(sv)
# q-sphere is useful for visualizing states when Bloch sphere fails to

Create the second Bell state
# Create a circuit with the second Bell state
qc = QuantumCircuit(2)
qc.x(0)
qc.h(0)
qc.cx(0,1)
qc.draw("mpl")

The explanation is that:
# Get the statevector of the circuit
sv = Statevector(qc)
# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Create the 3-qubit GHZ state
# Create a circuit with 3-qubit GHZ state
qc= QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.draw("mpl")
# Get the statevector of the circuit
sv = Statevector(qc)
# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Create Qiskit logo state
# Create a circuit with the Qiskit logo state
qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.cx(0,3)
qc.x(1)
# Draw the circuit
qc.draw("mpl")
# Get the statevector of the circuit
sv = Statevector(qc)
# Plot the state using qsphere visualization
plot_state_qsphere(sv)

2. Create and run a simple quantum program
The four steps to writing a quantum program using Qiskit patterns are:
-
Map the problem to a quantum-native format.
-
Optimize the circuits and operators.
-
Execute using a quantum primitive function.
-
Analyze the results.
2.1 Map the problem to a quantum-native format
In a quantum program, quantum circuits are the native format in which to represent quantum instructions, and operators represent the observables to be measured. When creating a circuit, you'll usually create a new QuantumCircuit object, then add instructions to it in sequence.
The following code cell creates a circuit that produces the GHZ state which is a state wherein three qubits are fully entangled with each other.
The Qiskit SDK uses the LSb 0 bit numbering where the digit has value or . For more details, see the Bit-ordering in the Qiskit SDK topic.
# Create a GHZ state circuit
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
# Draw the circuit
qc.draw("mpl")
See QuantumCircuit in the documentation for all available operations.
When creating quantum circuits, you must also consider what type of data you want returned after execution. Qiskit provides two ways to return data: you can obtain a probability distribution for a set of qubits you choose to measure, or you can obtain the expectation value of an observable. Prepare your workload to measure your circuit in one of these two ways with Qiskit primitives (explained in detail in Step 3).
This example measures expectation values by using the qiskit.quantum_info submodule, which is specified by using operators (mathematical objects used to represent an action or process that changes a quantum state). The following code cell creates six three-qubit Pauli operators: ZZZ, ZZX, ZII, XXI, ZZI and III.
# Set up six different observables.
observables_labels = ["ZZZ", "ZZX", "ZII", "XXI", "ZZI", "III"]
observables = [SparsePauliOp(label) for label in observables_labels]
print(observables)
[SparsePauliOp(['ZZZ'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZX'],
coeffs=[1.+0.j]), SparsePauliOp(['ZII'],
coeffs=[1.+0.j]), SparsePauliOp(['XXI'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['III'],
coeffs=[1.+0.j])]
Here, something like the ZZI operator is a shorthand for the tensor product , which means measuring Z on qubit 2 and Z on qubit 1 together, and obtaining information about the correlation between qubit 2 and qubit 1. Expectation values like this are also typically written as .
If the state we observe is the three-qubit GHZ state, then the measurement of should be 1.
2.2 Optimize the circuits and operators
When executing circuits on a device, it is important to optimize the set of instructions that the circuit contains and minimize the overall depth (roughly the number of instructions) of the circuit. This ensures that you obtain the best results possible by reducing the effects of error and noise. Additionally, the circuit's instructions must conform to a backend device's Instruction Set Architecture (ISA) and must consider the device's basis gates and qubit connectivity.
The following code instantiates a real device to submit a job to and transforms the circuit and observables to match that backend's ISA. If you have not previously saved your credentials, follow instructions here to authenticate with your API token.
# Choose a real backend
service = QiskitRuntimeService(channel='ibm_quantum_platform',)
backend = service.least_busy(min_num_qubits=156)
# print backend details
print(
f"Name: {backend.name}\n"
f"Version: {backend.backend_version}\n"
f"No. of qubits: {backend.num_qubits}\n"
f"Processor type: {backend.processor_type}\n"
)
Name: ibm_marrakesh
Version: 1.0.21
No. of qubits: 156
Processor type: {'family': 'Heron', 'revision': '2'}
# option to use the AerSimulator instead of a real quantum device
seed_sim=42
backend=AerSimulator.from_backend(backend,seed_simulator=seed_sim)
Transpile the circuit into ISA circuit
# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc)
isa_circuit.draw("mpl", idle_wires=False)

mapped_observables = [
observable.apply_layout(isa_circuit.layout) for observable in observables
]
print(mapped_observables)
[SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIXIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])]
2.3 Execute using the quantum primitives
Quantum computers can produce random results, so you usually collect a sample of the outputs by running the circuit many times. You can estimate the value of the observable by using the Estimator class. Estimator is one of two primitives; the other is Sampler, which can be used to get data from a quantum computer. These objects possess a run() method that executes the selection of circuits, observables, and parameters (if applicable), using a primitive unified bloc (PUB).
When running this code on real quantum hardware, consider applying error mitigation and suppression techniques to reduce the quantum computer's intrinsic noise.
# Construct the Estimator instance.
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 1
estimator.options.default_shots = 5000
Submit a job using the Estimator primitive.
# One pub, with one circuit to run against six different observables.
job = estimator.run([(isa_circuit, mapped_observables)])
# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job.job_id()}")
>>> Job ID: 97ecd036-1767-49b0-a1dc-c71638c3c3c4
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")
After a job is submitted, you can wait until either the job is completed within your current python instance, or use the job_id to retrieve the data at a later time. (See the section on retrieving jobs for details.)
After the job completes, examine its output through the job's result() attribute.
# This is the result of the entire submission. You submitted one Pub,
# so this contains one inner result (and some metadata of its own).
job_result = job.result()
# This is the result from our single pub, which had six observables,
# so contains information on all six.
pub_result = job.result()[0]
Now we can also execute the circuit using the Sampler primitive
# We include the measurements in the circuit
qc.measure_all()
sampler = Sampler(mode=backend)
qc.draw(output="mpl")

Submit a job using the Sampler primitive.
job_sampler = sampler.run(pm.run([qc]))
# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job_sampler.job_id()}")
# Get the results
results_sampler = job_sampler.result()
>>> Job ID: a6ee4d2f-c80d-4a86-9a76-e4b1a74502e7
2.4 Analyze the results
The analyze step is typically where you might postprocess your results using, for example, measurement error mitigation or zero noise extrapolation (ZNE). You might feed these results into another workflow for further analysis or prepare a plot of the key values and data. In general, this step is specific to your problem. For this example, plot each of the expectation values that were measured for our circuit.
The expectation values and standard deviations for the observables you specified to Estimator are accessed through the job result's PubResult.data.evs and PubResult.data.stds attributes. To obtain the results from Sampler, use the PubResult.data.meas.get_counts() function, which will return a dict of measurements in the form of bitstrings as keys and counts as their corresponding values. For more information, see Get started with Sampler.
# Plot the result
from matplotlib import pyplot as plt
values = pub_result.data.evs
errors = pub_result.data.stds
# plotting graph
# Plotting with error bars
plt.errorbar(observables_labels, values, yerr=errors, fmt='-o', capsize=5)
plt.xlabel("Observables")
plt.ylabel("Values")
plt.title("Plot of Observables vs Values with Error Bars")
plt.grid(True)
plt.tight_layout()
plt.show()

We see that the observables and have an expectation value of 1, since introduces two minus signs that cancel out, and acts as the identity, leaving the GHZ state unchanged. The rest of the observables have an expectation value of 0, since their operators introduce an odd number of minus signs, or the operators flip a number of qubits that make the overlapping states orthogonal.
Now we plot the results for the Sampler
counts_list = results_sampler[0].data.meas.get_counts()
print(counts_list)
print(f"Outcomes : {counts_list}")
display(plot_histogram(counts_list, title="GHZ state"))
{'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}
Outcomes : {'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}

2.5 Scale to large numbers of qubits
In quantum computing, utility-scale work is crucial for making progress in the field. Such work requires computations to be done on a much larger scale; working with circuits that might use over 100 qubits and over 1000 gates. This example takes a small step in that direction scaling the GHZ problem to qubits. It uses the Qiskit patterns workflow and ends by measuring the expectation value .
Step 1. Map the problem
Write a function that returns a QuantumCircuit that prepares an -qubit GHZ state (essentially an extended Bell state), then use that function to prepare a 10-qubit GHZ state and collect the observables to be measured.
def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:
qc = QuantumCircuit(n)
qc.h(0)
for i in range(n-1):
qc.cx(i, i+1)
return qc
n = 10
qc_n_GHZ = get_qc_for_n_qubit_GHZ_state(n)
qc_n_GHZ.draw("mpl")

Next, map to the operators of interest. This example uses the ZZ operators between qubits to examine the behavior as they get farther apart. Increasingly inaccurate (corrupted) expectation values between distant qubits would reveal the level of noise present.
# ZZII...II, ZIZI...II, ... , ZIII...IZ
operator_strings = [
"Z" + i * "I" + "Z" + "I" * (n-i-2) for i in range(n-1)
]
print(operator_strings)
print(len(operator_strings))
operators = [SparsePauliOp(operator) for operator in operator_strings]
['ZZIIIIIIII', 'ZIZIIIIIII', 'ZIIZIIIIII', 'ZIIIZIIIII', 'ZIIIIZIIII', 'ZIIIIIZIII', 'ZIIIIIIZII', 'ZIIIIIIIZI', 'ZIIIIIIIIZ']
9
Step 2. Optimize the problem for execution on quantum backend
Transform the circuit and observables to match the backend's ISA.
# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc_n_GHZ)
isa_operators_list = [operator.apply_layout(isa_circuit.layout) for operator in operators]
Step 3. Execute on backend
Submit the job and if you execute it on hardware enable error suppression by using a technique to reduce errors called dynamical decoupling. The resilience level specifies how much resilience to build against errors. Higher levels generate more accurate results, at the expense of longer processing times. For further explanation of the options set in the following code, see Configure error mitigation for Qiskit Runtime.
# Submit the circuit to Estimator
job = estimator.run([(isa_circuit, isa_operators_list)])
job_id = job.job_id()
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")
Step 4. Post-process results
To better understand the behavior of entangled quantum states on real hardware, we analyze the pairwise correlations between qubits in the Z basis. Specifically, we look at the expectation values ⟨Z₀Zᵢ⟩, which measure how strongly qubit 0 is correlated with each other qubit i. In particular we are going to plot:
Which values of do you expect to see in the plot?
Options:
a) Decreasing as we increase
b) Constant in 1
c) Small deviations around 1
d) Alternating 1 and 0 for odd and even values of
data = list(range(1, len(operators) + 1)) # Distance between the Z operators
result = job.result()[0]
values = result.data.evs # Expectation value at each Z operator.
values = [
v / values[0] for v in values
] # Normalize the expectation values to evaluate how they decay with distance.
plt.plot(data, values, marker="o", label=f"{n}-qubit GHZ state")
plt.xlabel("Distance between qubits $i$")
plt.ylabel(r"$\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle $")
plt.legend()
plt.show()

In this plot we notice that fluctuates around the value 1, even though in an ideal simulation all should be 1.
As you can see, the results of 10 qubit experiments are good but still have some errors. One way to improve the results is to implement GHZ state more efficiently.
Usually one implements GHZ state with a staircase-like CNOT gates sequence. However, you can implement GHZ state more efficiently, reducing the 2-qubit depth from n to n/2 or less.
One important metric to benchmark how accurate the results will be, or how little noise will have for a circuit is 2-qubit gate depth. This is because the error rates for 2-qubit gates (~10 times higher than single qubit gates) dominate the errors of the whole circuit. Use the following code to get 2-qubit gate depth of a circuit.
qc.depth(lambda x: x.operation.num_qubits == 2)
def better_ghz(n):
"fan out"
s = int(n / 2)
qc = QuantumCircuit(n)
qc.h(s)
for m in range(s, 0, -1):
qc.cx(m, m - 1)
if not (n % 2 == 0 and m == s):
qc.cx(n - m - 1, n - m)
return qc
better_ghz(n).draw("mpl")

# Check 2-qubit gate depth before transpilation
qc_better_ghz = better_ghz(n)
qc_better_ghz.depth(lambda x: x.operation.num_qubits == 2)
5
An interesting thing to note here is that we were able to reduce the quantum depth of the circuit we want to execute just by being smart and thinking of a different way to program it. However, there will be situations and algorithms where we can't rely on these clever tricks. This is where the transpiler comes in handy, it helps us optimize all these aspects efficiently, so we don't have to worry too much about them.
3. Encoding Information
3.1 Amplitude encoding
Now that we've seen how to build quantum circuits, it is interesting to explore how we can encode classical information into quantum states. One powerful method is, amplitude encoding, where the amplitudes of a quantum state represent the components of a classical vector.
Let's consider a simple example. Suppose we want to encode the classical vector
into a quantum state of two qubits. The goal is to prepare the quantum state:
where (or ) and the vector is normalized such that:
Now we consider the particular example:
Then the corresponding quantum state is:
This state can be prepared using a combination of rotation gates of angles and for qubits 0 and 1 respectively
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np
qc = QuantumCircuit(2)
qc.ry(np.pi / 6, 0)
qc.ry(np.pi / 4, 1)
simulator = AerSimulator()
qc.save_statevector()
result = simulator.run(qc).result()
statevector = result.get_statevector()
print("Statevector:", statevector)
qc.draw(output="mpl")
Statevector: Statevector([0.8923991 +0.j, 0.23911762+0.j, 0.36964381+0.j,
0.09904576+0.j],
dims=(2, 2))
from qiskit.quantum_info import Statevector
# Define our vector
v = np.array([0.8924, 0.3696, 0.2391, 0.0990])
v = v/np.linalg.norm(v)
# Create a statevector from the vector
state = Statevector(v)
# Initialize a quantum circuit with 2 qubits
qc = QuantumCircuit(2)
qc.initialize(state.data, [0, 1])
# Optional: simulate the state
print("Statevector:", state)
# Visualize the circuit
qc.decompose().decompose().decompose().decompose().decompose().draw("mpl")
Statevector: Statevector([0.89242154+0.j, 0.36960892+0.j, 0.23910577+0.j,
0.09900239+0.j],
dims=(2, 2))

Hence we have seen how to encode information using rotational gates.
3.2 Angle encoding and parametrized circuits
A particularly interesting way of encoding information into a quantum computer is designing a quantum circuit that contains some rotational angles or parameters that can be tuned in order to represent a family of functions . Let us for example consider the following parametrized quantum circuit:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
# Define a symbolic parameter
theta = Parameter("θ")
qc = QuantumCircuit(2)
# We applied a parametrized RX gate
qc.rx(theta, 0)
qc.cx(0, 1)
qc.draw("mpl")
Mathematically, we can analyze what is the family of functions we can represent with this circuit:
It is pretty clear that the number of states we can represent with this quantum circuit is limited, as we cannot represent states or for example. However, the family of states we can represent starts to grow when we introduce more rotations in the adequate places:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
# Define a symbolic parameter
theta1 = Parameter("θ1")
theta2 = Parameter("θ2")
qc = QuantumCircuit(2)
qc.rx(theta1, 0)
qc.rx(theta2, 1)
qc.cx(0, 1)
qc.draw("mpl")
In this case, the quantum states we will represent are:
\begin{align*} \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2) R_x^{\{0}}(\theta_1) \ket{00} &= \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2)\left( \cos(\theta_1/2)\ket{00} - i\sin(\theta_1/2)\ket{10} \right) \\ &= \text{CNOT}_{01}\left( \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \right. \\ &\quad \left. - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{10} + \sin(\theta_1/2)\sin(\theta_2/2)\ket{11} \right) \\ &= \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \\ &\quad + \sin(\theta_1/2)\sin(\theta_2/2)\ket{10} - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{11} \end{align*}We can see that this circuit generates a broader family of quantum states compared to the previous one. In particular, it can now produce states with non-zero amplitudes for or that were not possible with the circuit above. However, this circuit is still not a universal quantum state generator, although it may be sufficiently expressive to design circuits with some flexibility for representing certain functions. In general, the more independent parameters (angles) we introduce, the more expresiveness the circuit has to approximate arbitrary quantum states.
Ansatzes and Circuit library
This kind of parameterized quantum circuit can be used to build Ansatzes, trial quantum states that aim to approximate the solution of a problem. These Ansatzes are a central component of Variational Quantum Algorithms, a class of hybrid quantum-classical algorithms that use a quantum computer to evaluate a cost function and a classical optimizer to minimize it. We will go into detail about these topics in a later Unit, but for now, we will introduce how to construct a simple ansatz using the Circuit library in Qiskit.
from qiskit.circuit.library import efficient_su2
SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
SU2_ansatz.decompose().draw(output="mpl")

We have seen how to construct a simple Ansatz using the efficient_su2 function of the qiskit.circuit.library that will be able to generate a wide range of quantum states by tuning its parameters