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

Replace __fixed_parameters__ with analysis option #734

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
82 changes: 54 additions & 28 deletions qiskit_experiments/curve_analysis/curve_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
ExperimentData,
AnalysisResultData,
Options,
AnalysisConfig,
)

PARAMS_ENTRY_PREFIX = "@Parameters_"
Expand Down Expand Up @@ -233,13 +234,22 @@ class AnalysisExample(CurveAnalysis):
#: List[SeriesDef]: List of mapping representing a data series
__series__ = list()

#: List[str]: Fixed parameter in fit function. Value should be set to the analysis options.
__fixed_parameters__ = list()

def __init__(self):
"""Initialize data fields that are privately accessed by methods."""
super().__init__()

if hasattr(self, "__fixed_parameters__"):
warnings.warn(
"The class attribute __fixed_parameters__ has been deprecated and will be removed. "
"Now this attribute is absorbed in analysis options as fixed_parameters.",
DeprecationWarning,
stacklevel=2,
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
)
# pylint: disable=no-member
self._options.fixed_parameters = {
p: self.options.get(p, None) for p in self.__fixed_parameters__
}

#: Dict[str, Any]: Experiment metadata
self.__experiment_metadata = None

Expand Down Expand Up @@ -271,21 +281,12 @@ def _fit_params(cls) -> List[str]:
)

# remove the first function argument. this is usually x, i.e. not a fit parameter.
fit_params = list(list(fsigs)[0].parameters.keys())[1:]

# remove fixed parameters
if cls.__fixed_parameters__ is not None:
for fixed_param in cls.__fixed_parameters__:
try:
fit_params.remove(fixed_param)
except ValueError as ex:
raise AnalysisError(
f"Defined fixed parameter {fixed_param} is not a fit function argument."
"Update series definition to ensure the parameter name is defined with "
f"fit functions. Currently available parameters are {fit_params}."
) from ex

return fit_params
return list(list(fsigs)[0].parameters.keys())[1:]

@property
def parameters(self) -> List[str]:
"""Return parameters of this curve analysis."""
return [s for s in self._fit_params() if s not in self.options.fixed_parameters]

@classmethod
def _default_options(cls) -> Options:
Expand Down Expand Up @@ -339,6 +340,9 @@ def _default_options(cls) -> Options:
as extra information.
curve_fitter_options (Dict[str, Any]) Options that are passed to the
specified curve fitting function.
fixed_parameters (Dict[str, Any]): Fitting model parameters that are fixed
during the curve fitting. This should be provided with default value
keyed on one of the parameter names in the series definition.
"""
options = super()._default_options()

Expand All @@ -360,11 +364,9 @@ def _default_options(cls) -> Options:
options.style = PlotterStyle()
options.extra = dict()
options.curve_fitter_options = dict()

# automatically populate initial guess and boundary
fit_params = cls._fit_params()
options.p0 = {par_name: None for par_name in fit_params}
options.bounds = {par_name: None for par_name in fit_params}
options.p0 = {}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hint for review. This cannot be initialized with parameter names, because fixed params are now instance attribute that user can modify after the instance is created. Note that now we have .parameters property, thus user can know what parameters live in the fit model.

options.bounds = {}
options.fixed_parameters = {}

return options

Expand Down Expand Up @@ -754,16 +756,15 @@ def _run_analysis(
#

# Update all fit functions in the series definitions if fixed parameter is defined.
# Fixed parameters should be provided by the analysis options.
if self.__fixed_parameters__:
assigned_params = {k: self.options.get(k, None) for k in self.__fixed_parameters__}
assigned_params = self.options.fixed_parameters

if assigned_params:
# Check if all parameters are assigned.
if any(v is None for v in assigned_params.values()):
raise AnalysisError(
f"Unassigned fixed-value parameters for the fit "
f"function {self.__class__.__name__}."
f"All values of fixed-parameters, i.e. {self.__fixed_parameters__}, "
f"All values of fixed-parameters, i.e. {assigned_params}, "
"must be provided by the analysis options to run this analysis."
)
Comment on lines +761 to 771
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be a bit more concise and amend the code after this block too?

if any(v is None for v in self.options.fixed_parameters.values()):
    raise AnalysisError(f"All values of {self.options.fixed_parameters} must be provided.")

then the line below can become if it is not too long:

dict_def["fit_func"] = functools.partial(series_def.fit_func, **self.options.fixed_parameters)

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 this is good suggestion but anyways this code block will be immediately removed by #726 so I just leave this as is


Expand Down Expand Up @@ -815,7 +816,7 @@ def _run_analysis(

# Generate algorithmic initial guesses and boundaries
default_fit_opt = FitOptions(
parameters=self._fit_params(),
parameters=self.parameters,
default_p0=self.options.p0,
default_bounds=self.options.bounds,
**self.options.curve_fitter_options,
Expand Down Expand Up @@ -964,6 +965,31 @@ def _run_analysis(

return analysis_results, figures

@classmethod
def from_config(cls, config: Union[AnalysisConfig, Dict]) -> "CurveAnalysis":
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
instance = super().from_config(config)

# When fixed param value is hard-coded as options. This is deprecated data structure.
loaded_opts = instance.options.__dict__

# pylint: disable=no-member
deprecated_fixed_params = {
p: loaded_opts[p] for p in instance.parameters if p in loaded_opts
}
if any(deprecated_fixed_params):
warnings.warn(
"Fixed parameter value should be defined in options.fixed_parameters as "
"a dictionary values, rather than a standalone analysis option. "
"Please re-save this experiment to be loaded after deprecation period.",
DeprecationWarning,
stacklevel=2,
)
new_fixed_params = instance.options.fixed_parameters
new_fixed_params.update(deprecated_fixed_params)
instance.set_options(fixed_parameters=new_fixed_params)

return instance


def is_error_not_significant(
val: Union[float, uncertainties.UFloat],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,15 @@ def _default_options(cls):
descriptions of analysis options.

Analysis Options:
angle_per_gate (float): The ideal angle per repeated gate.
The user must set this option as it defaults to None.
phase_offset (float): A phase offset for the analysis. This phase offset will be
:math:`\pi/2` if the square-root of X gate is added before the repeated gates.
This is decided for the user in :meth:`set_schedule` depending on whether the
sx gate is included in the experiment.
max_good_angle_error (float): The maximum angle error for which the fit is
considered as good. Defaults to :math:`\pi/2`.
"""
default_options = super()._default_options()
default_options.result_parameters = ["d_theta"]
default_options.xlabel = "Number of gates (n)"
default_options.ylabel = "Population"
default_options.angle_per_gate = None
default_options.phase_offset = 0.0
default_options.max_good_angle_error = np.pi / 2
default_options.amp = 1.0
default_options.ylim = [0, 1.0]
default_options.max_good_angle_error = np.pi / 2

return default_options

Expand All @@ -140,6 +131,8 @@ def _generate_fit_guesses(
Raises:
CalibrationError: When ``angle_per_gate`` is missing.
"""
fixed_params = self.options.fixed_parameters

curve_data = self._data()
max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True)
max_y, min_y = np.max(curve_data.y), np.min(curve_data.y)
Expand All @@ -152,16 +145,19 @@ def _generate_fit_guesses(
if "amp" in user_opt.p0:
user_opt.p0.set_if_empty(amp=max_y - min_y)
user_opt.bounds.set_if_empty(amp=(0, 2 * max_abs_y))
amp = user_opt.p0["amp"]
else:
# Fixed parameter
amp = fixed_params.get("amp", 1.0)

# Base the initial guess on the intended angle_per_gate and phase offset.
apg = self.options.angle_per_gate
phi = self.options.phase_offset
apg = user_opt.p0.get("angle_per_gate", fixed_params.get("angle_per_gate", 0.0))
phi = user_opt.p0.get("phase_offset", fixed_params.get("phase_offset", 0.0))

# Prepare logical guess for specific condition (often satisfied)
d_theta_guesses = []

offsets = apg * curve_data.x + phi
amp = user_opt.p0.get("amp", self.options.amp)
for i in range(curve_data.x.size):
xi = curve_data.x[i]
yi = curve_data.y[i]
Expand Down
13 changes: 8 additions & 5 deletions qiskit_experiments/library/calibration/fine_amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,10 @@ def __init__(
auto_update=auto_update,
)
self.analysis.set_options(
angle_per_gate=np.pi,
phase_offset=np.pi / 2,
amp=1,
fixed_parameters={
"angle_per_gate": np.pi,
"phase_offset": np.pi / 2,
}
)

@classmethod
Expand Down Expand Up @@ -222,8 +223,10 @@ def __init__(
auto_update=auto_update,
)
self.analysis.set_options(
angle_per_gate=np.pi / 2,
phase_offset=np.pi,
fixed_parameters={
"angle_per_gate": np.pi / 2,
"phase_offset": np.pi,
}
)

@classmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,3 @@ class FineAmplitudeAnalysis(ErrorAmplificationAnalysis):
filter_kwargs={"series": 1},
),
]

__fixed_parameters__ = ["angle_per_gate", "phase_offset"]
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

"""Fine DRAG calibration analysis."""

import warnings

import numpy as np
from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis
from qiskit_experiments.framework import Options
Expand All @@ -32,6 +34,16 @@ class FineDragAnalysis(ErrorAmplificationAnalysis):

__fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"]

def __init__(self):
super().__init__()

warnings.warn(
nkanazawa1989 marked this conversation as resolved.
Show resolved Hide resolved
f"{self.__class__.__name__} has been deprecated. Use ErrorAmplificationAnalysis "
"instance with the analysis options involving the fixed_parameters.",
DeprecationWarning,
stacklevel=2,
)

@classmethod
def _default_options(cls) -> Options:
"""Default analysis options."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

"""Fine frequency experiment analysis."""

import warnings

import numpy as np

from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis
Expand All @@ -33,6 +35,16 @@ class FineFrequencyAnalysis(ErrorAmplificationAnalysis):

__fixed_parameters__ = ["angle_per_gate", "phase_offset"]

def __init__(self):
super().__init__()

warnings.warn(
f"{self.__class__.__name__} has been deprecated. Use ErrorAmplificationAnalysis "
"instance with the analysis options involving the fixed_parameters.",
DeprecationWarning,
stacklevel=2,
)

@classmethod
def _default_options(cls) -> Options:
"""Default analysis options."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

"""Fine half angle calibration analysis."""

import warnings

import numpy as np
from qiskit_experiments.framework import Options
from qiskit_experiments.curve_analysis import ErrorAmplificationAnalysis, ParameterRepr
Expand All @@ -31,6 +33,16 @@ class FineHalfAngleAnalysis(ErrorAmplificationAnalysis):

__fixed_parameters__ = ["angle_per_gate", "phase_offset", "amp"]

def __init__(self):
super().__init__()

warnings.warn(
f"{self.__class__.__name__} has been deprecated. Use ErrorAmplificationAnalysis "
"instance with the analysis options involving the fixed_parameters.",
DeprecationWarning,
stacklevel=2,
)

@classmethod
def _default_options(cls) -> Options:
r"""Default analysis options.
Expand Down
20 changes: 12 additions & 8 deletions qiskit_experiments/library/characterization/fine_amplitude.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,10 @@ def __init__(self, qubit: int, backend: Optional[Backend] = None):
super().__init__([qubit], XGate(), backend=backend)
# Set default analysis options
self.analysis.set_options(
angle_per_gate=np.pi,
phase_offset=np.pi / 2,
amp=1,
Copy link
Contributor

Choose a reason for hiding this comment

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

We no longer need to fix amp=1 right? Was this missed in a previous PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, indeed this is not fixed. amp is in the default option of this analysis to give some initial value (this is bit strange) but not listed in the fixed parameters.

fixed_parameters={
"angle_per_gate": np.pi,
"phase_offset": np.pi / 2,
}
)

@classmethod
Expand Down Expand Up @@ -290,8 +291,10 @@ def __init__(self, qubit: int, backend: Optional[Backend] = None):
super().__init__([qubit], SXGate(), backend=backend)
# Set default analysis options
self.analysis.set_options(
angle_per_gate=np.pi / 2,
phase_offset=np.pi,
fixed_parameters={
"angle_per_gate": np.pi / 2,
"phase_offset": np.pi,
}
)

@classmethod
Expand Down Expand Up @@ -353,9 +356,10 @@ def __init__(self, qubits: Sequence[int], backend: Optional[Backend] = None):
super().__init__(qubits, gate, backend=backend, measurement_qubits=[qubits[1]])
# Set default analysis options
self.analysis.set_options(
angle_per_gate=np.pi / 2,
phase_offset=np.pi,
amp=1,
fixed_parameters={
"angle_per_gate": np.pi / 2,
"phase_offset": np.pi,
},
outcome="1",
)

Expand Down
18 changes: 13 additions & 5 deletions qiskit_experiments/library/characterization/fine_drag.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
from qiskit.circuit.library import XGate, SXGate
from qiskit.providers.backend import Backend
from qiskit_experiments.framework import BaseExperiment, Options
from qiskit_experiments.library.characterization.analysis import (
FineDragAnalysis,
)
from qiskit_experiments.curve_analysis.standard_analysis import ErrorAmplificationAnalysis


class FineDrag(BaseExperiment):
Expand Down Expand Up @@ -126,7 +124,7 @@ class FineDrag(BaseExperiment):
This is the correction formula in the FineDRAG Updater.

# section: analysis_ref
:py:class:`FineDragAnalysis`
:py:class:`~qiskit_experiments.curve_analysis.ErrorAmplificationAnalysis`

# section: see_also
qiskit_experiments.library.calibration.drag.DragCal
Expand Down Expand Up @@ -161,7 +159,17 @@ def __init__(self, qubit: int, gate: Gate, backend: Optional[Backend] = None):
gate: The gate that will be repeated.
backend: Optional, the backend to run the experiment on.
"""
super().__init__([qubit], analysis=FineDragAnalysis(), backend=backend)
analysis = ErrorAmplificationAnalysis()
analysis.set_options(
normalization=True,
fixed_parameters={
"angle_per_gate": 0.0,
"phase_offset": np.pi / 2,
"amp": 1.0,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is amp=1.0 still needed? If so we can leave as is. This is also not really related to this PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Exactly. I just copied current implementation. For DRAG one I think amp is needed otherwise you need to add spam cal experiment like fine amp.

},
)

super().__init__([qubit], analysis=analysis, backend=backend)
self.set_experiment_options(gate=gate)

@staticmethod
Expand Down
Loading