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

Update all DynamicsBackend code/tests to work with hex for meas level 2 #379

Merged
merged 5 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions qiskit_dynamics/backend/backend_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,13 @@ def _get_memory_slot_probabilities(
defaults to the maximum index in memory_slot_indices. The default value
of unused memory slots is 0.
max_outcome_value: Maximum value that can be stored in a memory slot. All outcomes higher
than this will be rounded down.
than this will be rounded down. Defaults to 1.

Returns:
Dict: Keys are memory slot outcomes, values are the probabilities of those outcomes.
"""
num_memory_slots = num_memory_slots or (max(memory_slot_indices) + 1)
max_outcome_value = max_outcome_value or 1
memory_slot_probs = {}
for level_str, prob in probability_dict.items():
memory_slot_result = ["0"] * num_memory_slots
Expand All @@ -136,7 +137,7 @@ def _get_memory_slot_probabilities(
level = str(max_outcome_value)
memory_slot_result[-(idx + 1)] = level

memory_slot_result = hex(int("".join(memory_slot_result), 2))
memory_slot_result = hex(int("".join(memory_slot_result), max_outcome_value + 1))
if memory_slot_result in memory_slot_probs:
memory_slot_probs[memory_slot_result] += prob
else:
Expand Down
2 changes: 1 addition & 1 deletion qiskit_dynamics/backend/dynamics_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class DynamicsBackend(BackendV2):
Must be a positive float. Defaults to ``0.2``.
* ``max_outcome_level``: For ``meas_level == 2``, the maximum outcome for each subsystem. Values
will be rounded down to be no larger than ``max_outcome_level``. Must be a positive integer or
``None``. If ``None``, no rounding occurs. Defaults to ``1``.
``None``. If ``None``, defaults to ``1``.
* ``memory``: Boolean indicating whether to return a list of explicit measurement outcomes for
every experimental shot. Defaults to ``True``.
* ``seed_simulator``: Seed to use in random sampling. Defaults to ``None``.
Expand Down
4 changes: 2 additions & 2 deletions qiskit_dynamics/models/model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def _kron(A, B):


def vec_commutator(
A: Union[ArrayLike, csr_matrix, List[csr_matrix]]
A: Union[ArrayLike, csr_matrix, List[csr_matrix]],
) -> Union[ArrayLike, csr_matrix, List[csr_matrix]]:
r"""Linear algebraic vectorization of the linear map X -> -i[A, X]
in column-stacking convention. In column-stacking convention we have
Expand Down Expand Up @@ -72,7 +72,7 @@ def vec_commutator(


def vec_dissipator(
L: Union[ArrayLike, csr_matrix, List[csr_matrix]]
L: Union[ArrayLike, csr_matrix, List[csr_matrix]],
) -> Union[ArrayLike, csr_matrix, List[csr_matrix]]:
r"""Linear algebraic vectorization of the linear map
X -> L X L^\dagger - 0.5 * (L^\dagger L X + X L^\dagger L)
Expand Down
10 changes: 10 additions & 0 deletions releasenotes/notes/backend-hex-results-fd3762b9188cdd01.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
upgrade:
- |
:class:`.DynamicsBackend` now returns results in hexadecimal. Previously, results were returned
in ``n``-ary, where ``n`` is the value of the ``max_outcome_level`` option. The hexadecimal
values are generated by calling ``hex(int(x, n))``, where ``n`` is ``max_outcome_level`` and
``x`` is the original ``n``-ary value.
- |
Corresponding to the above change, the default behaviour for ``max_outcome_level`` is now to
treat a ``None`` value as ``1``.
21 changes: 13 additions & 8 deletions test/dynamics/backend/test_backend_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,30 +182,33 @@ def test_trivial_case(self):
probability_dict = {"000": 0.25, "001": 0.3, "200": 0.4, "010": 0.05}

output = _get_memory_slot_probabilities(
probability_dict=probability_dict, memory_slot_indices=[0, 1, 2]
probability_dict=probability_dict, memory_slot_indices=[0, 1, 2], max_outcome_value=2
)
self.assertDictEqual(output, probability_dict)
self.assertDictEqual(output, {hex(int(k, 3)): v for k, v in probability_dict.items()})

def test_basic_reordering(self):
"""Test case with simple re-ordering."""

probability_dict = {"000": 0.25, "001": 0.3, "200": 0.4, "010": 0.05}

output = _get_memory_slot_probabilities(
probability_dict=probability_dict, memory_slot_indices=[2, 0, 1]
probability_dict=probability_dict, memory_slot_indices=[2, 0, 1], max_outcome_value=2
)
self.assertDictEqual(output, {"000": 0.25, "100": 0.3, "020": 0.4, "001": 0.05})
expected = {"000": 0.25, "100": 0.3, "020": 0.4, "001": 0.05}

self.assertDictEqual(output, {hex(int(k, 3)): v for k, v in expected.items()})

def test_extra_memory_slots(self):
"""Test case with more memory slots than there are digits in probability_dict keys."""

probability_dict = {"000": 0.25, "001": 0.3, "200": 0.4, "010": 0.05}

output = _get_memory_slot_probabilities(
probability_dict=probability_dict,
memory_slot_indices=[3, 0, 1],
probability_dict=probability_dict, memory_slot_indices=[3, 0, 1], max_outcome_value=2
)
self.assertDictEqual(output, {"0000": 0.25, "1000": 0.3, "0020": 0.4, "0001": 0.05})
expected = {"0000": 0.25, "1000": 0.3, "0020": 0.4, "0001": 0.05}

self.assertDictEqual(output, {hex(int(k, 3)): v for k, v in expected.items()})

def test_bound_and_merging(self):
"""Test case with max outcome bound."""
Expand All @@ -218,7 +221,9 @@ def test_bound_and_merging(self):
num_memory_slots=4,
max_outcome_value=1,
)
self.assertDictEqual(output, {"0000": 0.25, "0100": 0.3, "0010": 0.4, "0001": 0.05})
expected = {"0000": 0.25, "0100": 0.3, "0010": 0.4, "0001": 0.05}

self.assertDictEqual(output, {hex(int(k, 2)): v for k, v in expected.items()})


class Test_sample_probability_dict(QiskitDynamicsTestCase):
Expand Down
52 changes: 21 additions & 31 deletions test/dynamics/backend/test_dynamics_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,7 @@ def test_circuit_with_multiple_classical_registers(self):
circ.add_calibration("x", [0], x_sched0)

result = self.simple_backend.run(circ, seed_simulator=1234567).result()
self.assertDictEqual(result.get_counts(), {"00": 1024})
self.assertTrue(result.get_memory() == ["00"] * 1024)
self.assertTrue(all(x == "0x0" for x in result.to_dict()["results"][0]["data"]["memory"]))

def test_circuit_with_target_pulse_instructions(self):
"""Test running a circuit on a simulator with defined instructions."""
Expand Down Expand Up @@ -483,19 +482,14 @@ def test_circuit_memory_slot_num(self):
result0 = self.backend_2q.run(circ0, seed_simulator=1234567).result()
result1 = self.backend_2q.run(circ1, seed_simulator=1234567).result()

# extract results form memory slots and validate all others are 0
result0_dict = {}
for string, count in result0.get_counts().items():
self.assertTrue(string[:3] == "000")
result0_dict[string[3:]] = count

result1_dict = {}
for string, count in result1.get_counts().items():
self.assertTrue(string[-4] + string[-2] + string[-1] == "000")
result1_dict[string[-5] + string[-3]] = count
# extract results from both experiments and validate consistency
# results object converts memory into binary
result0_dict = result0.get_counts()
result1_dict = result1.get_counts()

# validate consistent results
self.assertDictEqual(result0_dict, result1_dict)
self.assertEqual(result0_dict["1"], result1_dict["100"])
self.assertEqual(result0_dict["11"], result1_dict["10100"])
self.assertEqual(result0_dict["10"], result1_dict["10000"])

def test_schedule_memory_slot_num(self):
"""Test correct memory_slot number in schedule."""
Expand All @@ -518,18 +512,14 @@ def test_schedule_memory_slot_num(self):
result0 = self.backend_2q.run(schedule0, seed_simulator=1234567).result()
result1 = self.backend_2q.run(schedule1, seed_simulator=1234567).result()

# extract results form memory slots and validate all others are 0
# extract results from both experiments and validate consistency
# results object converts memory into binary
result0_dict = result0.get_counts()
for string in result0_dict:
self.assertTrue(len(string) == 2)
result1_dict = result1.get_counts()

result1_dict = {}
for string, count in result1.get_counts().items():
self.assertTrue(string[-4] + string[-2] + string[-1] == "000")
result1_dict[string[-5] + string[-3]] = count

# validate consistent results
self.assertDictEqual(result0_dict, result1_dict)
self.assertEqual(result0_dict["1"], result1_dict["100"])
self.assertEqual(result0_dict["11"], result1_dict["10100"])
self.assertEqual(result0_dict["10"], result1_dict["10000"])

result0_iq = (
self.backend_2q.run(schedule0, meas_level=1, seed_simulator=1234567)
Expand All @@ -551,15 +541,15 @@ def test_measure_higher_levels(self):

solver = Solver(static_hamiltonian=np.diag([-1.0, 0.0, 1.0]), dt=0.1)
qutrit_backend = DynamicsBackend(
solver=solver, max_outcome_level=None, initial_state=Statevector([0.0, 0.0, 1.0])
solver=solver, max_outcome_level=2, initial_state=Statevector([0.0, 0.0, 1.0])
)

circ = QuantumCircuit(1, 1)
circ.measure([0], [0])

res = qutrit_backend.run(circ).result()

self.assertDictEqual(res.get_counts(), {"2": 1024})
self.assertTrue(all(x == "0x2" for x in res.to_dict()["results"][0]["data"]["memory"]))

def test_setting_experiment_result_function(self):
"""Test overriding default experiment_result_function."""
Expand Down Expand Up @@ -610,7 +600,7 @@ def test_metadata_transfer(self):

solver = Solver(static_hamiltonian=np.diag([-1.0, 0.0, 1.0]), dt=0.1)
qutrit_backend = DynamicsBackend(
solver=solver, max_outcome_level=None, initial_state=Statevector([0.0, 0.0, 1.0])
solver=solver, max_outcome_level=2, initial_state=Statevector([0.0, 0.0, 1.0])
)

circ0 = QuantumCircuit(1, 1, metadata={"key0": "value0"})
Expand All @@ -619,10 +609,9 @@ def test_metadata_transfer(self):
circ1.measure([0], [0])

res = qutrit_backend.run([circ0, circ1]).result()

self.assertDictEqual(res.get_counts(0), {"2": 1024})
self.assertTrue(all(x == "0x2" for x in res.to_dict()["results"][0]["data"]["memory"]))
self.assertDictEqual(res.results[0].header.metadata, {"key0": "value0"})
self.assertDictEqual(res.get_counts(1), {"2": 1024})
self.assertTrue(all(x == "0x2" for x in res.to_dict()["results"][1]["data"]["memory"]))
self.assertDictEqual(res.results[1].header.metadata, {"key1": "value1"})

def test_valid_measurement_properties(self):
Expand Down Expand Up @@ -1038,8 +1027,9 @@ def test_simple_example(self):
backend=self.simple_backend,
seed=1234567,
)
expected = {"000": 513, "010": 511}

self.assertDictEqual(output.data.counts, {"000": 513, "010": 511})
self.assertDictEqual(output.data.counts, {hex(int(k, 2)): v for k, v in expected.items()})


class Test_get_channel_backend_freqs(QiskitDynamicsTestCase):
Expand Down
2 changes: 1 addition & 1 deletion test/dynamics/models/test_generator_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# files need to carry a notice indicating that they have been altered from the originals.
# pylint: disable=invalid-name,no-member

"""Tests for generator_models.py. """
"""Tests for generator_models.py."""

from functools import partial

Expand Down
Loading