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 7 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
8 changes: 5 additions & 3 deletions src/mqt/ddsim/hybridqasmsimulator.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

if TYPE_CHECKING:
from qiskit import QuantumCircuit
Expand Down Expand Up @@ -51,7 +51,7 @@ def _default_options(cls) -> Options:
def target(self):
return self._HSF_TARGET

def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
def _run_experiment(self, qc: QuantumCircuit, values: Sequence[float] | None = None, **options) -> ExperimentResult:
start_time = time.time()
seed = options.get("seed", -1)
mode = options.get("mode", "amplitude")
Expand All @@ -71,7 +71,9 @@ def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
msg = f"Simulation mode{mode} not supported by hybrid simulator. Available modes are 'amplitude' and 'dd'."
raise QiskitError(msg)

sim = HybridCircuitSimulator(qc, seed=seed, mode=hybrid_mode, nthreads=nthreads)
bound_qc = self._bind_parameters(qc, values)
self._simulated_circuits.append(bound_qc)
sim = HybridCircuitSimulator(bound_qc, seed=seed, mode=hybrid_mode, nthreads=nthreads)

shots = options.get("shots", 1024)
if self._SHOW_STATE_VECTOR and shots > 0:
Expand Down
15 changes: 12 additions & 3 deletions src/mqt/ddsim/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

from qiskit.providers import JobError, JobStatus, JobV1

Expand Down Expand Up @@ -42,11 +42,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: list[QuantumCircuit],
parameter_values: Sequence[Sequence[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 +67,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
8 changes: 5 additions & 3 deletions src/mqt/ddsim/pathqasmsimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

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

if TYPE_CHECKING:
from quimb.tensor import Tensor, TensorNetwork
Expand Down Expand Up @@ -147,7 +147,7 @@ def _default_options(cls) -> Options:
def target(self):
return self._PATH_TARGET

def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
def _run_experiment(self, qc: QuantumCircuit, values: Sequence[float] | None = None, **options) -> ExperimentResult:
start_time = time.time()

pathsim_configuration = options.get("pathsim_configuration", PathSimulatorConfiguration())
Expand All @@ -172,7 +172,9 @@ def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
if seed is not None:
pathsim_configuration.seed = seed

sim = PathCircuitSimulator(qc, config=pathsim_configuration)
bound_qc = self._bind_parameters(qc, values)
self._simulated_circuits.append(bound_qc)
sim = PathCircuitSimulator(bound_qc, config=pathsim_configuration)

# determine the contraction path using cotengra in case this is requested
if pathsim_configuration.mode == PathSimulatorMode.cotengra:
Expand Down
64 changes: 52 additions & 12 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 Any, Sequence

from qiskit import QuantumCircuit
from qiskit.providers import BackendV2, Options
Expand All @@ -14,11 +14,11 @@
from qiskit.transpiler import Target
from qiskit.utils.multiprocessing import local_hardware_info

from . import __version__
from .header import DDSIMHeader
from .job import DDSIMJob
from .pyddsim import CircuitSimulator
from .target import DDSIMTargetBuilder
from mqt.ddsim import __version__
from mqt.ddsim.header import DDSIMHeader
from mqt.ddsim.job import DDSIMJob
from mqt.ddsim.pyddsim import CircuitSimulator
from mqt.ddsim.target import DDSIMTargetBuilder
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved


class QasmSimulatorBackend(BackendV2):
Expand Down Expand Up @@ -52,6 +52,7 @@ def _initialize_target(self) -> None:

def __init__(self, name="qasm_simulator", description="MQT DDSIM QASM Simulator") -> None:
super().__init__(name=name, description=description, backend_version=__version__)
self._simulated_circuits: list[QuantumCircuit] = []
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved
self._initialize_target()

@classmethod
Expand All @@ -73,22 +74,56 @@ def target(self):
def max_circuits(self):
return None

def run(self, quantum_circuits: QuantumCircuit | list[QuantumCircuit], **options: dict[str, Any]) -> DDSIMJob:
@staticmethod
def _bind_parameters(qc: QuantumCircuit, values: Sequence[float] | None) -> QuantumCircuit:
if values is None:
values = []

if len(qc.parameters) != len(values):
msg = "The number of parameters in the circuit does not match the number of parameters provided."
raise AssertionError(msg)
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved

return qc.bind_parameters(dict(zip(qc.parameters, values))) if values else qc

def run(
self,
quantum_circuits: QuantumCircuit | list[QuantumCircuit],
parameter_values: Sequence[Sequence[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:
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: list[QuantumCircuit],
parameter_values: Sequence[Sequence[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]

if not parameter_values:
result_list = [self._run_experiment(q_circ, values=None, **options) for q_circ in quantum_circuits]
else:
if len(quantum_circuits) != len(parameter_values):
msg = "The number of circuits to simulate does not match the size of the parameter list."
raise AssertionError(msg)
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved

result_list = [
self._run_experiment(q_circ, values, **options)
for q_circ, values in zip(quantum_circuits, parameter_values)
]
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved

end = time.time()

return Result(
Expand All @@ -102,16 +137,21 @@ def _run_job(self, job_id: int, quantum_circuits: list[QuantumCircuit], **option
time_taken=end - start,
)

def _run_experiment(self, qc: QuantumCircuit, **options: dict[str, Any]) -> ExperimentResult:
def _run_experiment(
self, qc: QuantumCircuit, values: Sequence[float] | None = None, **options: dict[str, Any]
) -> ExperimentResult:
start_time = time.time()
approximation_step_fidelity = options.get("approximation_step_fidelity", 1.0)
approximation_steps = options.get("approximation_steps", 1)
approximation_strategy = options.get("approximation_strategy", "fidelity")
seed = options.get("seed_simulator", -1)
shots = options.get("shots", 1024)

bound_qc = self._bind_parameters(qc, values)
self._simulated_circuits.append(bound_qc)

sim = CircuitSimulator(
qc,
bound_qc,
approximation_step_fidelity=approximation_step_fidelity,
approximation_steps=approximation_steps,
approximation_strategy=approximation_strategy,
Expand Down
8 changes: 5 additions & 3 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 Down Expand Up @@ -48,7 +48,7 @@ def _default_options(cls):
def target(self):
return self._US_TARGET

def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
def _run_experiment(self, qc: QuantumCircuit, values: Sequence[float] | None = None, **options) -> ExperimentResult:
start_time = time.time()
seed = options.get("seed", -1)
mode = options.get("mode", "recursive")
Expand All @@ -64,7 +64,9 @@ def _run_experiment(self, qc: QuantumCircuit, **options) -> ExperimentResult:
)
raise QiskitError(msg)

sim = UnitarySimulator(qc, seed=seed, mode=construction_mode)
bound_qc = self._bind_parameters(qc, values)
self._simulated_circuits.append(bound_qc)
sim = UnitarySimulator(bound_qc, seed=seed, mode=construction_mode)
sim.construct()
# Extract resulting matrix from final DD and write data
unitary: npt.NDArray[np.complex_] = np.zeros((2**qc.num_qubits, 2**qc.num_qubits), dtype=np.complex_)
Expand Down
54 changes: 54 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,59 @@ 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 raises the right type of errors
with pytest.raises(AssertionError) as exc_info:
backend.run([circuit_1, circuit_2], [[np.pi / 2, np.pi / 2]], shots=shots).result()
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved

assert str(exc_info.value) == "The number of circuits to simulate does not match the size of the parameter list."

with pytest.raises(AssertionError) as exc_info:
backend.run([circuit_1], [[np.pi / 2]], shots=shots).result()

assert (
str(exc_info.value)
== "The number of parameters in the circuit does not match the number of parameters provided."
)
burgholzer marked this conversation as resolved.
Show resolved Hide resolved

# 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)
counts_2 = result.get_counts(circuit_2)
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
andresbar98 marked this conversation as resolved.
Show resolved Hide resolved


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