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
2 changes: 1 addition & 1 deletion docs/_ext/custom_styles/example/example_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,5 @@ def __init__(self, qubit: int):
"""
super().__init__(qubits=[qubit])

def circuits(self, backend=None):
def _circuits(self, backend=None):
pass
141 changes: 111 additions & 30 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
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.exceptions import QiskitError
from qiskit.qobj.utils import MeasLevel
from qiskit_experiments.framework import Options
Expand Down Expand Up @@ -96,10 +95,6 @@ def run(

Returns:
The experiment data object.

Raises:
QiskitError: if experiment is run with an incompatible existing
ExperimentData container.
"""
# Create experiment data container
experiment_data = self._initialize_experiment_data(backend, experiment_data)
Expand All @@ -109,24 +104,8 @@ def run(
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)
circuits = self.circuits(backend, run_transpile=True)

# Run experiment jobs
max_experiments = getattr(backend.configuration(), "max_experiments", None)
Expand Down Expand Up @@ -158,6 +137,7 @@ def run(
# Optionally run analysis
if analysis and self.__analysis_class__:
experiment_data.add_analysis_callback(self.run_analysis)
experiment_data.add_analysis_callback(self._post_analysis_action)

# Return the ExperimentData future
return experiment_data
Expand All @@ -181,6 +161,106 @@ def _initialize_experiment_data(

return experiment_data._copy_metadata()

def _pre_transpile_action(self, backend: Backend):
"""An extra subroutine executed before transpilation.

Note:
This method may be implemented by a subclass that requires to update the
transpiler configuration based on the given backend instance,
otherwise the transpiler configuration should be updated with the
:py:meth:`_default_transpile_options` method.

For example, some specific transpiler options might change depending on the real
hardware execution or circuit simulator execution.
By default, this method does nothing.

Args:
backend: Target backend.
"""
pass

# pylint: disable = unused-argument
def _post_transpile_action(
self, circuits: List[QuantumCircuit], backend: Backend
) -> List[QuantumCircuit]:
"""An extra subroutine executed after transpilation.

Note:
This method may be implemented by a subclass that requires to update the
circuit or its metadata after transpilation.
Without this method, the transpiled circuit will be immediately executed on the backend.
This method enables the experiment to modify the circuit with pulse gates,
or some extra metadata regarding the transpiled sequence of instructions.

By default, this method just passes transpiled circuits to the execution chain.

Args:
circuits: List of transpiled circuits.
backend: Target backend.

Returns:
List of circuits to execute.
"""
return circuits

def circuits(
self, backend: Optional[Backend] = None, run_transpile: bool = False,
) -> List[QuantumCircuit]:
"""Run transpile and return transpiled circuits.

Args:
backend: Target backend.
run_transpile: Set ``True`` to run transpile, otherwise this returns logical circuits.
The logical circuits are convenient to grasp overview of the experiment,
while transpiled circuits can be used to scrutinize the program actually
executed on the backend. This defaults to ``False``.

Returns:
Experiment circuits.

Raises:
QiskitError: When transpile is performed without backend instance.
"""
logical_circs = self._circuits(backend)

if not run_transpile:
return logical_circs

# Run pre transpile if implemented by subclasses.
self._pre_transpile_action(backend)

# Get transpile options
circuits = transpile(
circuits=logical_circs, backend=backend, **self.transpile_options.__dict__
)

# Run post transpile. This is implemented by each experiment subclass.
circuits = self._post_transpile_action(circuits, backend)

return circuits

def _post_analysis_action(self, experiment_data: ExperimentData):
"""An extra subroutine executed after analysis.

Note:
This method may be implemented by a subclass that requires to perform
extra data processing based on the analyzed experimental result.

Note that the analysis routine will not complete until the backend job
is executed, and this method will be called after the analysis routine
is completed though a handler of the experiment result will be immediately
returned to users (a future object). This method is automatically triggered
when the analysis is finished, and will be processed in background.

If this method updates some other (mutable) objects, you may need manage
synchronization of update of the object data.
By default, this method does nothing.

Args:
experiment_data: A future object of the experimental result.
"""
pass

def run_analysis(self, experiment_data: ExperimentData, **options) -> ExperimentData:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this method should be the one that calls _post_analysis_action, not the Analysis.run method. Since if this may have side effects on this class this should be clear form this class.

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, that is what I originally implemented and people suggested me to exclude the post action from this method. However, this requires composite experiment run method to have boilerplate code because they need to call post analysis of each nested experiment. I think we can merge #407 first and revert the change based on #407.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I noticed above induces side effect in this situation 0c8b915

What happens if we call add_analysis_callback from run_analysis? Or should we just call post analysis without adding it to the experiment data callbacks?

"""Run analysis and update ExperimentData with analysis result.

Expand All @@ -194,7 +274,7 @@ def run_analysis(self, experiment_data: ExperimentData, **options) -> Experiment
An experiment data object containing the analysis results and figures.

Raises:
QiskitError: if experiment_data container is not valid for analysis.
QiskitError: Method is called with an empty experiment result.
"""
# Get analysis options
analysis_options = copy.copy(self.analysis_options)
Expand Down Expand Up @@ -230,8 +310,8 @@ def analysis(cls):
return cls.__analysis_class__()

@abstractmethod
def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Return a list of experiment circuits.
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Return a list of logical circuits with logical qubit index.

Args:
backend: Optional, a backend object.
Expand Down Expand Up @@ -349,10 +429,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 @@ -379,7 +455,12 @@ def _additional_metadata(self) -> Dict[str, any]:
"""
return {}

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

Args:
Expand Down
4 changes: 2 additions & 2 deletions qiskit_experiments/framework/composite/batch_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, experiments):
qubits = tuple(self._qubit_map.keys())
super().__init__(experiments, qubits)

def circuits(self, backend=None):
def _circuits(self, backend=None):

batch_circuits = []

Expand All @@ -51,7 +51,7 @@ def circuits(self, backend=None):
qubit_mapping = None
else:
qubit_mapping = [self._qubit_map[qubit] for qubit in expr.physical_qubits]
for circuit in expr.circuits(backend):
for circuit in expr._circuits(backend):
# Update metadata
circuit.metadata = {
"experiment_type": self._type,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, experiments, qubits, experiment_type=None):
super().__init__(qubits, experiment_type=experiment_type)

@abstractmethod
def circuits(self, backend=None):
def _circuits(self, backend=None):
pass

@property
Expand Down
4 changes: 2 additions & 2 deletions qiskit_experiments/framework/composite/parallel_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, experiments):
qubits += exp.physical_qubits
super().__init__(experiments, qubits)

def circuits(self, backend=None):
def _circuits(self, backend=None):

sub_circuits = []
sub_qubits = []
Expand All @@ -42,7 +42,7 @@ def circuits(self, backend=None):
# Generate data for combination
for expr in self._experiments:
# Add subcircuits
circs = expr.circuits(backend)
circs = expr._circuits(backend)
sub_circuits.append(circs)
sub_size.append(len(circs))

Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/library/calibration/drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def __init__(self, qubit: int):

super().__init__([qubit])

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Create the circuits for the Drag calibration.

Args:
Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/library/calibration/fine_amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def _pre_circuit(self) -> QuantumCircuit:

return circuit

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Create the circuits for the fine amplitude calibration experiment.

Args:
Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/library/calibration/fine_drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def _post_circuit() -> QuantumCircuit:
circ.ry(np.pi / 2, 0) # Maps unwanted Z rotations to qubit population.
return circ

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Create the circuits for the fine DRAG calibration experiment.

Args:
Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/library/calibration/rabi.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None):

return default_schedule

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Create the circuits for the Rabi experiment.

Args:
Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/library/calibration/ramsey_xy.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def _pre_circuit(self) -> QuantumCircuit:
"""
return QuantumCircuit(1)

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Create the circuits for the Ramsey XY calibration experiment.

Args:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def _build_cr_schedule(

return cross_resonance

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Return a list of experiment circuits.

Args:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def _template_circuit(self, freq_param) -> QuantumCircuit:

return circuit

def circuits(self, backend: Optional[Backend] = None):
def _circuits(self, backend: Optional[Backend] = None):
"""Create the circuit for the spectroscopy experiment.

The circuits are based on a GaussianSquare pulse and a frequency_shift instruction
Expand Down
15 changes: 12 additions & 3 deletions qiskit_experiments/library/characterization/t1.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
"""

from typing import List, Optional, Union
import numpy as np

from qiskit.providers import Backend
import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.providers import Backend
from qiskit.test.mock import FakeBackend

from qiskit_experiments.framework import BaseExperiment, Options
from qiskit_experiments.library.characterization.t1_analysis import T1Analysis
Expand Down Expand Up @@ -89,7 +90,7 @@ def __init__(
# Set experiment options
self.set_experiment_options(delays=delays, unit=unit)

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""
Return a list of experiment circuits

Expand Down Expand Up @@ -131,3 +132,11 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
circuits.append(circ)

return circuits

def _pre_transpile_action(self, backend: Backend):
"""Set schedule method if not simulator."""
is_simulator = getattr(backend.configuration(), "simulator", False)

if not is_simulator and not isinstance(backend, FakeBackend):
if "scheduling_method" not in self.transpile_options.__dict__:
self.set_transpile_options(scheduling_method="alap")
18 changes: 14 additions & 4 deletions qiskit_experiments/library/characterization/t2ramsey.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
"""

from typing import List, Optional, Union
import numpy as np

import numpy as np
import qiskit
from qiskit.utils import apply_prefix
from qiskit.providers import Backend
from qiskit.circuit import QuantumCircuit
from qiskit.providers import Backend
from qiskit.test.mock import FakeBackend
from qiskit.utils import apply_prefix

from qiskit_experiments.framework import BaseExperiment, Options
from .t2ramsey_analysis import T2RamseyAnalysis

Expand Down Expand Up @@ -102,7 +104,7 @@ def __init__(
super().__init__([qubit])
self.set_experiment_options(delays=delays, unit=unit, osc_freq=osc_freq)

def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
def _circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
"""Return a list of experiment circuits.

Each circuit consists of a Hadamard gate, followed by a fixed delay,
Expand Down Expand Up @@ -154,3 +156,11 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]:
circuits.append(circ)

return circuits

def _pre_transpile_action(self, backend: Backend):
"""Set schedule method if not simulator."""
is_simulator = getattr(backend.configuration(), "simulator", False)

if not is_simulator and not isinstance(backend, FakeBackend):
if "scheduling_method" not in self.transpile_options.__dict__:
self.set_transpile_options(scheduling_method="alap")
Loading