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

Improved support of parametrized gates in DDSIM Backends #293

Merged
merged 16 commits into from
Sep 28, 2023
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
19 changes: 16 additions & 3 deletions src/mqt/ddsim/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import functools
from concurrent import futures
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence, Union

from qiskit.providers import JobError, JobStatus, JobV1

if TYPE_CHECKING:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit.providers import BackendV2

Parameters = Union[Mapping[Parameter, ParameterValueType], Sequence[ParameterValueType]]


def requires_submit(func):
"""Decorator to ensure that a submit has been performed before
Expand Down Expand Up @@ -42,11 +46,18 @@ class DDSIMJob(JobV1):
_executor = futures.ThreadPoolExecutor(max_workers=1)

def __init__(
self, backend: BackendV2, job_id: str, fn: Callable, experiments: list[QuantumCircuit], **args: dict[str, Any]
self,
backend: BackendV2,
job_id: str,
fn: Callable,
experiments: Sequence[QuantumCircuit],
parameter_values: Sequence[Parameters] | None,
**args: dict[str, Any],
) -> None:
super().__init__(backend, job_id)
self._fn = fn
self._experiments = experiments
self._parameter_values = parameter_values
self._args = args
self._future: futures.Future | None = None

Expand All @@ -60,7 +71,9 @@ def submit(self) -> None:
msg = "Job was already submitted!"
raise JobError(msg)

self._future = self._executor.submit(self._fn, self._job_id, self._experiments, **self._args)
self._future = self._executor.submit(
self._fn, self._job_id, self._experiments, self._parameter_values, **self._args
)

@requires_submit
def result(self, timeout: float | None = None):
Expand Down
58 changes: 52 additions & 6 deletions src/mqt/ddsim/qasmsimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time
import uuid
from math import log2
from typing import Any
from typing import TYPE_CHECKING, Any, Mapping, Sequence, Union

from qiskit import QuantumCircuit
from qiskit.providers import BackendV2, Options
Expand All @@ -20,6 +20,12 @@
from .pyddsim import CircuitSimulator
from .target import DDSIMTargetBuilder

if TYPE_CHECKING:
from qiskit.circuit import Parameter
from qiskit.circuit.parameterexpression import ParameterValueType

Parameters = Union[Mapping[Parameter, ParameterValueType], Sequence[ParameterValueType]]


class QasmSimulatorBackend(BackendV2):
"""Python interface to MQT DDSIM."""
Expand Down Expand Up @@ -73,22 +79,62 @@ def target(self):
def max_circuits(self):
return None

def run(self, quantum_circuits: QuantumCircuit | list[QuantumCircuit], **options: dict[str, Any]) -> DDSIMJob:
@staticmethod
def _assign_parameters(
quantum_circuits: Sequence[QuantumCircuit],
parameter_values: Sequence[Parameters] | None,
) -> list[QuantumCircuit]:
if not any(qc.parameters for qc in quantum_circuits) and not parameter_values:
return list(quantum_circuits)

if parameter_values is None:
msg = "No parameter values provided although at least one parameterized circuit was supplied."
raise ValueError(msg)

if len(quantum_circuits) != len(parameter_values):
msg = f"The number of circuits ({len(quantum_circuits)}) does not match the number of provided parameter sets ({len(parameter_values)})."
raise ValueError(msg)

bound_circuits = [
qc.assign_parameters(parameters=values) for qc, values in zip(quantum_circuits, parameter_values)
]

# fix the circuit names
for qcb, qc in zip(bound_circuits, quantum_circuits):
qcb.name = qc.name

return bound_circuits

def run(
self,
quantum_circuits: QuantumCircuit | Sequence[QuantumCircuit],
parameter_values: Sequence[Parameters] | None = None,
**options,
) -> DDSIMJob:
if isinstance(quantum_circuits, QuantumCircuit):
quantum_circuits = [quantum_circuits]

job_id = str(uuid.uuid4())
local_job = DDSIMJob(self, job_id, self._run_job, quantum_circuits, **options)
local_job = DDSIMJob(self, job_id, self._run_job, quantum_circuits, parameter_values, **options)
local_job.submit()
return local_job

def _validate(self, quantum_circuits: list[QuantumCircuit]) -> None:
def _validate(self, quantum_circuits: Sequence[QuantumCircuit]) -> None:
pass

def _run_job(self, job_id: int, quantum_circuits: list[QuantumCircuit], **options: dict[str, Any]) -> Result:
def _run_job(
self,
job_id: int,
quantum_circuits: Sequence[QuantumCircuit],
parameter_values: Sequence[Parameters] | None,
**options: dict[str, Any],
) -> Result:
self._validate(quantum_circuits)
start = time.time()
result_list = [self._run_experiment(q_circ, **options) for q_circ in quantum_circuits]

bound_circuits = self._assign_parameters(quantum_circuits, parameter_values)
result_list = [self._run_experiment(q_circ, **options) for q_circ in bound_circuits]

end = time.time()

return Result(
Expand Down
12 changes: 6 additions & 6 deletions src/mqt/ddsim/unitarysimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

import time
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Sequence

import numpy as np
import numpy.typing as npt
Expand All @@ -11,10 +11,10 @@
from qiskit.result.models import ExperimentResult, ExperimentResultData
from qiskit.transpiler import Target

from mqt.ddsim.header import DDSIMHeader
from mqt.ddsim.pyddsim import ConstructionMode, UnitarySimulator, get_matrix
from mqt.ddsim.qasmsimulator import QasmSimulatorBackend
from mqt.ddsim.target import DDSIMTargetBuilder
from .header import DDSIMHeader
from .pyddsim import ConstructionMode, UnitarySimulator, get_matrix
from .qasmsimulator import QasmSimulatorBackend
from .target import DDSIMTargetBuilder

if TYPE_CHECKING:
from qiskit import QuantumCircuit
Expand Down Expand Up @@ -93,7 +93,7 @@ def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
header=DDSIMHeader(qc),
)

def _validate(self, quantum_circuits: list[QuantumCircuit]):
def _validate(self, quantum_circuits: Sequence[QuantumCircuit]):
"""Semantic validations of the quantum circuits which cannot be done via schemas.
Some of these may later move to backend schemas.
1. No shots
Expand Down
44 changes: 44 additions & 0 deletions test/python/simulator/test_qasm_simulator.py
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import numpy as np
import pytest
from qiskit import AncillaRegister, ClassicalRegister, QuantumCircuit, QuantumRegister, execute
from qiskit.circuit import Parameter

from mqt.ddsim.qasmsimulator import QasmSimulatorBackend

Expand Down Expand Up @@ -69,6 +70,49 @@ def test_qasm_simulator(circuit: QuantumCircuit, backend: QasmSimulatorBackend,
assert abs(target[key] - counts[key]) < threshold


def test_qasm_simulator_support_parametrized_gates(backend: QasmSimulatorBackend, shots: int):
"""Test backend's adequate support of parametrized gates"""

theta_a = Parameter("theta_a")
theta_b = Parameter("theta_b")
theta_c = Parameter("theta_c")
circuit_1 = QuantumCircuit(1)
circuit_2 = QuantumCircuit(1)
circuit_1.rx(theta_a, 0)
circuit_2.rx(theta_b, 0)
circuit_2.rz(theta_c, 0)
circuit_2.rx(theta_b, 0)
bare_circuit = QuantumCircuit(1)
bare_circuit.h(0)

with pytest.raises(
ValueError,
match=r"Mismatching number of values and parameters.*",
):
backend.run([bare_circuit], [[np.pi]], shots=shots).result()

with pytest.raises(
ValueError, match=r"No parameter values provided although at least one parameterized circuit was supplied."
):
backend.run([circuit_1, circuit_2], shots=shots).result()

with pytest.raises(
ValueError,
match=r"The number of circuits \(2\) does not match the number of provided parameter sets \(1\)\.",
):
backend.run([circuit_1, circuit_2], [[np.pi / 2]], shots=shots).result()

# Test backend's correct functionality with multiple circuit
result = backend.run([circuit_1, circuit_2], [[np.pi], [np.pi / 2, np.pi]], shots=shots).result()
assert result.success

counts_1 = result.get_counts(circuit_1)
counts_2 = result.get_counts(circuit_2)

assert counts_1 == {"1": shots}
assert counts_2 == {"0": shots}


def test_qasm_simulator_approximation(backend: QasmSimulatorBackend, shots: int):
"""Test data counts output for single circuit run against reference."""
circuit = QuantumCircuit(2)
Expand Down