Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions qiskit_experiments/framework/base_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def run(
if figures:
experiment_data.add_figures(figures)

# Run post analysis
if experiment_data.experiment is not None:
experiment_data.experiment._post_analysis_action(experiment_data)
Comment thread
nkanazawa1989 marked this conversation as resolved.
Outdated

return experiment_data

def _format_analysis_result(self, data, experiment_id, experiment_components=None):
Expand Down
139 changes: 103 additions & 36 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,43 +104,24 @@ 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.run_transpile(backend)

# Execute experiment
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

experiment_data.add_data(job, post_processing_callback=run_analysis)
experiment_data.add_data(job, post_processing_callback=self.run_analysis)
else:
experiment_data.add_data(job)

# Return the ExperimentData future
return experiment_data

def _initialize_experiment_data(
Expand All @@ -167,11 +143,106 @@ def _initialize_experiment_data(

return experiment_data._copy_metadata()

def run_analysis(self, experiment_data, **options) -> ExperimentData:
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 run_transpile(self, backend: Backend, **options) -> List[QuantumCircuit]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think this is a good name for this method. This isn't just doing run transpile, this is doing full circuit generation and the pre and post transpile actions along with transpilation. This is why I suggested calling it circuits in your original PR. Maybe run_circuits is better since it can potentially have side effects now with the pre and post actions, while the original circuits one didn't.

@nkanazawa1989 nkanazawa1989 Oct 5, 2021

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I see your point. I don't think they do have side effect, because users don't directly submit output of circuits method to the backend. I assume this is mainly used to check what is executed (I'm fine with making it protected). I feel circuits and run_circuits are confusing, so the options would be

  1. Turning current circuits into a protected method and replacing circuits method with run_transpile. This means one cannot check logical circuits. The output of circuits will be transpiled and usually they look very complicated.

  2. Assigning completely different name to run_transpile. Something like compile_experiment or prepare_execution?

How do these options sound?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 3b6b372. Basically run_transpile functionality is integrated into circuits method and the original circuits method is now renamed to _circuits. New circuits method has an option run_transpile defaults to False, so by default, it returns logical circuits as before.

"""Run transpile and return transpiled circuits.

Args:
backend: Target backend.
options: User provided runtime options.

Returns:
Transpiled circuit to execute.
"""
# Run pre transpile if implemented by subclasses.
self._pre_transpile_action(backend)

# Get transpile options
transpile_options = copy.copy(self.transpile_options)
transpile_options.update_options(
initial_layout=list(self._physical_qubits),
**options,
)
transpile_options = transpile_options.__dict__

circuits = transpile(circuits=self.circuits(backend), backend=backend, **transpile_options)

# 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. Otherwise you may want to
call :meth:`block_for_results` method of the ``experiment_data`` here
to freeze processing chain until the job result is returned.

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
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.

Args:
experiment_data (ExperimentData): the experiment data to analyze.
experiment_data: The experiment data to analyze.
options: additional analysis options. Any values set here will
override the value from :meth:`analysis_options`
for the current run.
Expand All @@ -180,7 +251,7 @@ def run_analysis(self, experiment_data, **options) -> ExperimentData:
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 @@ -335,10 +406,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 Down
13 changes: 11 additions & 2 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 @@ -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")
16 changes: 13 additions & 3 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 @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,16 @@ def _get_circuit_metadata(self, circuit):
return meta
return None

def _postprocess_transpiled_circuits(self, circuits, backend, **run_options):
"""Additional post-processing of transpiled circuits before running on backend"""
for c in circuits:
meta = self._get_circuit_metadata(c)
def _post_transpile_action(
self, circuits: List[QuantumCircuit], backend: Backend
) -> List[QuantumCircuit]:
"""Count gate operations in each circuit and update metadata."""
for circuit in circuits:
meta = self._get_circuit_metadata(circuit)
if meta is not None:
c_count_ops = RBUtils.count_ops(c, self.physical_qubits)
c_count_ops = RBUtils.count_ops(circuit, self.physical_qubits)
circuit_length = meta["xval"]
count_ops = [(key, (value, circuit_length)) for key, value in c_count_ops.items()]
meta.update({"count_ops": count_ops})

return circuits
2 changes: 1 addition & 1 deletion qiskit_experiments/test/t1_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self, t1, initial_prob1=None, readout0to1=None, readout1to0=None, d
configuration = QasmBackendConfiguration(
backend_name="t1_simulator",
backend_version="0",
n_qubits=int(1e6),
n_qubits=100,
basis_gates=["barrier", "x", "delay", "measure"],
gates=[],
local=True,
Expand Down
2 changes: 1 addition & 1 deletion qiskit_experiments/test/t2ramsey_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(
configuration = QasmBackendConfiguration(
backend_name="T2Ramsey_simulator",
backend_version="0",
n_qubits=int(1e6),
n_qubits=100,
basis_gates=["barrier", "h", "p", "delay", "measure"],
gates=[],
local=True,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
developer:
- |
The execution chain of :py:class:`~qiskit_experiments.framework.base_experiment.BaseExperiment`
has been updated with some flexibility. The new feature will benefit experiment developers
who need to modify the standard job execution and analysis workflow.

With this change, following three methods are newly introduced.

- :py:meth:`~qiskit_experiments.framework.base_experiment.BaseExperiment#_pre_transpile_action`
- :py:meth:`~qiskit_experiments.framework.base_experiment.BaseExperiment#_post_transpile_action`
- :py:meth:`~qiskit_experiments.framework.base_experiment.BaseExperiment#_post_analysis_action`

These methods allow a developer to insert extra data processing routine (somewhat of hooks)
in between circuit generation and result data generation.
This feature increases the flexibility of job execution.

In addition, :py:meth:`run_transpile` method is added to all experiment classes.
This returns a list of quantum circuits to execute on the given backend.