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 12 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
16 changes: 13 additions & 3 deletions src/mqt/ddsim/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

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

from qiskit.providers import JobError, JobStatus, JobV1

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


Expand Down Expand Up @@ -42,11 +43,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[Sequence[float]] | Sequence[Mapping[Parameter, float]] | 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 +68,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
63 changes: 57 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

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

if TYPE_CHECKING:
from qiskit.circuit import Parameter


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

def run(self, quantum_circuits: QuantumCircuit | list[QuantumCircuit], **options: dict[str, Any]) -> DDSIMJob:
@staticmethod
def _bind_parameters(
quantum_circuits: Sequence[QuantumCircuit],
parameter_values: Sequence[Sequence[float]] | Sequence[Mapping[Parameter, float]] | None,
) -> list[QuantumCircuit]:
if parameter_values is None:
parameter_values = []
burgholzer marked this conversation as resolved.
Show resolved Hide resolved

number_parametrized_circuits = 0 # Initialize the counter

for qc in quantum_circuits:
if qc.parameters:
number_parametrized_circuits += 1
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved

if number_parametrized_circuits != 0 or len(parameter_values) != 0:
if number_parametrized_circuits == 0:
msg = f"No parametrized circuits found, but {len(parameter_values)} parameters provided. The parameter list should either be empty or None."
raise ValueError(msg)
if len(parameter_values) != len(quantum_circuits):
msg = f"The number of circuits to simulate ({len(quantum_circuits)}) does not match the size of the parameter list ({len{parameter_values)})."
Fixed Show fixed Hide fixed
raise ValueError(msg)
bound_circuits = []
for qc, values in zip(quantum_circuits, parameter_values):
if len(qc.parameters) != len(values):
msg = f"The number of parameters in the circuit '{qc.name}' does not match the number of parameters provided ({len(values)}). Expected number of parameters is '{len(qc.parameters)}'."
raise ValueError(msg)
qc_bound = qc.bind_parameters(values)
qc_bound.name = qc.name # Preserves circuits' names
bound_circuits.append(qc_bound)

return bound_circuits

return list(quantum_circuits)

def run(
self,
quantum_circuits: QuantumCircuit | Sequence[QuantumCircuit],
parameter_values: Sequence[Sequence[float]] | Sequence[Mapping[Parameter, float]] | 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[Sequence[float]] | Sequence[Mapping[Parameter, float]] | 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._bind_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
40 changes: 40 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,45 @@ 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(2)
circuit_2 = QuantumCircuit(2)
circuit_1.ry(theta_a, 0)
circuit_1.rx(theta_b, 1)
circuit_2.rx(theta_c, 0)

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

threshold = 0.04 * shots
average = shots / 4
counts_1 = result.get_counts(circuit_1.name)
counts_2 = result.get_counts(circuit_2.name)
target_1 = {
"0": average,
"11": average,
"1": average,
}
target_2 = {
"0": shots * (np.cos(np.pi / 8)) ** 2,
"1": shots * (np.sin(np.pi / 8)) ** 2,
}

for key in target_1:
assert key in counts_1
assert abs(target_1[key] - counts_1[key]) < threshold

for key in target_2:
assert key in counts_2
assert abs(target_2[key] - counts_2[key]) < threshold


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