Test a Program against a hardware profile¶
A hardware-profile simulator checks whether a Program obeys a selected
device's rules as written. Like the general
Simulator, it applies circuit operations and can
include noise channels. It also enforces a native operation set, placement,
connectivity, capacity, and, for atom arrays, occupancy.
Use a profile to test choices such as which operations to use and where to place program qubits. The profile validates those choices; you supply the Program and layout.
Compare circuit behavior¶
The general-purpose simulator can run this Bell Program, which uses H and
CX without specifying device placement:
>>> import numpy as np
>>> import fatqat as fq
>>> import fatqat.operations as ops
>>> bell = fq.Program(2, 2)
>>> bell.add(ops.H, 0)
>>> bell.add(ops.CX, (0, 1))
>>> bell.measure_all()
>>> counts = fq.simulator.Simulator(runtime="numpy").run(
... bell,
... shots=16,
... simulation_config={"seed": 7},
... ).result().get_counts()
>>> sum(counts.values())
16
>>> set(counts) <= {"00", "11"}
True
Now ask the constrained superconducting profile about the same Program. The
profile can report whether it supports H, so you do not need to copy its
gate table into application code:
>>> profile = fq.simulator.SCQubitSimulator(
... num_qubits=6,
... couplings=((0, 1), (1, 2), (3, 4), (4, 5),
... (0, 3), (1, 4), (2, 5)),
... runtime="numpy",
... )
>>> profile.implementation_map.supports(ops.H)
False
Submitting bell would therefore raise
UnsupportedOperationError. FatQat does not silently
decompose H or CX into this profile's native operations.
Make placement explicit¶
Native operations are only half the question. On this 2 x 3 profile, integer device labels are arranged row by row:
0 --- 1 --- 2
| | |
3 --- 4 --- 5
The next Program is native, but placing its two qubits at 0 and 4 asks for
a diagonal CZ that the grid does not provide:
>>> qubits = fq.QuantumRegister(2, name="q")
>>> native = fq.Program([qubits])
>>> native.add(ops.X, qubits[0])
>>> native.add(ops.X, qubits[1])
>>> native.add(ops.CZ, (qubits[0], qubits[1]))
>>> bad_layout = fq.ResourceLayout({qubits[0]: 0, qubits[1]: 4})
>>> try:
... profile.run(native, resource_layout=bad_layout)
... except fq.errors.UnsupportedOperationError as error:
... print(error)
CZGate is not supported on device operands (0, 4)
Move the second program qubit to the neighbouring device label 1; the
Program itself does not need to change:
>>> layout = fq.ResourceLayout({qubits[0]: 0, qubits[1]: 1})
>>> state = profile.run(native, resource_layout=layout).result().get_statevector()
>>> state.shape
(4,)
>>> int(np.argmax(np.abs(state) ** 2))
3
This confirms that the operation set and placement are valid. The profile can also apply noise channels, as the next example shows. Use a physical emulator when the question depends on pulse shapes or how the state evolves during a control.
Add reference noise deliberately¶
The superconducting profile is ideal unless a noise model is passed. Its packaged model is a useful comparison baseline, not a current hardware characterization:
profile_type = fq.simulator.SCQubitSimulator
noisy_profile = profile_type(
num_qubits=6,
couplings=((0, 1), (1, 2), (3, 4), (4, 5),
(0, 3), (1, 4), (2, 5)),
runtime="numpy",
noise=profile_type.default_noise_model(),
)
measured_native = fq.Program(2, 2)
measured_native.add(ops.X, 0)
measured_native.add(ops.X, 1)
measured_native.add(ops.CZ, (0, 1))
measured_native.measure_all()
noisy_counts = noisy_profile.run(
measured_native,
shots=100,
simulation_config={"seed": 7},
).result().get_counts()
Compare runs with and without this model to study its effect on the output.
AtomArraySimulator has no packaged reference noise model; pass a
NoiseModel of your own when loading, loss, or other
effects belong in the experiment.
Track atom occupancy and pairing¶
Unlike the superconducting profile, the atom array has no fixed geometry.
Program resources define sites that begin empty. Put loads
the atoms, while Pair and Unpair reshape the connectivity on which CZ
is legal:
The changing distance is a picture of the pairing intent, not a simulated
trajectory. AtomArraySimulator records no coordinates or movement duration;
Pair declares that the two occupied sites may execute native CZ, and
Unpair removes that eligibility.
>>> atoms = fq.Program(2, 2)
>>> atoms.add(ops.Put, (0, 1))
>>> atoms.add(ops.Pair, (0, 1))
>>> atoms.add(ops.RX(np.pi), 0)
>>> atoms.add(ops.CZ, (0, 1))
>>> atoms.add(ops.Unpair, (0, 1))
>>> atoms.measure_all()
>>> atom_counts = fq.simulator.AtomArraySimulator().run(
... atoms,
... shots=8,
... simulation_config={"seed": 7},
... ).result().get_counts()
>>> atom_counts
{'10': 8}
Pairing is ideal unless you attach a noise assumption. For example, apply a small depolarizing channel independently to each atom whenever either movement instruction occurs:
>>> movement_noise = fq.NoiseModel()
>>> for movement in (ops.Pair, ops.Unpair):
... for target_position in (0, 1):
... movement_noise.add(
... fq.noise.Depolarizing(p=0.02),
... operation=movement,
... target_positions=target_position,
... )
>>> noisy_atom_backend = fq.simulator.AtomArraySimulator(
... noise=movement_noise,
... )
>>> noisy_counts = noisy_atom_backend.run(
... atoms,
... shots=100,
... simulation_config={"seed": 7},
... ).result().get_counts()
>>> sum(noisy_counts.values())
100
This channel perturbs the quantum state during Pair and Unpair; it does not
remove either atom. Use Loss instead when movement
should change occupancy.
Loss can be attached to any supported operation. Here it is sampled after RX,
so surviving atoms return 1, while lost atoms return 2:
>>> loss_model = fq.NoiseModel()
>>> loss_model.add(fq.noise.Loss(p=0.1), operation=ops.RX)
>>> lossy_atoms = fq.Program(1, 1)
>>> lossy_atoms.add(ops.Put, 0)
>>> lossy_atoms.add(ops.RX(np.pi), 0)
>>> lossy_atoms.measure_all()
>>> lossy_counts = fq.simulator.AtomArraySimulator(noise=loss_model).run(
... lossy_atoms,
... shots=100,
... simulation_config={"seed": 7},
... ).result().get_counts()
>>> lossy_counts
{'1': 86, '2': 14}
Pair before CZ is a program error; FatQat does not transport or
pair atoms automatically. A missing atom is different: supported gates find
nothing to act on, and measurement reports the erasure digit 2.
For native gates, program sizing, and method support, use the hardware-profile API. For pulse duration, physical levels, drift, or continuous-time noise, see Hamiltonian emulation.