Quantum Gates and Circuits: From NOT and CNOT to Algorithms with Qiskit
If the qubit is the information unit of quantum computing, the quantum gates they are the operations that transform it — the equivalent of the classic Boolean operators. But unlike classical AND, OR and NOT, quantum gates are always reversible and they operate on state vector amplitudes, not deterministic binary values.
In this article we build understanding of the main gates — Hadamard, X, Z, CNOT, SWAP — and we use them to build the first real quantum algorithm: the Bell state, which demonstrates entanglement. We then run the circuit on real IBM hardware via IBM Quantum Platform.
What You Will Learn
- 1-qubit gates: Hadamard (H), X (NOT), Z, S, T and their matrices
- 2-qubit gates: CNOT, CZ, SWAP and Toffoli
- Build circuits with Qiskit QuantumCircuit
- Bell State: The fundamental circuit that demonstrates entanglement
- Simulation with Qiskit Aer and visualization of results
- Running on real IBM hardware with Sampler Primitive
- Transpilation: from the logic circuit to the native hardware gates
1 Qubit Gates: State Vector Transformations
A 1-qubit gate and a unit 2x2 matrix (U^† U = I) that transforms the state vector. Unitarity guarantees reversibility — you can always "undo" the operation by applying the door added.
# Porte a 1 qubit: definizione matematica e Qiskit
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator, Statevector
# Gate X (Pauli-X) — il NOT quantistico
# Matrice: [[0, 1], [1, 0]]
# Effetto: |0⟩ -> |1⟩, |1⟩ -> |0⟩
print("Gate X (NOT):")
print(Operator.from_label('X').data)
qc_x = QuantumCircuit(1)
qc_x.x(0)
print(f"X|0⟩ = {Statevector.from_instruction(qc_x).data}")
# [0.+0.j, 1.+0.j] = |1⟩
# Gate Hadamard (H) — crea superposizione
# Matrice: [[1/√2, 1/√2], [1/√2, -1/√2]]
# Effetto: |0⟩ -> |+⟩, |1⟩ -> |−⟩
print("\nGate H (Hadamard):")
qc_h = QuantumCircuit(1)
qc_h.h(0)
print(f"H|0⟩ = {Statevector.from_instruction(qc_h).data}")
# [0.707+0.j, 0.707+0.j] = |+⟩ = (|0⟩+|1⟩)/√2
# Gate Z (Pauli-Z) — phase flip
# Matrice: [[1, 0], [0, -1]]
# Effetto: |0⟩ -> |0⟩, |1⟩ -> -|1⟩ (cambia la fase di |1⟩)
print("\nGate Z (Phase Flip):")
print(f"Z|+⟩ = ?")
qc_z = QuantumCircuit(1)
qc_z.h(0) # prima crea |+⟩
qc_z.z(0) # poi applica Z
print(f"Z|+⟩ = {Statevector.from_instruction(qc_z).data}")
# [0.707+0.j, -0.707+0.j] = |−⟩
# Gate S e T — rotazioni di fase piu fini
# S = [[1,0],[0,i]]: rotazione di π/2 attorno asse Z
# T = [[1,0],[0,e^{iπ/4}]]: rotazione di π/4 attorno asse Z
qc_st = QuantumCircuit(1)
qc_st.h(0)
qc_st.s(0)
qc_st.t(0)
print(f"\nT(S|+⟩) = {Statevector.from_instruction(qc_st).data}")
2 Qubit gates: CNOT, CZ and SWAP
2-qubit gates operate on pairs of qubits and are necessary to create entanglement. The most important is the CNOT (Controlled-NOT):
# Porte a 2 qubit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
# CNOT (CX): se qubit control e |1⟩, flip il qubit target
# Matrice 4x4: [[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]
print("CNOT truth table:")
for control_state, target_state in [(0,0), (0,1), (1,0), (1,1)]:
qc = QuantumCircuit(2)
if control_state == 1:
qc.x(0)
if target_state == 1:
qc.x(1)
qc.cx(0, 1) # qubit 0 = control, qubit 1 = target
sv = Statevector.from_instruction(qc)
result_probs = sv.probabilities_dict()
result = max(result_probs, key=result_probs.get)
print(f" CNOT|{control_state}{target_state}⟩ = |{result}⟩")
# CNOT|00⟩ = |00⟩
# CNOT|01⟩ = |01⟩
# CNOT|10⟩ = |11⟩ <- target si inverte quando control e 1
# CNOT|11⟩ = |10⟩ <- target si inverte quando control e 1
# SWAP: scambia gli stati dei due qubit
qc_swap = QuantumCircuit(2)
qc_swap.x(0) # qubit 0 = |1⟩, qubit 1 = |0⟩
qc_swap.swap(0, 1)
sv_swap = Statevector.from_instruction(qc_swap)
print(f"\nSWAP|10⟩ = {max(sv_swap.probabilities_dict(), key=sv_swap.probabilities_dict().get)}")
# SWAP|10⟩ = |01⟩
# Toffoli (CCX): CNOT con 2 qubit di controllo
# Il gate universale per computazione classica reversibile
qc_toffoli = QuantumCircuit(3)
qc_toffoli.x(0) # control 1 = |1⟩
qc_toffoli.x(1) # control 2 = |1⟩
qc_toffoli.ccx(0, 1, 2) # flip target solo se entrambi i control sono |1⟩
sv_t = Statevector.from_instruction(qc_toffoli)
print(f"Toffoli|110⟩ = {max(sv_t.probabilities_dict(), key=sv_t.probabilities_dict().get)}")
# Toffoli|110⟩ = |111⟩
The Bell State: First Complete Circuit
The Bell state is the simplest circuit that demonstrates real entanglement. It only requires two gates — Hadamard and CNOT — and is the basis of many quantum algorithms:
# Bell State: costruzione, simulazione e analisi
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector, DensityMatrix
from qiskit_aer import AerSimulator
from qiskit import transpile
# Costruzione del circuito Bell State |Φ+⟩
qc_bell = QuantumCircuit(2, 2, name='Bell State')
# Passo 1: Hadamard su qubit 0
# Stato: (|00⟩ + |10⟩) / √2
qc_bell.h(0)
# Passo 2: CNOT con qubit 0 come control, qubit 1 come target
# Stato: (|00⟩ + |11⟩) / √2 <- Bell state!
qc_bell.cx(0, 1)
# Visualizza il circuito
print("Circuito Bell State:")
print(qc_bell.draw('text'))
print()
# Analisi statevector (senza misura)
sv = Statevector.from_instruction(qc_bell)
print(f"Statevector: {sv.data}")
# [0.707, 0, 0, 0.707] -> 50% |00⟩, 50% |11⟩
print(f"Probabilita: {sv.probabilities_dict()}")
# {'00': 0.5, '11': 0.5} <- nessuna probabilita per '01' e '10'
# Aggiungi misura e simula
qc_bell.measure([0, 1], [0, 1])
sim = AerSimulator()
compiled = transpile(qc_bell, sim)
result = sim.run(compiled, shots=10000).result()
counts = result.get_counts()
print(f"\nRisultati simulazione (10000 shots): {counts}")
# {'00': ~5000, '11': ~5000} — MAI '01' o '10'
# Verifica entanglement: calcola concurrence
dm = DensityMatrix.from_instruction(QuantumCircuit(2).compose(
QuantumCircuit(2).compose(QuantumCircuit(2))
))
# (entanglement misurabile con partial trace e reduced density matrix)
The 4 Bell States
There are 4 Bell states — the basis of the 4-dimensional Hilbert space of 2 qubits:
# Tutti e 4 i Bell States
from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
def create_bell_state(phi_plus=True, psi=False) -> QuantumCircuit:
"""
|Φ+⟩ = (|00⟩ + |11⟩)/√2 (phi_plus=True, psi=False)
|Φ-⟩ = (|00⟩ - |11⟩)/√2 (phi_plus=False, psi=False)
|Ψ+⟩ = (|01⟩ + |10⟩)/√2 (phi_plus=True, psi=True)
|Ψ-⟩ = (|01⟩ - |10⟩)/√2 (phi_plus=False, psi=True)
"""
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
if psi:
qc.x(1) # Flip target: |00⟩ e |11⟩ diventano |01⟩ e |10⟩
if not phi_plus:
qc.z(0) # Phase flip: + diventa -
return qc
bell_states = {
'|Φ+⟩': create_bell_state(True, False),
'|Φ-⟩': create_bell_state(False, False),
'|Ψ+⟩': create_bell_state(True, True),
'|Ψ-⟩': create_bell_state(False, True),
}
for name, qc in bell_states.items():
sv = Statevector.from_instruction(qc)
probs = {k: round(v, 3) for k, v in sv.probabilities_dict().items() if v > 0.01}
print(f"{name}: probabilita = {probs}")
Running on Real IBM Hardware
Simulate and useful to develop, but the real test and physical hardware. Here is the workflow complete with Qiskit v2 to run Bell state on a real IBM processor:
# Esecuzione su hardware IBM reale con Qiskit v2 Primitives
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Autenticazione
service = QiskitRuntimeService(channel='ibm_quantum')
# Seleziona il backend meno occupato (per account gratuito)
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=2
)
print(f"Backend: {backend.name}")
print(f"Qubit disponibili: {backend.num_qubits}")
print(f"Job in coda: {backend.status().pending_jobs}")
# Crea il circuito Bell State
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
# NUOVO in Qiskit v2: transpile ottimizzato per l'hardware specifico
# 83x piu veloce rispetto a Qiskit 0.x per circuiti complessi
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1 # 0=veloce, 1=bilanciato, 2=lento ma ottimale
)
isa_circuit = pass_manager.run(qc)
print(f"\nGate originali: {qc.count_ops()}")
print(f"Gate dopo transpilation: {isa_circuit.count_ops()}")
# Il transpiler aggiunge swap gates perche l'hardware ha connettivita limitata
# Esegui con SamplerV2 Primitive
sampler = Sampler(backend)
job = sampler.run([isa_circuit], shots=4096)
print(f"\nJob ID: {job.job_id()}")
print("Attendere risultati dall'hardware quantistico...")
print("(tipicamente 1-5 minuti in coda + 30 secondi di esecuzione)")
result = job.result()
counts = result[0].data.c.get_counts()
print(f"\nRisultati hardware reale (4096 shots):")
for state, count in sorted(counts.items()):
print(f" |{state}⟩: {count} ({count/4096*100:.1f}%)")
# Output tipico su hardware reale (con noise):
# |00⟩: ~1850 (45.2%) <- meno di 50% a causa del noise
# |11⟩: ~1920 (46.9%)
# |01⟩: ~150 (3.7%) <- errori da gate noise
# |10⟩: ~176 (4.3%) <- errori da gate noise
Transpilation: From Logic Circuit to Hardware
A logic circuit uses abstract gates (H, CNOT, etc.) but every IBM processor has a set of different native gates. The transpiler converts the logic circuit into native instructions of the specific hardware, also optimizing the routing (the physical qubits are not all connected to each other).
# Analisi della transpilation
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
service = QiskitRuntimeService(channel='ibm_quantum')
backend = service.least_busy(operational=True, simulator=False)
# Circuito di esempio: 3 qubit, multiple gate
qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.cx(1, 2)
qc.rx(0.5, 0)
qc.ry(1.0, 1)
# Transpilation a diversi livelli di ottimizzazione
for opt_level in [0, 1, 2]:
pm = generate_preset_pass_manager(backend=backend, optimization_level=opt_level)
transpiled = pm.run(qc)
ops = transpiled.count_ops()
total_gates = sum(ops.values())
print(f"Optimization level {opt_level}: {total_gates} gates totali, ops={ops}")
# L'IBM Heron ha come native gates: CZ, RZ, SX, X
# Il transpiler decompone ogni gate logico in queste native gate
# Ottimizzazione level 2 riduce tipicamente il circuit depth del 20-40%
Important Limitations to Keep in Mind
- Hardware noise: Results on real hardware always include errors. Don't use quantum hardware when you need deterministic results — use the simulator for development and hardware for final validation
- Circuit depth limits: Gates must complete before decoherence. On IBM Heron 2026, maximum practical depth and ~100-200 layers before the noise is excessive
- Queue time: Queue time on real hardware can range from minutes to hours. Plan accordingly for development workflows
- Gate universality: H + CNOT (or Toffoli) are universal — any classically computable computation and expressible with these gates
Conclusions
Quantum gates are reversible linear transformations on the state vector — mathematically elegant and computationally powerful. The Bell state demonstrates in 2 gates something it doesn't have classical equivalent: entanglement. The Qiskit v2 workflow — circuit design, transpilation, Sampler Primitive — and mature enough to support real development.
The next article uses these building blocks to build the first quantum algorithm advantage: Grover's algorithm for O(sqrt N) quadratic search on unstructured databases.







