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

feat: add density matrix simulation via qiskit Aer #380

Merged
merged 27 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
607f639
add AerDensityMatrixBackend class
CalMacCQ Aug 15, 2024
7c73f2d
update documentation
CalMacCQ Aug 15, 2024
35fd4f0
add AerDensityMatrixBackend
CalMacCQ Aug 22, 2024
611f5ae
add test for the noiseless case
CalMacCQ Aug 22, 2024
8b6c4b8
add backend to API docs
CalMacCQ Aug 22, 2024
166333b
remove duplicate documentation
CalMacCQ Aug 22, 2024
67456b7
Merge branch 'main' into feat/Aer_density
CalMacCQ Aug 22, 2024
853ca8e
add changelog entry
CalMacCQ Aug 22, 2024
4e8e969
add docstring for density matrix backend
CalMacCQ Aug 22, 2024
a6ede1f
slight simplification to process_circuits
CalMacCQ Aug 22, 2024
36953f9
make noise_model arg conistent with AerBackend
CalMacCQ Aug 23, 2024
7a62e76
add test for noisy density matrix simulation
CalMacCQ Aug 23, 2024
152d569
attempt to add NoiseModel to BackendInfo
CalMacCQ Aug 23, 2024
04db9ed
clean up test file
CalMacCQ Aug 23, 2024
4b9d421
remove print
CalMacCQ Aug 23, 2024
efbeed7
remove redundant assignment
CalMacCQ Aug 23, 2024
1d5fd8c
typing improvements
CalMacCQ Aug 30, 2024
64a3ad8
set architecture in BackendInfo
CalMacCQ Aug 30, 2024
3dcdd81
test some architecture properties
CalMacCQ Aug 30, 2024
6f5cc47
minor simplification to test circuit
CalMacCQ Aug 30, 2024
b9e2b0d
update comment
CalMacCQ Sep 3, 2024
10078ab
use save_density_matrix method
CalMacCQ Sep 3, 2024
6d00b85
add test of the purity
CalMacCQ Sep 3, 2024
8ef8332
clean up test file
CalMacCQ Sep 3, 2024
01f1fdd
additonal purity check in density matrix test
CalMacCQ Sep 3, 2024
ff91d0a
adjust purity check to avoid rounding error
CalMacCQ Sep 4, 2024
6f1e99a
Merge branch 'main' into feat/Aer_density
CalMacCQ Sep 5, 2024
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: 5 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ API documentation
:inherited-members:
:members:

.. autoclass:: pytket.extensions.qiskit.AerDensityMatrixBackend
:special-members: __init__
:inherited-members:
:members:

.. automodule:: pytket.extensions.qiskit
:members: qiskit_to_tk, tk_to_qiskit, process_characterisation

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
Changelog
~~~~~~~~~

.. currentmodule:: pytket.extensions.qiskit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this deliberate? I'm unfamiliar with the syntax

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, basically it means that the refernces to classes and functions don't need to be fully qualified. The link is also generated

i.e.

:py:class:`IBMQBackend`

vs

:py:class:`~pytket.extensions.qiskit.IBMQBackend`



Unreleased
----------

* Added :py:class:`AerDensityMatrixBackend` simulator. This simulator has the option to support a :py:class:`NoiseModel`.
* Fix conversion of symbols into qiskit.
* Require qiskit >= 1.2.0.
* Add conversion of controlled unitary gates from qiskit to tket.
Expand Down
1 change: 1 addition & 0 deletions docs/intro.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Available IBM Backends
AerBackend
AerStateBackend
AerUnitaryBackend
AerDensityMatrixBackend


An example using the shots-based :py:class:`AerBackend` simulator is shown below.
Expand Down
1 change: 1 addition & 0 deletions pytket/extensions/qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AerBackend,
AerStateBackend,
AerUnitaryBackend,
AerDensityMatrixBackend,
IBMQEmulatorBackend,
)
from .backends.config import set_ibmq_config
Expand Down
8 changes: 7 additions & 1 deletion pytket/extensions/qiskit/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@
"""Backends for connecting to IBM devices and simulators directly from pytket"""

from .ibm import IBMQBackend, NoIBMQCredentialsError
from .aer import AerBackend, AerStateBackend, AerUnitaryBackend, qiskit_aer_backend
from .aer import (
AerBackend,
AerStateBackend,
AerUnitaryBackend,
AerDensityMatrixBackend,
qiskit_aer_backend,
)
from .ibmq_emulator import IBMQEmulatorBackend
123 changes: 99 additions & 24 deletions pytket/extensions/qiskit/backends/aer.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,16 @@ def process_circuits(
c0, ppcirc_rep = tkc, None

qc = tk_to_qiskit(c0, replace_implicit_swaps)

if self.supports_state:
qc.save_state()

elif self.supports_density_matrix:
qc.save_density_matrix()

elif self.supports_unitary:
qc.save_unitary()

qcs.append(qc)
tkc_qubits_count.append(c0.n_qubits)
ppcirc_strs.append(json.dumps(ppcirc_rep))
Expand Down Expand Up @@ -478,16 +484,16 @@ class AerBackend(_AerBaseBackend):
:param n_qubits: The maximum number of qubits supported by the backend.
"""

_persistent_handles = False
_supports_shots = True
_supports_counts = True
_supports_expectation = True
_expectation_allows_nonhermitian = False
_persistent_handles: bool = False
_supports_shots: bool = True
_supports_counts: bool = True
_supports_expectation: bool = True
_expectation_allows_nonhermitian: bool = False

_memory = True
_memory: bool = True

_qiskit_backend_name = "aer_simulator"
_allowed_special_gates = {
_qiskit_backend_name: str = "aer_simulator"
_allowed_special_gates: set[OpType] = {
OpType.Measure,
OpType.Barrier,
OpType.Reset,
Expand All @@ -504,9 +510,9 @@ def __init__(
super().__init__()
self._qiskit_backend = qiskit_aer_backend(self._qiskit_backend_name)
self._qiskit_backend.set_options(method=simulation_method)
gate_set = _tket_gate_set_from_qiskit_backend(self._qiskit_backend).union(
self._allowed_special_gates
)
gate_set: set[OpType] = _tket_gate_set_from_qiskit_backend(
self._qiskit_backend
).union(self._allowed_special_gates)

self._crosstalk_params = crosstalk_params
if self._crosstalk_params is not None:
Expand Down Expand Up @@ -575,15 +581,15 @@ class AerStateBackend(_AerBaseBackend):
:param n_qubits: The maximum number of qubits supported by the backend.
"""

_persistent_handles = False
_supports_state = True
_supports_expectation = True
_expectation_allows_nonhermitian = False
_persistent_handles: bool = False
_supports_state: bool = True
_supports_expectation: bool = True
_expectation_allows_nonhermitian: bool = False

_noise_model = None
_memory = False
_noise_model: Optional[NoiseModel] = None
_memory: bool = False

_qiskit_backend_name = "aer_simulator_statevector"
_qiskit_backend_name: str = "aer_simulator_statevector"

def __init__(
self,
Expand Down Expand Up @@ -613,14 +619,14 @@ class AerUnitaryBackend(_AerBaseBackend):
:param n_qubits: The maximum number of qubits supported by the backend.
"""

_persistent_handles = False
_supports_unitary = True
_persistent_handles: bool = False
_supports_unitary: bool = True

_memory = False
_noise_model = None
_needs_transpile = True
_memory: bool = False
_noise_model: Optional[NoiseModel] = None
_needs_transpile: bool = True

_qiskit_backend_name = "aer_simulator_unitary"
_qiskit_backend_name: str = "aer_simulator_unitary"

def __init__(self, n_qubits: int = 40) -> None:
super().__init__()
Expand All @@ -641,6 +647,75 @@ def __init__(self, n_qubits: int = 40) -> None:
]


class AerDensityMatrixBackend(_AerBaseBackend):
"""
Backend for running simulations on the Qiskit Aer density matrix simulator.

:param noise_model: Noise model to apply during simulation. Defaults to None.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be defined here or in the init? I'm not sure what our standard is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to me that we document this in the class docstring rather than the __init__ method. I don't have a strong prefernce either way.

:param n_qubits: The maximum number of qubits supported by the backend.
"""

_supports_density_matrix: bool = True
_supports_state: bool = False
_memory: bool = False
_noise_model: Optional[NoiseModel] = None
_needs_transpile: bool = True
_supports_expectation: bool = True

_qiskit_backend_name: str = "aer_simulator_density_matrix"

_allowed_special_gates: set[OpType] = {
OpType.Measure,
OpType.Barrier,
OpType.Reset,
OpType.RangePredicate,
}

def __init__(
self,
noise_model: Optional[NoiseModel] = None,
n_qubits: int = 40,
) -> None:
super().__init__()
self._qiskit_backend = qiskit_aer_backend(self._qiskit_backend_name)

gate_set: set[OpType] = _tket_gate_set_from_qiskit_backend(
self._qiskit_backend
).union(self._allowed_special_gates)
self._noise_model = _map_trivial_noise_model_to_none(noise_model)
characterisation: NoiseModelCharacterisation = (
_get_characterisation_of_noise_model(self._noise_model, gate_set)
)
self._has_arch: bool = bool(characterisation.architecture) and bool(
characterisation.architecture.nodes
)

self._backend_info = BackendInfo(
name=type(self).__name__,
device_name=self._qiskit_backend_name,
version=__extension_version__,
architecture=(
FullyConnected(n_qubits)
if not self._has_arch
else characterisation.architecture
),
gate_set=_tket_gate_set_from_qiskit_backend(self._qiskit_backend),
supports_midcircuit_measurement=True,
supports_reset=True,
supports_fast_feedforward=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely true for the density matrix simulator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so. I have a test case for this here

all_node_gate_errors=characterisation.node_errors,
all_edge_gate_errors=characterisation.edge_errors,
all_readout_errors=characterisation.readout_errors,
averaged_node_gate_errors=characterisation.averaged_node_errors,
averaged_edge_gate_errors=characterisation.averaged_edge_errors,
averaged_readout_errors=characterisation.averaged_readout_errors,
misc={"characterisation": characterisation.generic_q_errors},
)
self._required_predicates = [
GateSetPredicate(self._backend_info.gate_set),
]


def _process_noise_model(
noise_model: NoiseModel, gate_set: set[OpType]
) -> NoiseModelCharacterisation:
Expand Down
6 changes: 5 additions & 1 deletion pytket/extensions/qiskit/result_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def qiskit_experimentresult_to_backendresult(
for index in range(size):
q_bits.append(Qubit(name, index))

shots, counts, state, unitary = (None,) * 4
shots, counts, state, unitary, density_matrix = (None,) * 5
datadict = result.data.to_dict()
if _result_is_empty_shots(result):
n_bits = len(c_bits) if c_bits else 0
Expand All @@ -129,13 +129,17 @@ def qiskit_experimentresult_to_backendresult(
if "unitary" in datadict:
unitary = datadict["unitary"].reverse_qargs().data

if "density_matrix" in datadict:
density_matrix = datadict["density_matrix"].reverse_qargs().data

return BackendResult(
c_bits=c_bits,
q_bits=q_bits,
shots=shots,
counts=counts,
state=state,
unitary=unitary,
density_matrix=density_matrix,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BackendResult already has a density_matrix attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just passing it in as an arg when defining the BackendResult though right?

ppcirc=ppcirc,
)

Expand Down
70 changes: 69 additions & 1 deletion tests/backend_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
AerStateBackend,
AerUnitaryBackend,
IBMQEmulatorBackend,
AerDensityMatrixBackend,
)
from pytket.extensions.qiskit import (
qiskit_to_tk,
Expand All @@ -76,6 +77,7 @@
get_pauli_expectation_value,
get_operator_expectation_value,
)
from pytket.architecture import FullyConnected
from pytket.utils.operators import QubitPauliOperator
from pytket.utils.results import compare_statevectors, compare_unitaries

Expand Down Expand Up @@ -1346,7 +1348,7 @@ def test_statevector_simulator_gateset_deterministic() -> None:
circ.H(2)
circ.Measure(2, 0)
circ.CZ(0, 1, condition_bits=[0], condition_value=1)
circ.add_gate(OpType.Reset, [2])
circ.Reset(2)
compiled_circ = sv_backend.get_compiled_circuit(circ)
assert sv_backend.valid_circuit(compiled_circ)
tket_statevector = sv_backend.run_circuit(compiled_circ).get_state()
Expand Down Expand Up @@ -1430,3 +1432,69 @@ def test_ibmq_local_emulator(
counts = r.get_counts()
# Most results should be (0,0) or (1,1):
assert sum(c0 != c1 for c0, c1 in counts) < 25


# https://github.com/CQCL/pytket-qiskit/issues/231
def test_noiseless_density_matrix_simulation() -> None:
density_matrix_backend = AerDensityMatrixBackend()
assert density_matrix_backend.supports_density_matrix is True

assert isinstance(density_matrix_backend.backend_info.architecture, FullyConnected)

circ1 = Circuit(3).X(0).X(1).CCX(0, 1, 2)

output_state = np.array([0] * 7 + [1])

result1 = density_matrix_backend.run_circuit(circ1)
noiseless_dm1 = result1.get_density_matrix()

assert noiseless_dm1.shape == (8, 8)
assert np.allclose(noiseless_dm1, np.outer(output_state, output_state.conj()))
# Check purity to verify that we have a pure state
np.isclose(np.trace(noiseless_dm1**2).real, 1)

# Example with resets and conditional gates
# Prepares a state deterministically if input is a computational basis state
circ2 = Circuit(3, 1)
circ2.CCX(*range(3))
circ2.U1(1 / 4, 2)
circ2.H(2)
circ2.Measure(2, 0)
circ2.CZ(0, 1, condition_bits=[0], condition_value=1)
circ2.Reset(2)

result2 = density_matrix_backend.run_circuit(circ2)
assert result1.get_density_matrix().shape == (8, 8)
state_backend = AerStateBackend()
statevector = state_backend.run_circuit(circ2).get_state()
noiseless_dm2 = result2.get_density_matrix()
assert np.allclose(noiseless_dm2, np.outer(statevector, statevector.conj()))
# Check purity to verify that we have a pure state
assert np.isclose(np.trace(noiseless_dm2**2).real, 1)


# https://github.com/CQCL/pytket-qiskit/issues/231
def test_noisy_density_matrix_simulation() -> None:

# Test that __init__ works with a very simple noise model
noise_model = NoiseModel()
noise_model.add_quantum_error(depolarizing_error(0.6, 2), ["cz"], [0, 1])
noise_model.add_quantum_error(depolarizing_error(0.6, 2), ["cz"], [1, 2])

noisy_density_sim = AerDensityMatrixBackend(noise_model)
assert isinstance(noisy_density_sim.backend_info.architecture, Architecture)
assert len(noisy_density_sim.backend_info.architecture.nodes) == 3

circ = Circuit(3)
circ.X(0)
circ.X(1)
circ.SX(1)
circ.CZ(0, 1)
circ.CZ(1, 2)
assert noisy_density_sim.valid_circuit(circ)

result = noisy_density_sim.run_circuit(circ)
noisy_dm = result.get_density_matrix()
assert noisy_dm.shape == (8, 8)
# Check purity to verify mixed state
assert np.trace(noisy_dm**2).real < 0.99