Skip to content
Closed
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
122 changes: 34 additions & 88 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,19 @@
Base Experiment class.
"""

from abc import ABC, abstractmethod
from typing import Iterable, Optional, Tuple, List, Dict
import copy
from abc import ABC, abstractmethod
from numbers import Integral
from typing import Iterable, Optional, Tuple, List, Dict

from qiskit import transpile, assemble, QuantumCircuit
from qiskit.providers import BaseJob
from qiskit.providers.backend import Backend
from qiskit.providers.basebackend import BaseBackend as LegacyBackend
from qiskit.test.mock import FakeBackend
from qiskit import QuantumCircuit
from qiskit.exceptions import QiskitError
from qiskit.providers.backend import Backend
from qiskit.qobj.utils import MeasLevel

from qiskit_experiments.framework import Options
from qiskit_experiments.framework.experiment_data import ExperimentData
from .events import ExperimentRunner


class BaseExperiment(ABC):
Expand All @@ -47,6 +46,18 @@ class BaseExperiment(ABC):
# ExperimentData class for experiment
__experiment_data__ = ExperimentData

# Execute hooks
__execute_events__ = ["backend_run"]

# Transpile hooks
__transpile_events__ = ["transpile_circuits"]

# Pre-processing hooks
__pre_processing_events__ = ["initialize_experiment_data", "update_run_options"]

# Post-processing hooks
__post_processing_events__ = ["add_job_metadata", "set_analysis"]

def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None):
"""Initialize the experiment object.

Expand Down Expand Up @@ -101,71 +112,27 @@ def run(
QiskitError: if experiment is run with an incompatible existing
ExperimentData container.
"""
# Create experiment data container
experiment_data = self._initialize_experiment_data(backend, experiment_data)

# Run options
run_opts = copy.copy(self.run_options)
run_opts.update_options(**run_options)
run_opts = run_opts.__dict__

# Scheduling parameters
if backend.configuration().simulator is False and isinstance(backend, FakeBackend) is False:
timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {})
timing_constraints["acquire_alignment"] = getattr(
timing_constraints, "acquire_alignment", 16
)
scheduling_method = getattr(
self.transpile_options.__dict__, "scheduling_method", "alap"
)
self.set_transpile_options(
timing_constraints=timing_constraints, scheduling_method=scheduling_method
)

# Generate and transpile circuits
transpile_opts = copy.copy(self.transpile_options.__dict__)
transpile_opts["initial_layout"] = list(self._physical_qubits)
circuits = transpile(self.circuits(backend), backend, **transpile_opts)
self._postprocess_transpiled_circuits(circuits, backend, **run_options)
runner = ExperimentRunner(self)

if isinstance(backend, LegacyBackend):
qobj = assemble(circuits, backend=backend, **run_opts)
job = backend.run(qobj)
else:
job = backend.run(circuits, **run_opts)

# Add Job to ExperimentData and add analysis for post processing.
run_analysis = None

# Add experiment option metadata
self._add_job_metadata(experiment_data, job, **run_opts)

if analysis and self.__analysis_class__ is not None:
run_analysis = self.run_analysis
# add pre-processing events
for handler in self.__pre_processing_events__:
runner.add_handler(handler, module="events_preprocessing")

experiment_data.add_data(job, post_processing_callback=run_analysis)
# add transpiler events
for handler in self.__transpile_events__:
runner.add_handler(handler, module="events_transpiler")

# Return the ExperimentData future
return experiment_data
# add execution events
for handler in self.__execute_events__:
runner.add_handler(handler, module="events_execute")

def _initialize_experiment_data(
self, backend: Backend, experiment_data: Optional[ExperimentData] = None
) -> ExperimentData:
"""Initialize the return data container for the experiment run"""
if experiment_data is None:
return self.__experiment_data__(experiment=self, backend=backend)

# Validate experiment is compatible with existing data
if not isinstance(experiment_data, ExperimentData):
raise QiskitError("Input `experiment_data` is not a valid ExperimentData.")
if experiment_data.experiment_type != self._type:
raise QiskitError("Existing ExperimentData contains data from a different experiment.")
if experiment_data.metadata.get("physical_qubits") != list(self.physical_qubits):
raise QiskitError(
"Existing ExperimentData contains data for a different set of physical qubits."
)
# add post-processing events
for handler in self.__post_processing_events__:
runner.add_handler(handler, module="events_postprocess")

return experiment_data._copy_metadata()
return runner.run(
backend=backend, analysis=analysis, experiment_data=experiment_data, **run_options
)

def run_analysis(self, experiment_data, **options) -> ExperimentData:
"""Run analysis and update ExperimentData with analysis result.
Expand Down Expand Up @@ -335,10 +302,6 @@ def set_analysis_options(self, **fields):
"""
self._analysis_options.update_options(**fields)

def _postprocess_transpiled_circuits(self, circuits, backend, **run_options):
"""Additional post-processing of transpiled circuits before running on backend"""
pass

def _metadata(self) -> Dict[str, any]:
"""Return experiment metadata for ExperimentData.

Expand All @@ -364,20 +327,3 @@ def _additional_metadata(self) -> Dict[str, any]:
additional experiment metadata in ExperimentData.
"""
return {}

def _add_job_metadata(self, experiment_data: ExperimentData, job: BaseJob, **run_options):
"""Add runtime job metadata to ExperimentData.

Args:
experiment_data: the experiment data container.
job: the job object.
run_options: backend run options for the job.
"""
metadata = {
"job_id": job.job_id(),
"experiment_options": copy.copy(self.experiment_options.__dict__),
"transpile_options": copy.copy(self.transpile_options.__dict__),
"analysis_options": copy.copy(self.analysis_options.__dict__),
"run_options": copy.copy(run_options),
}
experiment_data._metadata["job_metadata"].append(metadata)
17 changes: 17 additions & 0 deletions qiskit_experiments/framework/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Event hooks to run experiment.
"""

from .runner import ExperimentRunner
29 changes: 29 additions & 0 deletions qiskit_experiments/framework/events/events_execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Execute events.
"""

from qiskit import assemble
from qiskit.providers.basebackend import BaseBackend as LegacyBackend


def backend_run(experiment, backend, circuits, run_options, **kwargs):

if isinstance(backend, LegacyBackend):
qobj = assemble(circuits, backend=backend, **run_options)
job = backend.run(qobj)
else:
job = backend.run(circuits, **run_options)

return {"job": job}
47 changes: 47 additions & 0 deletions qiskit_experiments/framework/events/events_postprocess.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Post processing events.
"""

import copy


def set_analysis(experiment, analysis, job, experiment_data, **kwargs):

if analysis and experiment.__analysis_class__ is not None:
run_analysis_callback = experiment.run_analysis
else:
run_analysis_callback = None

experiment_data.add_data(job, post_processing_callback=run_analysis_callback)


def add_job_metadata(experiment, experiment_data, job, run_options, **kwargs):
"""Add runtime job metadata to ExperimentData.

Args:
experiment: Experiment object.
experiment_data: The experiment data container.
job: The job object.
run_options: Backend run options for the job.
"""
metadata = {
"job_id": job.job_id(),
"experiment_options": copy.copy(experiment.experiment_options.__dict__),
"transpile_options": copy.copy(experiment.transpile_options.__dict__),
"analysis_options": copy.copy(experiment.analysis_options.__dict__),
"run_options": copy.copy(run_options)
}

experiment_data._metadata["job_metadata"].append(metadata)
44 changes: 44 additions & 0 deletions qiskit_experiments/framework/events/events_preprocessing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Pre processing events.
"""
import copy

from qiskit_experiments.framework import ExperimentData
from qiskit.exceptions import QiskitError


def initialize_experiment_data(experiment, backend, expeirment_data, **kwargs):
if expeirment_data is not None:
init_data = experiment.__experiment_data__(experiment=experiment, backend=backend)
else:
# Validate experiment is compatible with existing data
if not isinstance(expeirment_data, ExperimentData):
raise QiskitError("Input `experiment_data` is not a valid ExperimentData.")
if expeirment_data.experiment_type != experiment.experiment_type:
raise QiskitError("Existing ExperimentData contains data from a different experiment.")
if expeirment_data.metadata.get("physical_qubits") != list(experiment.physical_qubits):
raise QiskitError(
"Existing ExperimentData contains data for a different set of physical qubits."
)
init_data = expeirment_data._copy_metadata()

return {"expeirment_data": init_data}


def update_run_options(experiment, run_options, **kwargs):
options = copy.copy(experiment.run_options).__dict__
options.update(**run_options)

return {"run_options": options}
82 changes: 82 additions & 0 deletions qiskit_experiments/framework/events/events_transpiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Transpile events.
"""

import copy

from qiskit import transpile
from qiskit.test.mock import FakeBackend

from qiskit_experiments.framework import ParallelExperiment


def count_transpiled_ops(experiment, circuits, **kwargs):

def get_metadata(circuit):
if circuit.metadata["experiment_type"] == experiment.experiment_type:
return circuit.metadata
if circuit.metadata["experiment_type"] == ParallelExperiment.__name__:
for meta in circuit.metadata["composite_metadata"]:
if meta["physical_qubits"] == experiment.physical_qubits:
return meta
return dict()

for circ in circuits:
meta = get_metadata(circ)
if meta:
qubits = range(len(circ.qubits))
c_count_ops = {}
for instr, qargs, _ in circ:
instr_qubits = []
skip_instr = False
for qubit in qargs:
qubit_index = circ.qubits.index(qubit)
if qubit_index not in qubits:
skip_instr = True
instr_qubits.append(qubit_index)
if not skip_instr:
instr_qubits = tuple(instr_qubits)
c_count_ops[(instr_qubits, instr.name)] = (
c_count_ops.get((instr_qubits, instr.name), 0) + 1
)
circuit_length = meta["xval"]
count_ops = [(key, (value, circuit_length)) for key, value in c_count_ops.items()]
meta.update({"count_ops": count_ops})


def set_scheduling_contraints(expeirment, backend, **kwargs):
if not backend.configuration().simulator and not isinstance(backend, FakeBackend):
timing_constraints = getattr(
expeirment.transpile_options.__dict__,
"timing_constraints",
{}
)
timing_constraints["acquire_alignment"] = getattr(
timing_constraints, "acquire_alignment", 16
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 16 hard-coded here? This should be configurable, e.g. in an option.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, agreed. I just copied it from the original code.

)
scheduling_method = getattr(
expeirment.transpile_options.__dict__, "scheduling_method", "alap"
)
expeirment.set_transpile_options(
timing_constraints=timing_constraints, scheduling_method=scheduling_method
)


def transpile_circuits(experiment, backend, **kwargs):
options = copy.copy(experiment.transpile_options.__dict__)
options["initial_layout"] = list(experiment.physical_qubits)
circuits = transpile(experiment.circuits(backend), backend, **options)

return {"circuits": circuits}
Loading