Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fix: Measurement properties automatic padding for DynamicsBackend initialization #209

Merged
merged 10 commits into from
Apr 28, 2023
Merged
34 changes: 30 additions & 4 deletions qiskit_dynamics/backend/dynamics_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,18 @@ def __init__(

# add default simulator measure instructions
measure_properties = {}
instruction_schedule_map = target.instruction_schedule_map()
for qubit in self.options.subsystem_labels:
instruction_schedule_map = target.instruction_schedule_map()
if not instruction_schedule_map.has(instruction="measure", qubits=qubit):
with pulse.build() as meas_sched:
pulse.acquire(
duration=1, qubit_or_channel=qubit, register=pulse.MemorySlot(qubit)
)

measure_properties[(qubit,)] = InstructionProperties(calibration=meas_sched)
measure_properties[(qubit,)] = InstructionProperties(calibration=meas_sched)

target.add_instruction(Measure(), measure_properties)
if bool(measure_properties):
target.add_instruction(Measure(), measure_properties)

target.dt = solver._dt

Expand Down Expand Up @@ -682,6 +683,31 @@ def from_backend(
channels=hamiltonian_channels,
)

# Add control_channel_map from backend (only if not specified before by user)
if "control_channel_map" not in options:
if hasattr(backend, "control_channels"):
control_channel_map_backend = {
qubits: backend.control_channels[qubits][0].index
for qubits in backend.control_channels
}

elif hasattr(backend.configuration(), "control_channels"):
control_channel_map_backend = {
qubits: backend.configuration().control_channels[qubits][0].index
for qubits in backend.configuration().control_channels
}

else:
control_channel_map_backend = {}

# Reduce control_channel_map based on which channels are in the model
if bool(control_channel_map_backend):
control_channel_map = {}
for label, idx in control_channel_map_backend.items():
if f"u{idx}" in hamiltonian_channels:
control_channel_map[label] = idx
options["control_channel_map"] = control_channel_map

# build the solver
if rotating_frame == "auto":
if "dense" in evaluation_mode:
Expand Down Expand Up @@ -867,7 +893,7 @@ def _get_acquire_instruction_timings(
) -> Tuple[List[List[float]], List[List[int]], List[List[int]]]:
"""Get the required data from the acquire commands in each schedule.

Additionally validates that each schedule has acquire instructions occurring at one time, at
Additionally validates that each schedule has Acquire instructions occurring at one time, at
least one memory slot is being listed, and all measured subsystems exist in
``valid_subsystem_labels``.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
features:
- |
The :meth:`DynamicsBackend.from_backend` method has been updated to automatically populate the
`control_channel_map` option based on the supplied backend if the user does not supply one.

fixes:
- |
A bug in :meth:`DynamicsBackend.__init__` causing existing measurement instructions for a user-supplied
:class:`Target` to be overwritten has been fixed.

102 changes: 101 additions & 1 deletion test/dynamics/backend/test_dynamics_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from scipy.sparse import csr_matrix

from qiskit import QiskitError, pulse, QuantumCircuit
from qiskit.circuit.library import XGate
from qiskit.circuit.library import XGate, Measure
from qiskit.transpiler import Target, InstructionProperties
from qiskit.quantum_info import Statevector, DensityMatrix
from qiskit.result.models import ExperimentResult, ExperimentResultData
Expand Down Expand Up @@ -264,6 +264,7 @@ def setUp(self):
dt=0.1,
rotating_frame=static_ham_2q,
)
self.solver_2q = solver_2q
self.backend_2q = DynamicsBackend(solver=solver_2q, subsystem_dims=[2, 2])

# function to discriminate 0 and 1 for default centers.
Expand Down Expand Up @@ -549,6 +550,55 @@ def test_metadata_transfer(self):
self.assertDictEqual(res.get_counts(1), {"2": 1024})
self.assertDictEqual(res.results[1].header.metadata, {"key1": "value1"})

def test_valid_measurement_properties(self):
"""Test that DynamicsBackend instantiation always carries measurement instructions."""

# Case where no measurement instruction is added manually
instruction_schedule_map = self.backend_2q.target.instruction_schedule_map()
for q in range(self.simple_backend.num_qubits):
self.assertTrue(instruction_schedule_map.has(instruction="measure", qubits=q))
self.assertTrue(
isinstance(
instruction_schedule_map.get("measure", q).instructions[0][1], pulse.Acquire
)
)
self.assertEqual(len(instruction_schedule_map.get("measure", q).instructions), 1)

# Case where measurement instruction is added manually
custom_meas_duration = 3
with pulse.build() as meas_sched0:
pulse.acquire(
duration=custom_meas_duration, qubit_or_channel=0, register=pulse.MemorySlot(0)
)

with pulse.build() as meas_sched1:
pulse.acquire(
duration=custom_meas_duration, qubit_or_channel=1, register=pulse.MemorySlot(1)
)

measure_properties = {
(0,): InstructionProperties(calibration=meas_sched0),
(1,): InstructionProperties(calibration=meas_sched1),
}
target = Target()
target.add_instruction(Measure(), measure_properties)
custom_meas_backend = DynamicsBackend(
solver=self.solver_2q, target=target, subsystem_dims=[2, 2]
)
instruction_schedule_map = custom_meas_backend.target.instruction_schedule_map()
for q in range(self.simple_backend.num_qubits):
self.assertTrue(instruction_schedule_map.has(instruction="measure", qubits=q))
self.assertTrue(
isinstance(
instruction_schedule_map.get("measure", q).instructions[0][1], pulse.Acquire
)
)
self.assertEqual(instruction_schedule_map.get("measure", q).instructions, 1)
self.assertEqual(
instruction_schedule_map.get("measure", q).instructions[0][1].duration,
custom_meas_duration,
)


class TestDynamicsBackend_from_backend(QiskitDynamicsTestCase):
"""Test class for DynamicsBackend.from_backend and resulting DynamicsBackend instances."""
Expand Down Expand Up @@ -617,6 +667,17 @@ def setUp(self):
[UchannelLO(3, (1 + 0j))],
]

configuration.control_channels = {
(0, 1): [pulse.ControlChannel(0)],
(1, 0): [pulse.ControlChannel(1)],
(1, 2): [pulse.ControlChannel(2)],
(2, 1): [pulse.ControlChannel(3)],
(1, 3): [pulse.ControlChannel(4)],
(3, 1): [pulse.ControlChannel(5)],
(3, 4): [pulse.ControlChannel(6)],
(4, 3): [pulse.ControlChannel(7)],
}

defaults = SimpleNamespace()
defaults.qubit_freq_est = [
5175383639.513607,
Expand All @@ -630,6 +691,7 @@ def setUp(self):
backend = SimpleNamespace()
backend.configuration = lambda: configuration
backend.defaults = lambda: defaults
backend.control_channels = backend.configuration().control_channels

self.valid_backend = backend

Expand Down Expand Up @@ -827,6 +889,44 @@ def test_building_model_case2(self):
)
self.assertAllClose(expected_operators / 1e9, solver.model.operators / 1e9)

def test_setting_control_channel_map(self):
"""Test automatic padding of control_channel_map in DynamicsBackend
options from original backend."""

# Check that manual setting of the map overrides the one from original backend
control_channel_map = {(0, 1): 4}
backend = DynamicsBackend.from_backend(
self.valid_backend, control_channel_map=control_channel_map
)
self.assertDictEqual(backend.options.control_channel_map, {(0, 1): 4})

# Check that control_channel_map from original backend is set in DynamicsBackend.options
backend = DynamicsBackend.from_backend(self.valid_backend)
self.assertDictEqual(
backend.options.control_channel_map,
{
(0, 1): 0,
(1, 0): 1,
(1, 2): 2,
(2, 1): 3,
(1, 3): 4,
(3, 1): 5,
(3, 4): 6,
(4, 3): 7,
},
)

# Check that reduction to subsystem_list is correct
backend = DynamicsBackend.from_backend(self.valid_backend, subsystem_list=[0, 1, 2])
self.assertDictEqual(
backend.options.control_channel_map,
{(0, 1): 0, (1, 0): 1, (1, 2): 2, (2, 1): 3, (1, 3): 4},
)

# Check that manually setting the option after the declaration overwrites the previous map
backend.set_options(control_channel_map={(0, 1): 3})
self.assertDictEqual(backend.options.control_channel_map, {(0, 1): 3})


class Test_default_experiment_result_function(QiskitDynamicsTestCase):
"""Test default_experiment_result_function."""
Expand Down