From 6ce293f8cea84d76ae6ee42790370471293e277d Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 24 Jul 2023 16:00:10 +0900 Subject: [PATCH 01/15] Add frequency scan option --- .../library/characterization/t1.py | 135 +++++++++++++++--- .../characterization/test_stark_p1_spect.py | 91 +++++++++++- 2 files changed, 201 insertions(+), 25 deletions(-) diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 43275d836c..279f7bc41d 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -21,7 +21,7 @@ from qiskit.providers.backend import Backend from qiskit.utils import optionals as _optional -from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options +from qiskit_experiments.framework import BackendTiming, BaseExperiment, ExperimentData, Options from qiskit_experiments.library.characterization.analysis.t1_analysis import ( T1Analysis, StarkP1SpectAnalysis, @@ -202,16 +202,18 @@ def _default_experiment_options(cls) -> Options: of the Stark tone, in seconds. stark_risefall (float): Ratio of sigma to the duration of the rising and falling edges of the Stark tone. - min_stark_amp (float): Minimum Stark tone amplitude. - max_stark_amp (float): Maximum Stark tone amplitude. - num_stark_amps (int): Number of Stark tone amplitudes to scan. + min_xval (float): Minimum x value. + max_xval (float): Maximum x value. + num_xvals (int): Number of x-values to scan. + xval_type (str): Type of x-value. Either ``amplitude`` or ``frequency``. + Setting to frequency requires pre-calibration of Stark shift coefficients. spacing (str): A policy for the spacing to create an amplitude list from ``min_stark_amp`` to ``max_stark_amp``. Either ``linear`` or ``quadratic`` must be specified. - stark_amps (list[float]): The list of amplitude that will be scanned in the experiment. - If not set, then ``num_stark_amps`` amplitudes spaced according to - the ``spacing`` policy between ``min_stark_amp`` and ``max_stark_amp`` are used. - If ``stark_amps`` is set, these parameters are ignored. + xvals (list[float]): The list of x-values that will be scanned in the experiment. + If not set, then ``num_xvals`` parameters spaced according to + the ``spacing`` policy between ``min_xval`` and ``max_xval`` are used. + If ``xvals`` is set, these parameters are ignored. """ options = super()._default_experiment_options() options.update_options( @@ -220,13 +222,17 @@ def _default_experiment_options(cls) -> Options: stark_freq_offset=80e6, stark_sigma=15e-9, stark_risefall=2, - min_stark_amp=-1, - max_stark_amp=1, - num_stark_amps=201, + min_xval=-1, + max_xval=1, + num_xvals=201, + xval_type="amplitude", spacing="quadratic", - stark_amps=None, + xvals=None, + service=None, + stark_coefficients="latest", ) options.set_validator("spacing", ["linear", "quadratic"]) + options.set_validator("xval_type", ["amplitude", "frequency"]) options.set_validator("stark_freq_offset", (0, np.inf)) options.set_validator("stark_channel", pulse.channels.PulseChannel) return options @@ -234,6 +240,10 @@ def _default_experiment_options(cls) -> Options: def _set_backend(self, backend: Backend): super()._set_backend(backend) self._timing = BackendTiming(backend) + if self.experiment_options.service is not None: + self.set_experiment_options( + service=ExperimentData.get_service_from_backend(backend), + ) def parameters(self) -> np.ndarray: """Stark tone amplitudes to use in circuits. @@ -244,21 +254,110 @@ def parameters(self) -> np.ndarray: """ opt = self.experiment_options # alias - if opt.stark_amps is None: + if opt.xvals is None: if opt.spacing == "linear": - params = np.linspace(opt.min_stark_amp, opt.max_stark_amp, opt.num_stark_amps) + params = np.linspace(opt.min_xval, opt.max_xval, opt.num_xvals) elif opt.spacing == "quadratic": - min_sqrt = np.sign(opt.min_stark_amp) * np.sqrt(np.abs(opt.min_stark_amp)) - max_sqrt = np.sign(opt.max_stark_amp) * np.sqrt(np.abs(opt.max_stark_amp)) - lin_params = np.linspace(min_sqrt, max_sqrt, opt.num_stark_amps) + min_sqrt = np.sign(opt.min_xval) * np.sqrt(np.abs(opt.min_xval)) + max_sqrt = np.sign(opt.max_xval) * np.sqrt(np.abs(opt.max_xval)) + lin_params = np.linspace(min_sqrt, max_sqrt, opt.num_xvals) params = np.sign(lin_params) * lin_params**2 else: raise ValueError(f"Spacing option {opt.spacing} is not valid.") else: - params = np.asarray(opt.stark_amps, dtype=float) + params = np.asarray(opt.xvals, dtype=float) + if opt.xval_type == "frequency": + return self._frequencies_to_amplitudes(params) return params + def _frequencies_to_amplitudes(self, params: np.ndarray) -> np.ndarray: + """A helper method to convert frequency values to amplitude. + + Args: + params: Parameters representing a frequency of Stark shift. + + Returns: + Corresponding Stark tone amplitudes. + + Raises: + RuntimeError: When service or analysis results for Stark coefficients are not available. + TypeError: When attached analysis class is not valid. + KeyError: When stark_coefficients dictionary is provided but keys are missing. + ValueError: When specified Stark shift is not available. + """ + opt = self.experiment_options # alias + + if not isinstance(self.analysis, StarkP1SpectAnalysis): + raise TypeError( + f"Analysis class {self.analysis.__class__.__name__} is not a subclass of " + "StarkP1SpectAnalysis. Use proper analysis class to scan frequencies." + ) + coef_names = self.analysis.stark_coefficients_names + + if opt.stark_coefficients == "latest": + if opt.service is None: + raise RuntimeError( + "Experiment service is not available. Provide a dictionary of " + "Stark coefficients in the experiment options." + ) + coefficients = self.analysis.retrieve_coefficients_from_service( + service=opt.service, + qubit=self.physical_qubits[0], + backend=self._backend_data.name, + ) + if coefficients is None: + raise RuntimeError( + "Experiment results for the coefficients of the Stark shift is not found " + f"for the backend {self._backend_data.name} qubit {self.physical_qubits}." + ) + else: + missing = set(coef_names) - opt.stark_coefficients.keys() + if any(missing): + raise KeyError( + f"Following coefficient data is missing in the 'stark_coefficients': {missing}." + ) + coefficients = opt.stark_coefficients + positive = np.asarray([coefficients[coef_names[idx]] for idx in [2, 1, 0]]) + negative = np.asarray([coefficients[coef_names[idx]] for idx in [5, 4, 3]]) + offset = coefficients[coef_names[6]] + + amplitudes = np.zeros_like(params) + for idx, param in enumerate(params): + constant = offset - param + if np.isclose(constant, 0.0): + # This is a point where Stark amplitude is expected to be zero. + # Because np.sign(0) = 0 as an exception, this requires special handling. + # https://numpy.org/doc/stable/reference/generated/numpy.sign.html + amplitudes[idx] = 0.0 + continue + if param > 0: + amp_candidates = np.roots(np.append(positive, constant)) + else: + amp_candidates = np.roots(np.append(negative, constant)) + # Because the fit function is third order, we get three solutions here. + # Only one valid solution must exist because we assume + # a monotonic trend for Stark shift against tone amplitude in domain of definition. + # + # The criteria of the valid solution are + # - Frequency shift and tone have the same sign (by definition) + # - Tone amplitude is a real value + # - The absolute value of tone amplitude must be less than 1.0 + # + valid_amp = np.where( + (np.sign(amp_candidates.real) == np.sign(param)) + & np.isclose(amp_candidates.imag, 0.0) + & (np.abs(amp_candidates.real) < 1.0) + ) + try: + amplitudes[idx] = float(amp_candidates[valid_amp].real) + except TypeError as ex: + raise ValueError( + f"Stark shift at frequency value of {param} Hz is not available on this device." + ) from ex + + return amplitudes + def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: """Create circuits with parameters for P1 experiment with Stark shift. diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/characterization/test_stark_p1_spect.py index e3a4f9e2cc..fabf9e50fa 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/characterization/test_stark_p1_spect.py @@ -82,9 +82,9 @@ def test_linear_spaced_parameters(self): """Test generating parameters with linear spacing.""" exp = StarkP1Spectroscopy((0,)) exp.set_experiment_options( - min_stark_amp=-1, - max_stark_amp=1, - num_stark_amps=5, + min_xval=-1, + max_xval=1, + num_xvals=5, spacing="linear", ) params = exp.parameters() @@ -96,9 +96,9 @@ def test_quadratic_spaced_parameters(self): """Test generating parameters with quadratic spacing.""" exp = StarkP1Spectroscopy((0,)) exp.set_experiment_options( - min_stark_amp=-1, - max_stark_amp=1, - num_stark_amps=5, + min_xval=-1, + max_xval=1, + num_xvals=5, spacing="quadratic", ) params = exp.parameters() @@ -112,6 +112,83 @@ def test_invalid_spacing(self): with self.assertRaises(ValueError): exp.set_experiment_options(spacing="invalid_option") + def test_raises_scanning_frequency_without_service(self): + """Test raises error when frequency is set without having service or coefficients set.""" + exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) + exp.set_experiment_options( + xvals=[-100e6, -50e6, 0, 50e6, 100e6], + xval_type="frequency", + stark_coefficients="latest", + ) + with self.assertRaises(RuntimeError): + exp.parameters() + + def test_scanning_frequency(self): + """Test scanning frequency with experiment service.""" + exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) + + ref_amplitudes = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) + test_freqs = np.where( + ref_amplitudes > 0, + ( + self.coeffs["stark_pos_coef_o1"] * ref_amplitudes + + self.coeffs["stark_pos_coef_o2"] * ref_amplitudes**2 + + self.coeffs["stark_pos_coef_o3"] * ref_amplitudes**3 + + self.coeffs["stark_ferr"] + ), + ( + self.coeffs["stark_neg_coef_o1"] * ref_amplitudes + + self.coeffs["stark_neg_coef_o2"] * ref_amplitudes**2 + + self.coeffs["stark_neg_coef_o3"] * ref_amplitudes**3 + + self.coeffs["stark_ferr"] + ), + ) + exp.set_experiment_options( + xvals=test_freqs, + xval_type="frequency", + service=self.service, + ) + params = exp.parameters() + + np.testing.assert_array_almost_equal(params, ref_amplitudes) + + def test_scanning_frequency_with_coeffs(self): + """Test scanning frequency with manually provided Stark coefficients.""" + exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) + + ref_amplitudes = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) + test_freqs = np.where( + ref_amplitudes > 0, + ( + self.coeffs["stark_pos_coef_o1"] * ref_amplitudes + + self.coeffs["stark_pos_coef_o2"] * ref_amplitudes**2 + + self.coeffs["stark_pos_coef_o3"] * ref_amplitudes**3 + + self.coeffs["stark_ferr"] + ), + ( + self.coeffs["stark_neg_coef_o1"] * ref_amplitudes + + self.coeffs["stark_neg_coef_o2"] * ref_amplitudes**2 + + self.coeffs["stark_neg_coef_o3"] * ref_amplitudes**3 + + self.coeffs["stark_ferr"] + ), + ) + exp.set_experiment_options( + xvals=test_freqs, + xval_type="frequency", + stark_coefficients={ + "stark_pos_coef_o1": self.coeffs["stark_pos_coef_o1"], + "stark_pos_coef_o2": self.coeffs["stark_pos_coef_o2"], + "stark_pos_coef_o3": self.coeffs["stark_pos_coef_o3"], + "stark_neg_coef_o1": self.coeffs["stark_neg_coef_o1"], + "stark_neg_coef_o2": self.coeffs["stark_neg_coef_o2"], + "stark_neg_coef_o3": self.coeffs["stark_neg_coef_o3"], + "stark_ferr": self.coeffs["stark_ferr"], + }, + ) + params = exp.parameters() + + np.testing.assert_array_almost_equal(params, ref_amplitudes) + def test_circuits(self): """Test generated circuits.""" backend = FakeHanoiV2() @@ -125,7 +202,7 @@ def test_circuits(self): exp = StarkP1Spectroscopy((0,), backend) exp.set_experiment_options( - stark_amps=[-0.5, 0.5], + xvals=[-0.5, 0.5], stark_freq_offset=10e6, t1_delay=100, stark_sigma=15, From 498f8a619e6f700532e96bc8f0f5fc2ed42be8d8 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 26 Jul 2023 14:06:03 +0900 Subject: [PATCH 02/15] Add function to calculate min, max frequency value --- .../characterization/analysis/t1_analysis.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index cf34a15460..bd7f287a82 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -275,6 +275,50 @@ def retrieve_coefficients_from_service( return None return out + @classmethod + def estimate_minmax_frequencies( + cls, + service: IBMExperimentService, + qubit: int, + backend: str, + max_amplitudes: Tuple[float, float] = (-0.9, 0.9), + ) -> Tuple[float, float]: + """Inquire maximum and minimum Stark shfit available within specified amplitude range. + + Args: + service: A valid experiment service instance. + qubit: Qubit index. + backend: Name of the backend. + max_amplitudes: Minimum and maximum amplitude. + + Returns: + Tuple of minimum and maximum frequency. + """ + coeffs = cls.retrieve_coefficients_from_service(service, qubit, backend) + names = cls.stark_coefficients_names # alias + pos_idxs = [2, 1, 0] + neg_idxs = [5, 4, 3] + + freqs = [] + for idxs, max_amp in zip((neg_idxs, pos_idxs), max_amplitudes): + # Solve for inflection points by computing the point where derivertive becomes zero. + solutions = np.roots( + [deriv * coeffs[names[idx]] for deriv, idx in zip([3, 2, 1], idxs)] + ) + inflection_points = solutions[ + (solutions.imag == 0) & (np.sign(solutions) == np.sign(max_amp)) + ] + if len(inflection_points) == 0: + amp = max_amp + else: + # When multiple inflection points are found, use the most outer one. + # There could be a small inflection point around amp=0, + # when the first order term is significant. + amp = min(max_amp, max(inflection_points, key=abs), key=abs) + polyfun = np.poly1d([coeffs[names[idx]] for idx in [*idxs, 6]]) + freqs.append(polyfun(amp)) + return freqs + def _convert_axis( self, xdata: np.ndarray, From ac22b8ea0c7db9a9375b119c9f2a9ba4af162a84 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 26 Jul 2023 14:06:37 +0900 Subject: [PATCH 03/15] Improve freq->amp conversion for more robustness --- .../library/characterization/t1.py | 64 +++-- .../characterization/test_stark_p1_spect.py | 251 +++++++++++------- 2 files changed, 197 insertions(+), 118 deletions(-) diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 279f7bc41d..8b5eacac23 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -222,8 +222,8 @@ def _default_experiment_options(cls) -> Options: stark_freq_offset=80e6, stark_sigma=15e-9, stark_risefall=2, - min_xval=-1, - max_xval=1, + min_xval=-1.0, + max_xval=1.0, num_xvals=201, xval_type="amplitude", spacing="quadratic", @@ -240,7 +240,7 @@ def _default_experiment_options(cls) -> Options: def _set_backend(self, backend: Backend): super()._set_backend(backend) self._timing = BackendTiming(backend) - if self.experiment_options.service is not None: + if self.experiment_options.service is None: self.set_experiment_options( service=ExperimentData.get_service_from_backend(backend), ) @@ -323,38 +323,46 @@ def _frequencies_to_amplitudes(self, params: np.ndarray) -> np.ndarray: offset = coefficients[coef_names[6]] amplitudes = np.zeros_like(params) - for idx, param in enumerate(params): - constant = offset - param - if np.isclose(constant, 0.0): - # This is a point where Stark amplitude is expected to be zero. - # Because np.sign(0) = 0 as an exception, this requires special handling. - # https://numpy.org/doc/stable/reference/generated/numpy.sign.html - amplitudes[idx] = 0.0 + for idx, tgt_freq in enumerate(params): + stark_shift = tgt_freq - offset + if np.isclose(stark_shift, 0): + amplitudes[idx] = 0 continue - if param > 0: - amp_candidates = np.roots(np.append(positive, constant)) + elif np.sign(stark_shift) > 0: + fit_coeffs = [*positive, -stark_shift] else: - amp_candidates = np.roots(np.append(negative, constant)) + fit_coeffs = [*negative, -stark_shift] + amp_candidates = np.roots(fit_coeffs) # Because the fit function is third order, we get three solutions here. # Only one valid solution must exist because we assume # a monotonic trend for Stark shift against tone amplitude in domain of definition. - # - # The criteria of the valid solution are - # - Frequency shift and tone have the same sign (by definition) - # - Tone amplitude is a real value - # - The absolute value of tone amplitude must be less than 1.0 - # - valid_amp = np.where( - (np.sign(amp_candidates.real) == np.sign(param)) - & np.isclose(amp_candidates.imag, 0.0) - & (np.abs(amp_candidates.real) < 1.0) + criteria = np.all( + [ + # Frequency shift and tone have the same sign by definition + np.sign(amp_candidates.real) == np.sign(stark_shift), + # Tone amplitude is a real value + np.isclose(amp_candidates.imag, 0.0), + # The absolute value of tone amplitude must be less than 1.0 + np.abs(amp_candidates.real) < 1.0, + ], + axis=0, ) - try: - amplitudes[idx] = float(amp_candidates[valid_amp].real) - except TypeError as ex: + valid_amps = amp_candidates[criteria] + if len(valid_amps) == 0: raise ValueError( - f"Stark shift at frequency value of {param} Hz is not available on this device." - ) from ex + f"Stark shift at frequency value of {tgt_freq} Hz is not available on " + f"the backend {self._backend_data.name} qubit {self.physical_qubits}." + ) + elif len(valid_amps) > 1: + # We assume a monotonic trend but sometimes a large third-order term causes + # inflection point and inverts the trend in larger amplitudes. + # In this case we would have more than one solutions, but we can + # take the smallerst amplitude before reaching to the inflection point. + before_inflection = np.argmin(np.abs(valid_amps.real)) + valid_amp = float(valid_amps[before_inflection].real) + else: + valid_amp = float(valid_amps.real) + amplitudes[idx] = valid_amp return amplitudes diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/characterization/test_stark_p1_spect.py index fabf9e50fa..e152618e44 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/characterization/test_stark_p1_spect.py @@ -14,6 +14,7 @@ from test.base import QiskitExperimentsTestCase +from ddt import ddt, data, named_data, unpack import numpy as np from qiskit import pulse from qiskit.circuit import QuantumCircuit, Gate @@ -43,40 +44,52 @@ def _run_spect_analysis( ] +@ddt class TestStarkP1Spectroscopy(QiskitExperimentsTestCase): """Test case for the Stark P1 Spectroscopy experiment.""" - def setUp(self): - super().setUp() - - self.service = FakeService() + @staticmethod + def create_service_helper( + pos_coef_o1: float, + pos_coef_o2: float, + pos_coef_o3: float, + neg_coef_o1: float, + neg_coef_o2: float, + neg_coef_o3: float, + ferr: float, + qubit: int, + backend_name: str, + ): + """A helper method to create service with analysis results.""" + service = FakeService() - self.service.create_experiment( + service.create_experiment( experiment_type="StarkRamseyXYAmpScan", - backend_name="fake_hanoi", + backend_name=backend_name, experiment_id="123456789", ) - self.coeffs = { - "stark_pos_coef_o1": 5e6, - "stark_pos_coef_o2": 200e6, - "stark_pos_coef_o3": -50e6, - "stark_neg_coef_o1": 5e6, - "stark_neg_coef_o2": -180e6, - "stark_neg_coef_o3": -40e6, - "stark_ferr": 100e3, + coeffs = { + "stark_pos_coef_o1": pos_coef_o1, + "stark_pos_coef_o2": pos_coef_o2, + "stark_pos_coef_o3": pos_coef_o3, + "stark_neg_coef_o1": neg_coef_o1, + "stark_neg_coef_o2": neg_coef_o2, + "stark_neg_coef_o3": neg_coef_o3, + "stark_ferr": ferr, } - for i, (key, value) in enumerate(self.coeffs.items()): - self.service.create_analysis_result( + for i, (key, value) in enumerate(coeffs.items()): + service.create_analysis_result( experiment_id="123456789", result_data={"value": value}, result_type=key, - device_components=["Q0"], + device_components=[f"Q{qubit}"], tags=[], quality="Good", verified=False, result_id=str(i), ) + return service def test_linear_spaced_parameters(self): """Test generating parameters with linear spacing.""" @@ -123,71 +136,93 @@ def test_raises_scanning_frequency_without_service(self): with self.assertRaises(RuntimeError): exp.parameters() - def test_scanning_frequency(self): - """Test scanning frequency with experiment service.""" + @named_data( + ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], + ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], + ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], + ) + @unpack + def test_scanning_frequency(self, po1, po2, po3, no1, no2, no3, ferr): + """Test scanning frequency with experiment service. + + This is a sort of round-trip test. + We generate amplitude from frequency through experimetn class. + this amplitude is converted into frequency again with the same coefficients. + Two frequencies must be consistent. + """ + service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") + exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) - ref_amplitudes = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) - test_freqs = np.where( - ref_amplitudes > 0, - ( - self.coeffs["stark_pos_coef_o1"] * ref_amplitudes - + self.coeffs["stark_pos_coef_o2"] * ref_amplitudes**2 - + self.coeffs["stark_pos_coef_o3"] * ref_amplitudes**3 - + self.coeffs["stark_ferr"] - ), - ( - self.coeffs["stark_neg_coef_o1"] * ref_amplitudes - + self.coeffs["stark_neg_coef_o2"] * ref_amplitudes**2 - + self.coeffs["stark_neg_coef_o3"] * ref_amplitudes**3 - + self.coeffs["stark_ferr"] - ), - ) + ref_freqs = np.linspace(-70e6, 70e6, 31) exp.set_experiment_options( - xvals=test_freqs, + xvals=ref_freqs, xval_type="frequency", - service=self.service, + service=service, ) - params = exp.parameters() - np.testing.assert_array_almost_equal(params, ref_amplitudes) + amplitudes = exp.parameters() + + # Compute frequency from parameter values with the same coefficient + analysis = StarkP1SpectAnalysis() + coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") + frequencies = analysis._convert_axis(amplitudes, coeffs) + + np.testing.assert_array_almost_equal(frequencies, ref_freqs) def test_scanning_frequency_with_coeffs(self): - """Test scanning frequency with manually provided Stark coefficients.""" + """Test scanning frequency with manually provided Stark coefficients. + + This is just a difference of API from the test_scanning_frequency. + Data driven test is omitted here. + """ + po1, po2, po3, no1, no2, no3, ferr = 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3 exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) - ref_amplitudes = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) + ref_amps = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) test_freqs = np.where( - ref_amplitudes > 0, - ( - self.coeffs["stark_pos_coef_o1"] * ref_amplitudes - + self.coeffs["stark_pos_coef_o2"] * ref_amplitudes**2 - + self.coeffs["stark_pos_coef_o3"] * ref_amplitudes**3 - + self.coeffs["stark_ferr"] - ), - ( - self.coeffs["stark_neg_coef_o1"] * ref_amplitudes - + self.coeffs["stark_neg_coef_o2"] * ref_amplitudes**2 - + self.coeffs["stark_neg_coef_o3"] * ref_amplitudes**3 - + self.coeffs["stark_ferr"] - ), + ref_amps > 0, + po1 * ref_amps + po2 * ref_amps**2 + po3 * ref_amps**3 + ferr, + no1 * ref_amps + no2 * ref_amps**2 + no3 * ref_amps**3 + ferr, ) exp.set_experiment_options( xvals=test_freqs, xval_type="frequency", stark_coefficients={ - "stark_pos_coef_o1": self.coeffs["stark_pos_coef_o1"], - "stark_pos_coef_o2": self.coeffs["stark_pos_coef_o2"], - "stark_pos_coef_o3": self.coeffs["stark_pos_coef_o3"], - "stark_neg_coef_o1": self.coeffs["stark_neg_coef_o1"], - "stark_neg_coef_o2": self.coeffs["stark_neg_coef_o2"], - "stark_neg_coef_o3": self.coeffs["stark_neg_coef_o3"], - "stark_ferr": self.coeffs["stark_ferr"], + "stark_pos_coef_o1": po1, + "stark_pos_coef_o2": po2, + "stark_pos_coef_o3": po3, + "stark_neg_coef_o1": no1, + "stark_neg_coef_o2": no2, + "stark_neg_coef_o3": no3, + "stark_ferr": ferr, + }, + ) + params = exp.parameters() + np.testing.assert_array_almost_equal(params, ref_amps) + + def test_scaning_frequency_around_zero(self): + """Test scanning frequency around zero.""" + exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) + exp.set_experiment_options( + xvals=[0, 500e3], + xval_type="frequency", + stark_coefficients={ + "stark_pos_coef_o1": 5e6, + "stark_pos_coef_o2": 100e6, + "stark_pos_coef_o3": 10e6, + "stark_neg_coef_o1": -5e6, + "stark_neg_coef_o2": -100e6, + "stark_neg_coef_o3": -10e6, + "stark_ferr": 500e3, }, ) params = exp.parameters() + # Frequency offset is 500 kHz and we need negative shift to tune frequency at zero. + self.assertLess(params[0], 0) - np.testing.assert_array_almost_equal(params, ref_amplitudes) + # Frequency offset is 500 kHz and we don't need tone. + self.assertAlmostEqual(params[1], 0) def test_circuits(self): """Test generated circuits.""" @@ -245,16 +280,47 @@ def test_circuits(self): def test_retrieve_coefficients(self): """Test retrieving Stark coefficients from the experiment service.""" + po1, po2, po3, no1, no2, no3, ferr = 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3 + service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") + retrieved_coeffs = StarkP1SpectAnalysis.retrieve_coefficients_from_service( - service=self.service, + service=service, qubit=0, backend="fake_hanoi", ) self.assertDictEqual( retrieved_coeffs, - self.coeffs, + { + "stark_pos_coef_o1": po1, + "stark_pos_coef_o2": po2, + "stark_pos_coef_o3": po3, + "stark_neg_coef_o1": no1, + "stark_neg_coef_o2": no2, + "stark_neg_coef_o3": no3, + "stark_ferr": ferr, + }, ) + @named_data( + ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], + ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], + ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], + ) + @unpack + def test_estimating_minmax_frequency(self, po1, po2, po3, no1, no2, no3, ferr): + """Test computing the minimum and maximum frequency within the amplitude budget.""" + service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") + analysis = StarkP1SpectAnalysis() + + minf, maxf = analysis.estimate_minmax_frequencies(service, 0, "fake_hanoi", (-0.9, 0.9)) + + amps = np.linspace(-0.9, 0.9, 101) + coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") + freqs = analysis._convert_axis(amps, coeffs) + + self.assertAlmostEqual(minf, min(freqs), delta=1e6) + self.assertAlmostEqual(maxf, max(freqs), delta=1e6) + def test_running_analysis_without_service(self): """Test running analysis without setting service to the experiment data. @@ -263,24 +329,36 @@ def test_running_analysis_without_service(self): analysis = StarkP1SpectAnalysisReturnXvals() xvals = np.linspace(-1, 1, 11) + ref_xvals = xvals exp_data = ExperimentData() for x in xvals: exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) analysis.run(exp_data, replace_results=True) test_xvals = exp_data.analysis_results("xvals").value - ref_xvals = xvals np.testing.assert_array_almost_equal(test_xvals, ref_xvals) - def test_running_analysis_with_service(self): + @named_data( + ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], + ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], + ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], + ) + @unpack + def test_running_analysis_with_service(self, po1, po2, po3, no1, no2, no3, ferr): """Test running analysis by setting service to the experiment data. This must convert x-axis into frequencies with the Stark coefficients. """ + service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") analysis = StarkP1SpectAnalysisReturnXvals() xvals = np.linspace(-1, 1, 11) + ref_xvals = np.where( + xvals > 0, + po1 * xvals + po2 * xvals**2 + po3 * xvals**3 + ferr, + no1 * xvals + no2 * xvals**2 + no3 * xvals**3 + ferr, + ) exp_data = ExperimentData( - service=self.service, + service=service, backend=FakeHanoiV2(), ) exp_data.metadata.update({"physical_qubits": [0]}) @@ -288,46 +366,39 @@ def test_running_analysis_with_service(self): exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) analysis.run(exp_data, replace_results=True) test_xvals = exp_data.analysis_results("xvals").value - ref_xvals = np.where( - xvals > 0, - ( - self.coeffs["stark_pos_coef_o1"] * xvals - + self.coeffs["stark_pos_coef_o2"] * xvals**2 - + self.coeffs["stark_pos_coef_o3"] * xvals**3 - + self.coeffs["stark_ferr"] - ), - ( - self.coeffs["stark_neg_coef_o1"] * xvals - + self.coeffs["stark_neg_coef_o2"] * xvals**2 - + self.coeffs["stark_neg_coef_o3"] * xvals**3 - + self.coeffs["stark_ferr"] - ), - ) np.testing.assert_array_almost_equal(test_xvals, ref_xvals) def test_running_analysis_with_user_provided_coeffs(self): """Test running analysis by manually providing Stark coefficients. This must convert x-axis into frequencies with the provided coefficients. + This is just a difference of API from the test_running_analysis_with_service. + Data driven test is omitted here. """ + po1, po2, po3, no1, no2, no3, ferr = 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3 + analysis = StarkP1SpectAnalysisReturnXvals() analysis.set_options( stark_coefficients={ - "stark_pos_coef_o1": 0.0, - "stark_pos_coef_o2": 200e6, - "stark_pos_coef_o3": 0.0, - "stark_neg_coef_o1": 0.0, - "stark_neg_coef_o2": -200e6, - "stark_neg_coef_o3": 0.0, - "stark_ferr": 0.0, + "stark_pos_coef_o1": po1, + "stark_pos_coef_o2": po2, + "stark_pos_coef_o3": po3, + "stark_neg_coef_o1": no1, + "stark_neg_coef_o2": no2, + "stark_neg_coef_o3": no3, + "stark_ferr": ferr, } ) xvals = np.linspace(-1, 1, 11) + ref_xvals = np.where( + xvals > 0, + po1 * xvals + po2 * xvals**2 + po3 * xvals**3 + ferr, + no1 * xvals + no2 * xvals**2 + no3 * xvals**3 + ferr, + ) exp_data = ExperimentData() for x in xvals: exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) analysis.run(exp_data, replace_results=True) test_xvals = exp_data.analysis_results("xvals").value - ref_xvals = np.where(xvals > 0, 200e6 * xvals**2, -200e6 * xvals**2) np.testing.assert_array_almost_equal(test_xvals, ref_xvals) From effe196d7f1139a5fccfca7a54aea400fc322339 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 26 Jul 2023 14:23:18 +0900 Subject: [PATCH 04/15] Update interface of estimate_minmax_frequencies --- .../characterization/analysis/t1_analysis.py | 23 +++++++++++-------- .../characterization/test_stark_p1_spect.py | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index bd7f287a82..8ff3432c30 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -278,23 +278,28 @@ def retrieve_coefficients_from_service( @classmethod def estimate_minmax_frequencies( cls, - service: IBMExperimentService, - qubit: int, - backend: str, + coefficients: Dict[str, float], max_amplitudes: Tuple[float, float] = (-0.9, 0.9), ) -> Tuple[float, float]: """Inquire maximum and minimum Stark shfit available within specified amplitude range. Args: - service: A valid experiment service instance. - qubit: Qubit index. - backend: Name of the backend. + coefficients: A dictionary of Stark coefficients. max_amplitudes: Minimum and maximum amplitude. Returns: Tuple of minimum and maximum frequency. + + Raises: + KeyError: When coefficients are incomplete. """ - coeffs = cls.retrieve_coefficients_from_service(service, qubit, backend) + missing = set(cls.stark_coefficients_names) - coefficients.keys() + if any(missing): + raise KeyError( + "Following coefficient data is missing in the " + f"'stark_coefficients' dictionary: {missing}." + ) + names = cls.stark_coefficients_names # alias pos_idxs = [2, 1, 0] neg_idxs = [5, 4, 3] @@ -303,7 +308,7 @@ def estimate_minmax_frequencies( for idxs, max_amp in zip((neg_idxs, pos_idxs), max_amplitudes): # Solve for inflection points by computing the point where derivertive becomes zero. solutions = np.roots( - [deriv * coeffs[names[idx]] for deriv, idx in zip([3, 2, 1], idxs)] + [deriv * coefficients[names[idx]] for deriv, idx in zip([3, 2, 1], idxs)] ) inflection_points = solutions[ (solutions.imag == 0) & (np.sign(solutions) == np.sign(max_amp)) @@ -315,7 +320,7 @@ def estimate_minmax_frequencies( # There could be a small inflection point around amp=0, # when the first order term is significant. amp = min(max_amp, max(inflection_points, key=abs), key=abs) - polyfun = np.poly1d([coeffs[names[idx]] for idx in [*idxs, 6]]) + polyfun = np.poly1d([coefficients[names[idx]] for idx in [*idxs, 6]]) freqs.append(polyfun(amp)) return freqs diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/characterization/test_stark_p1_spect.py index e152618e44..d176c3a616 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/characterization/test_stark_p1_spect.py @@ -312,10 +312,10 @@ def test_estimating_minmax_frequency(self, po1, po2, po3, no1, no2, no3, ferr): service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") analysis = StarkP1SpectAnalysis() - minf, maxf = analysis.estimate_minmax_frequencies(service, 0, "fake_hanoi", (-0.9, 0.9)) + coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") + minf, maxf = analysis.estimate_minmax_frequencies(coeffs, (-0.9, 0.9)) amps = np.linspace(-0.9, 0.9, 101) - coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") freqs = analysis._convert_axis(amps, coeffs) self.assertAlmostEqual(minf, min(freqs), delta=1e6) From fda57749b0c46e2418258f70de6818ca17b6dfcd Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 2 Aug 2023 04:38:40 +0900 Subject: [PATCH 05/15] Fix lint error and missing doc for experiment options --- .../characterization/analysis/t1_analysis.py | 2 +- .../library/characterization/t1.py | 16 ++++++++++++++-- .../characterization/test_stark_p1_spect.py | 3 ++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index 8ff3432c30..ea77e6b569 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -322,7 +322,7 @@ def estimate_minmax_frequencies( amp = min(max_amp, max(inflection_points, key=abs), key=abs) polyfun = np.poly1d([coefficients[names[idx]] for idx in [*idxs, 6]]) freqs.append(polyfun(amp)) - return freqs + return tuple(freqs) def _convert_axis( self, diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 8b5eacac23..5b3891f1e5 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -214,6 +214,18 @@ def _default_experiment_options(cls) -> Options: If not set, then ``num_xvals`` parameters spaced according to the ``spacing`` policy between ``min_xval`` and ``max_xval`` are used. If ``xvals`` is set, these parameters are ignored. + service (IBMExperimentService): A valid experiment service instance that can + provide the Stark coefficients for the qubit to run experiment. + This is required only when ``stark_coefficients`` is ``latest`` and + ``xval_type`` is ``frequency``. This value is automatically set when + a backend is attached to this experiment instance. + stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to + convert tone amplitudes into amount of Stark shift. This dictionary must include + all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, + which are calibrated with :class:`.StarkRamseyXYAmpScan`. + Alternatively, it searches for these coefficients in the result database + when "latest" is set. This requires having the experiment service set in + the experiment data to analyze. """ options = super()._default_experiment_options() options.update_options( @@ -328,7 +340,7 @@ def _frequencies_to_amplitudes(self, params: np.ndarray) -> np.ndarray: if np.isclose(stark_shift, 0): amplitudes[idx] = 0 continue - elif np.sign(stark_shift) > 0: + if np.sign(stark_shift) > 0: fit_coeffs = [*positive, -stark_shift] else: fit_coeffs = [*negative, -stark_shift] @@ -353,7 +365,7 @@ def _frequencies_to_amplitudes(self, params: np.ndarray) -> np.ndarray: f"Stark shift at frequency value of {tgt_freq} Hz is not available on " f"the backend {self._backend_data.name} qubit {self.physical_qubits}." ) - elif len(valid_amps) > 1: + if len(valid_amps) > 1: # We assume a monotonic trend but sometimes a large third-order term causes # inflection point and inverts the trend in larger amplitudes. # In this case we would have more than one solutions, but we can diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/characterization/test_stark_p1_spect.py index d176c3a616..0989f4512b 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/characterization/test_stark_p1_spect.py @@ -14,7 +14,7 @@ from test.base import QiskitExperimentsTestCase -from ddt import ddt, data, named_data, unpack +from ddt import ddt, named_data, unpack import numpy as np from qiskit import pulse from qiskit.circuit import QuantumCircuit, Gate @@ -313,6 +313,7 @@ def test_estimating_minmax_frequency(self, po1, po2, po3, no1, no2, no3, ferr): analysis = StarkP1SpectAnalysis() coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") + # pylint: disable=unbalanced-tuple-unpacking minf, maxf = analysis.estimate_minmax_frequencies(coeffs, (-0.9, 0.9)) amps = np.linspace(-0.9, 0.9, 101) From 6e2f44176e41c0784a6faa30925e840d2aa320f4 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 10 Jan 2024 11:10:46 +0900 Subject: [PATCH 06/15] Wording suggestions Co-authored-by: Will Shanks --- .../library/characterization/analysis/t1_analysis.py | 4 ++-- qiskit_experiments/library/characterization/t1.py | 6 +++--- test/library/characterization/test_stark_p1_spect.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index d42867a950..e7196a17eb 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -270,7 +270,7 @@ def estimate_minmax_frequencies( coefficients: Dict[str, float], max_amplitudes: Tuple[float, float] = (-0.9, 0.9), ) -> Tuple[float, float]: - """Inquire maximum and minimum Stark shfit available within specified amplitude range. + """Inquire maximum and minimum Stark shift available within specified amplitude range. Args: coefficients: A dictionary of Stark coefficients. @@ -286,7 +286,7 @@ def estimate_minmax_frequencies( if any(missing): raise KeyError( "Following coefficient data is missing in the " - f"'stark_coefficients' dictionary: {missing}." + f"stark 'coefficients' dictionary: {missing}." ) names = cls.stark_coefficients_names # alias diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 5b3891f1e5..1ba3cadcfd 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -223,9 +223,9 @@ def _default_experiment_options(cls) -> Options: convert tone amplitudes into amount of Stark shift. This dictionary must include all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, which are calibrated with :class:`.StarkRamseyXYAmpScan`. - Alternatively, it searches for these coefficients in the result database - when "latest" is set. This requires having the experiment service set in - the experiment data to analyze. + Alternatively, a search for these coefficients in the result database is run + when "latest" is set. This requires having the experiment service available + in the ``backend`` set for the experiment. """ options = super()._default_experiment_options() options.update_options( diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/characterization/test_stark_p1_spect.py index 0989f4512b..3ae4c17e76 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/characterization/test_stark_p1_spect.py @@ -146,9 +146,9 @@ def test_scanning_frequency(self, po1, po2, po3, no1, no2, no3, ferr): """Test scanning frequency with experiment service. This is a sort of round-trip test. - We generate amplitude from frequency through experimetn class. - this amplitude is converted into frequency again with the same coefficients. - Two frequencies must be consistent. + We generate amplitudes from frequencies through the experiment class. + These amplitudes are converted into frequencies again with the same coefficients. + The two sets of frequencies must be consistent. """ service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") From da444b6968d66f241aa16049486965c8bc028aea Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 17 Jan 2024 22:39:26 +0900 Subject: [PATCH 07/15] Rework - reorganize; move all experiments and analyses to own library.driven_freq_tuning - add util; define StarkCoefficients dataclasses and util functions - separation; delegated the role of coefficient manipulation from the analysis class to util functions --- docs/apidocs/index.rst | 1 + docs/apidocs/mod_driven_freq_tuning.rst | 6 + qiskit_experiments/__init__.py | 1 + qiskit_experiments/library/__init__.py | 13 +- .../library/characterization/__init__.py | 11 +- .../characterization/analysis/__init__.py | 4 +- .../analysis/ramsey_xy_analysis.py | 398 ----------- .../characterization/analysis/t1_analysis.py | 269 +------- .../library/characterization/ramsey_xy.py | 628 +----------------- .../library/characterization/t1.py | 349 +--------- .../library/driven_freq_tuning/__init__.py | 66 ++ .../library/driven_freq_tuning/analyses.py | 584 ++++++++++++++++ .../driven_freq_tuning/coefficient_utils.py | 234 +++++++ .../library/driven_freq_tuning/p1_spect.py | 270 ++++++++ .../library/driven_freq_tuning/ramsey.py | 359 ++++++++++ .../driven_freq_tuning/ramsey_amp_scan.py | 311 +++++++++ test/library/driven_freq_tuning/__init__.py | 12 + .../test_stark_p1_spect.py | 263 +++----- .../test_stark_ramsey_xy.py | 66 +- test/library/driven_freq_tuning/test_utils.py | 161 +++++ 20 files changed, 2136 insertions(+), 1870 deletions(-) create mode 100644 docs/apidocs/mod_driven_freq_tuning.rst create mode 100644 qiskit_experiments/library/driven_freq_tuning/__init__.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/analyses.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/p1_spect.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/ramsey.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py create mode 100644 test/library/driven_freq_tuning/__init__.py rename test/library/{characterization => driven_freq_tuning}/test_stark_p1_spect.py (53%) rename test/library/{characterization => driven_freq_tuning}/test_stark_ramsey_xy.py (84%) create mode 100644 test/library/driven_freq_tuning/test_utils.py diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst index 01efc9c174..fac3299b4d 100644 --- a/docs/apidocs/index.rst +++ b/docs/apidocs/index.rst @@ -30,6 +30,7 @@ Experiment Modules mod_calibration mod_characterization + mod_driven_freq_tuning mod_randomized_benchmarking mod_tomography mod_quantum_volume diff --git a/docs/apidocs/mod_driven_freq_tuning.rst b/docs/apidocs/mod_driven_freq_tuning.rst new file mode 100644 index 0000000000..bdc7fa1462 --- /dev/null +++ b/docs/apidocs/mod_driven_freq_tuning.rst @@ -0,0 +1,6 @@ +.. _qiskit-experiments-driven-freq-tuning: + +.. automodule:: qiskit_experiments.library.driven_freq_tuning + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index e9c613b259..32532928b8 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -49,6 +49,7 @@ - :mod:`qiskit_experiments.library.calibration` - :mod:`qiskit_experiments.library.characterization` +- :mod:`qiskit_experiments.library.driven_freq_tuning` - :mod:`qiskit_experiments.library.randomized_benchmarking` - :mod:`qiskit_experiments.library.tomography` """ diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 6737402863..29770ed7b3 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -76,8 +76,9 @@ ~characterization.FineXDrag ~characterization.FineSXDrag ~characterization.MultiStateDiscrimination - ~characterization.StarkRamseyXY - ~characterization.StarkRamseyXYAmpScan + ~driven_freq_tuning.StarkRamseyXY + ~driven_freq_tuning.StarkRamseyXYAmpScan + ~driven_freq_tuning.StarkP1Spectroscopy .. _characterization two qubits: @@ -160,7 +161,6 @@ class instance to manage parameters and pulse schedules. ) from .characterization import ( T1, - StarkP1Spectroscopy, T2Hahn, T2Ramsey, Tphi, @@ -187,8 +187,6 @@ class instance to manage parameters and pulse schedules. CorrelatedReadoutError, ZZRamsey, MultiStateDiscrimination, - StarkRamseyXY, - StarkRamseyXYAmpScan, ) from .randomized_benchmarking import StandardRB, InterleavedRB from .tomography import ( @@ -199,6 +197,11 @@ class instance to manage parameters and pulse schedules. MitigatedProcessTomography, ) from .quantum_volume import QuantumVolume +from .driven_freq_tuning import ( + StarkRamseyXY, + StarkRamseyXYAmpScan, + StarkP1Spectroscopy, +) # Experiment Sub-modules from . import calibration diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index 8aaaceee4c..daa29bb4a4 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -24,7 +24,6 @@ :template: autosummary/experiment.rst T1 - StarkP1Spectroscopy T2Ramsey T2Hahn Tphi @@ -50,8 +49,6 @@ ResonatorSpectroscopy MultiStateDiscrimination ZZRamsey - StarkRamseyXY - StarkRamseyXYAmpScan Analysis @@ -63,7 +60,6 @@ T1Analysis T1KerneledAnalysis - StarkP1SpectAnalysis T2RamseyAnalysis T2HahnAnalysis TphiAnalysis @@ -71,7 +67,6 @@ DragCalAnalysis FineAmplitudeAnalysis RamseyXYAnalysis - StarkRamseyXYAmpScanAnalysis ReadoutAngleAnalysis ResonatorSpectroscopyAnalysis LocalReadoutErrorAnalysis @@ -85,8 +80,6 @@ DragCalAnalysis, FineAmplitudeAnalysis, RamseyXYAnalysis, - StarkRamseyXYAmpScanAnalysis, - StarkP1SpectAnalysis, T2RamseyAnalysis, T1Analysis, T1KerneledAnalysis, @@ -101,7 +94,7 @@ MultiStateDiscriminationAnalysis, ) -from .t1 import T1, StarkP1Spectroscopy +from .t1 import T1 from .qubit_spectroscopy import QubitSpectroscopy from .ef_spectroscopy import EFSpectroscopy from .t2ramsey import T2Ramsey @@ -111,7 +104,7 @@ from .rabi import Rabi, EFRabi from .half_angle import HalfAngle from .fine_amplitude import FineAmplitude, FineXAmplitude, FineSXAmplitude, FineZXAmplitude -from .ramsey_xy import RamseyXY, StarkRamseyXY, StarkRamseyXYAmpScan +from .ramsey_xy import RamseyXY from .fine_frequency import FineFrequency from .drag import RoughDrag from .readout_angle import ReadoutAngle diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index 8520060772..f249dcd9be 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -14,10 +14,10 @@ from .drag_analysis import DragCalAnalysis from .fine_amplitude_analysis import FineAmplitudeAnalysis -from .ramsey_xy_analysis import RamseyXYAnalysis, StarkRamseyXYAmpScanAnalysis +from .ramsey_xy_analysis import RamseyXYAnalysis from .t2ramsey_analysis import T2RamseyAnalysis from .t2hahn_analysis import T2HahnAnalysis -from .t1_analysis import T1Analysis, T1KerneledAnalysis, StarkP1SpectAnalysis +from .t1_analysis import T1Analysis, T1KerneledAnalysis from .tphi_analysis import TphiAnalysis from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis from .readout_angle_analysis import ReadoutAngleAnalysis diff --git a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py index 1a0ac92837..27a588a550 100644 --- a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py @@ -16,11 +16,8 @@ import lmfit import numpy as np -from uncertainties import unumpy as unp import qiskit_experiments.curve_analysis as curve -import qiskit_experiments.visualization as vis -from qiskit_experiments.framework import ExperimentData class RamseyXYAnalysis(curve.CurveAnalysis): @@ -209,398 +206,3 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: return "good" return "bad" - - -class StarkRamseyXYAmpScanAnalysis(curve.CurveAnalysis): - r"""Ramsey XY analysis for the Stark shifted phase sweep. - - # section: overview - - This analysis is a variant of :class:`RamseyXYAnalysis`. In both cases, the X and Y - data are treated as the real and imaginary parts of a complex oscillating signal. - In :class:`RamseyXYAnalysis`, the data are fit assuming a phase varying linearly with - the x-data corresponding to a constant frequency and assuming an exponentially - decaying amplitude. By contrast, in this model, the phase is assumed to be - a third order polynomial :math:`\theta(x)` of the x-data. - Additionally, the amplitude is not assumed to follow a specific form. - Techniques to compute a good initial guess for the polynomial coefficients inside - a trigonometric function like this are not trivial. Instead, this analysis extracts the - raw phase and runs fits on the extracted data to a polynomial :math:`\theta(x)` directly. - - The measured P1 values for a Ramsey X and Y experiment can be written in the form of - a trignometric function taking the phase polynomial :math:`\theta(x)`: - - .. math:: - - P_X = \text{amp}(x) \cdot \cos \theta(x) + \text{offset},\\ - P_Y = \text{amp}(x) \cdot \sin \theta(x) + \text{offset}. - - Hence the phase polynomial can be extracted as follows - - .. math:: - - \theta(x) = \tan^{-1} \frac{P_Y}{P_X}. - - Because the arctangent is implemented by the ``atan2`` function - defined in :math:`[-\pi, \pi]`, the computed :math:`\theta(x)` is unwrapped to - ensure continuous phase evolution. - - We call attention to the fact that :math:`\text{amp}(x)` is also Stark tone amplitude - dependent because of the qubit frequency dependence of the dephasing rate. - In general :math:`\text{amp}(x)` is unpredictable due to dephasing from - two-level systems distributed randomly in frequency - or potentially due to qubit heating. This prevents us from precisely fitting - the raw :math:`P_X`, :math:`P_Y` data. Fitting only the phase data makes the - analysis robust to amplitude dependent dephasing. - - In this analysis, the phase polynomial is defined as - - .. math:: - - \theta(x) = 2 \pi t_S f_S(x) - - where - - .. math:: - - f_S(x) = c_1 x + c_2 x^2 + c_3 x^3 + f_{\rm err}, - - denotes the Stark shift. For the lowest order perturbative expansion of a single driven qubit, - the Stark shift is a quadratic function of :math:`x`, but linear and cubic terms - and a constant offset are also considered to account for - other effects, e.g. strong drive, collisions, TLS, and so forth, - and frequency mis-calibration, respectively. - - # section: fit_model - - .. math:: - - \theta^\nu(x) = c_1^\nu x + c_2^\nu x^2 + c_3^\nu x^3 + f_{\rm err}, - - where :math:`\nu \in \{+, -\}`. - The Stark shift is asymmetric with respect to :math:`x=0`, because of the - anti-crossings of higher energy levels. In a typical transmon qubit, - these levels appear only in :math:`f_S < 0` because of the negative anharmonicity. - To precisely fit the results, this analysis uses different model parameters - for positive (:math:`x > 0`) and negative (:math:`x < 0`) shift domains. - - # section: fit_parameters - - defpar c_1^+: - desc: The linear term coefficient of the positive Stark shift - (fit parameter: ``stark_pos_coef_o1``). - init_guess: 0. - bounds: None - - defpar c_2^+: - desc: The quadratic term coefficient of the positive Stark shift. - This parameter must be positive because Stark amplitude is chosen to - induce blue shift when its sign is positive. - Note that the quadratic term is the primary term - (fit parameter: ``stark_pos_coef_o2``). - init_guess: 1e6. - bounds: [0, inf] - - defpar c_3^+: - desc: The cubic term coefficient of the positive Stark shift - (fit parameter: ``stark_pos_coef_o3``). - init_guess: 0. - bounds: None - - defpar c_1^-: - desc: The linear term coefficient of the negative Stark shift. - (fit parameter: ``stark_neg_coef_o1``). - init_guess: 0. - bounds: None - - defpar c_2^-: - desc: The quadratic term coefficient of the negative Stark shift. - This parameter must be negative because Stark amplitude is chosen to - induce red shift when its sign is negative. - Note that the quadratic term is the primary term - (fit parameter: ``stark_neg_coef_o2``). - init_guess: -1e6. - bounds: [-inf, 0] - - defpar c_3^-: - desc: The cubic term coefficient of the negative Stark shift - (fit parameter: ``stark_neg_coef_o3``). - init_guess: 0. - bounds: None - - defpar f_{\rm err}: - desc: Constant phase accumulation which is independent of the Stark tone amplitude. - (fit parameter: ``stark_ferr``). - init_guess: 0 - bounds: None - - # section: see_also - - :class:`qiskit_experiments.library.characterization.analysis.ramsey_xy_analysis.RamseyXYAnalysis` - - """ - - def __init__(self): - - models = [ - lmfit.models.ExpressionModel( - expr="c1_pos * x + c2_pos * x**2 + c3_pos * x**3 + f_err", - name="FREQpos", - ), - lmfit.models.ExpressionModel( - expr="c1_neg * x + c2_neg * x**2 + c3_neg * x**3 + f_err", - name="FREQneg", - ), - ] - super().__init__(models=models) - - @classmethod - def _default_options(cls): - """Default analysis options. - - Analysis Options: - pulse_len (float): Duration of effective Stark pulse in units of sec. - """ - ramsey_plotter = vis.CurvePlotter(vis.MplDrawer()) - ramsey_plotter.set_figure_options( - xlabel="Stark tone amplitude", - ylabel=["Stark shift", "P1"], - yval_unit=["Hz", None], - series_params={ - "Fpos": { - "color": "#123FE8", - "symbol": "^", - "label": "", - "canvas": 0, - }, - "Fneg": { - "color": "#123FE8", - "symbol": "v", - "label": "", - "canvas": 0, - }, - "Xpos": { - "color": "#123FE8", - "symbol": "o", - "label": "Ramsey X", - "canvas": 1, - }, - "Ypos": { - "color": "#6312E8", - "symbol": "^", - "label": "Ramsey Y", - "canvas": 1, - }, - "Xneg": { - "color": "#E83812", - "symbol": "o", - "label": "Ramsey X", - "canvas": 1, - }, - "Yneg": { - "color": "#E89012", - "symbol": "^", - "label": "Ramsey Y", - "canvas": 1, - }, - }, - sharey=False, - ) - ramsey_plotter.set_options(subplots=(2, 1), style=vis.PlotStyle({"figsize": (10, 8)})) - - options = super()._default_options() - options.update_options( - data_subfit_map={ - "Xpos": {"series": "X", "direction": "pos"}, - "Ypos": {"series": "Y", "direction": "pos"}, - "Xneg": {"series": "X", "direction": "neg"}, - "Yneg": {"series": "Y", "direction": "neg"}, - }, - result_parameters=[ - curve.ParameterRepr("c1_pos", "stark_pos_coef_o1", "Hz"), - curve.ParameterRepr("c2_pos", "stark_pos_coef_o2", "Hz"), - curve.ParameterRepr("c3_pos", "stark_pos_coef_o3", "Hz"), - curve.ParameterRepr("c1_neg", "stark_neg_coef_o1", "Hz"), - curve.ParameterRepr("c2_neg", "stark_neg_coef_o2", "Hz"), - curve.ParameterRepr("c3_neg", "stark_neg_coef_o3", "Hz"), - curve.ParameterRepr("f_err", "stark_ferr", "Hz"), - ], - plotter=ramsey_plotter, - fit_category="freq", - pulse_len=None, - ) - - return options - - def _freq_phase_coef(self) -> float: - """Return a coefficient to convert frequency into phase value.""" - try: - return 2 * np.pi * self.options.pulse_len - except TypeError as ex: - raise TypeError( - "A float-value duration in units of sec of the Stark pulse must be provided. " - f"The pulse_len option value {self.options.pulse_len} is not valid." - ) from ex - - def _format_data( - self, - curve_data: curve.ScatterTable, - category: str = "freq", - ) -> curve.ScatterTable: - - curve_data = super()._format_data(curve_data, category="ramsey_xy") - ramsey_xy = curve_data[curve_data.category == "ramsey_xy"] - - # Create phase data by arctan(Y/X) - columns = list(curve_data.columns) - phase_data = np.empty((0, len(columns))) - y_mean = ramsey_xy.yval.mean() - - grouped = ramsey_xy.groupby("name") - for m_id, direction in enumerate(("pos", "neg")): - x_quadrature = grouped.get_group(f"X{direction}") - y_quadrature = grouped.get_group(f"Y{direction}") - if not np.array_equal(x_quadrature.xval, y_quadrature.xval): - raise ValueError( - "Amplitude values of X and Y quadrature are different. " - "Same values must be used." - ) - x_uarray = unp.uarray(x_quadrature.yval, x_quadrature.yerr) - y_uarray = unp.uarray(y_quadrature.yval, y_quadrature.yerr) - - amplitudes = x_quadrature.xval.to_numpy() - - # pylint: disable=no-member - phase = unp.arctan2(y_uarray - y_mean, x_uarray - y_mean) - phase_n = unp.nominal_values(phase) - phase_s = unp.std_devs(phase) - - # Unwrap phase - # We assume a smooth slope and correct 2pi phase jump to minimize the change of the slope. - unwrapped_phase = np.unwrap(phase_n) - if amplitudes[0] < 0: - # Preserve phase value closest to 0 amplitude - unwrapped_phase = unwrapped_phase + (phase_n[-1] - unwrapped_phase[-1]) - - # Store new data - tmp = np.empty((len(amplitudes), len(columns)), dtype=object) - tmp[:, columns.index("xval")] = amplitudes - tmp[:, columns.index("yval")] = unwrapped_phase / self._freq_phase_coef() - tmp[:, columns.index("yerr")] = phase_s / self._freq_phase_coef() - tmp[:, columns.index("name")] = f"FREQ{direction}" - tmp[:, columns.index("class_id")] = m_id - tmp[:, columns.index("shots")] = x_quadrature.shots + y_quadrature.shots - tmp[:, columns.index("category")] = category - phase_data = np.r_[phase_data, tmp] - - return curve_data.append_list_values(other=phase_data) - - def _generate_fit_guesses( - self, - user_opt: curve.FitOptions, - curve_data: curve.ScatterTable, - ) -> Union[curve.FitOptions, List[curve.FitOptions]]: - """Create algorithmic initial fit guess from analysis options and curve data. - - Args: - user_opt: Fit options filled with user provided guess and bounds. - curve_data: Formatted data collection to fit. - - Returns: - List of fit options that are passed to the fitter function. - """ - user_opt.bounds.set_if_empty(c2_pos=(0, np.inf), c2_neg=(-np.inf, 0)) - user_opt.p0.set_if_empty( - c1_pos=0, c2_pos=1e6, c3_pos=0, c1_neg=0, c2_neg=-1e6, c3_neg=0, f_err=0 - ) - return user_opt - - def _create_figures( - self, - curve_data: curve.ScatterTable, - ) -> List["matplotlib.figure.Figure"]: - - # plot unwrapped phase on first axis - for d in ("pos", "neg"): - sub_data = curve_data[(curve_data.name == f"FREQ{d}") & (curve_data.category == "freq")] - self.plotter.set_series_data( - series_name=f"F{d}", - x_formatted=sub_data.xval.to_numpy(), - y_formatted=sub_data.yval.to_numpy(), - y_formatted_err=sub_data.yerr.to_numpy(), - ) - - # plot raw RamseyXY plot on second axis - for name in ("Xpos", "Ypos", "Xneg", "Yneg"): - sub_data = curve_data[(curve_data.name == name) & (curve_data.category == "ramsey_xy")] - self.plotter.set_series_data( - series_name=name, - x_formatted=sub_data.xval.to_numpy(), - y_formatted=sub_data.yval.to_numpy(), - y_formatted_err=sub_data.yerr.to_numpy(), - ) - - # find base and amplitude guess - ramsey_xy = curve_data[curve_data.category == "ramsey_xy"] - offset_guess = 0.5 * (ramsey_xy.yval.min() + ramsey_xy.yval.max()) - amp_guess = 0.5 * np.ptp(ramsey_xy.yval) - - # plot frequency and Ramsey fit lines - line_data = curve_data[curve_data.category == "fitted"] - for direction in ("pos", "neg"): - sub_data = line_data[line_data.name == f"FREQ{direction}"] - if len(sub_data) == 0: - continue - xval = sub_data.xval.to_numpy() - yn = sub_data.yval.to_numpy() - ys = sub_data.yerr.to_numpy() - yval = unp.uarray(yn, ys) * self._freq_phase_coef() - - # Ramsey fit lines are predicted from the phase fit line. - # Note that this line doesn't need to match with the expeirment data - # because Ramsey P1 data may fluctuate due to phase damping. - - # pylint: disable=no-member - ramsey_cos = amp_guess * unp.cos(yval) + offset_guess - ramsey_sin = amp_guess * unp.sin(yval) + offset_guess - - self.plotter.set_series_data( - series_name=f"F{direction}", - x_interp=xval, - y_interp=yn, - ) - self.plotter.set_series_data( - series_name=f"X{direction}", - x_interp=xval, - y_interp=unp.nominal_values(ramsey_cos), - ) - self.plotter.set_series_data( - series_name=f"Y{direction}", - x_interp=xval, - y_interp=unp.nominal_values(ramsey_sin), - ) - - if np.isfinite(ys).all(): - self.plotter.set_series_data( - series_name=f"F{direction}", - y_interp_err=ys, - ) - self.plotter.set_series_data( - series_name=f"X{direction}", - y_interp_err=unp.std_devs(ramsey_cos), - ) - self.plotter.set_series_data( - series_name=f"Y{direction}", - y_interp_err=unp.std_devs(ramsey_sin), - ) - return [self.plotter.figure()] - - def _initialize( - self, - experiment_data: ExperimentData, - ): - super()._initialize(experiment_data) - - # Set scaling factor to convert phase to frequency - if "stark_length" in experiment_data.metadata: - self.set_options(pulse_len=experiment_data.metadata["stark_length"]) diff --git a/qiskit_experiments/library/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index e5887704a5..9ef0ed3bc3 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -12,19 +12,12 @@ """ T1 Analysis class. """ -from typing import Union, Tuple, List, Dict +from typing import Union import numpy as np -from qiskit_ibm_experiment import IBMExperimentService -from qiskit_ibm_experiment.exceptions import IBMApiError -from uncertainties import unumpy as unp import qiskit_experiments.curve_analysis as curve -import qiskit_experiments.data_processing as dp -import qiskit_experiments.visualization as vis -from qiskit_experiments.data_processing.exceptions import DataProcessorError -from qiskit_experiments.database_service.device_component import Qubit -from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options +from qiskit_experiments.framework import Options class T1Analysis(curve.DecayAnalysis): @@ -139,261 +132,3 @@ def _format_data( if avg_slope > 0: curve_data.yval = 1 - curve_data.yval return super()._format_data(curve_data) - - -class StarkP1SpectAnalysis(BaseAnalysis): - """Analysis class for StarkP1Spectroscopy. - - # section: overview - - The P1 landscape is hardly predictable because of the random appearance of - lossy TLS notches, and hence this analysis doesn't provide any - generic mathematical model to fit the measurement data. - A developer may subclass this to conduct own analysis. - - This analysis just visualizes the measured P1 values against Stark tone amplitudes. - The tone amplitudes can be converted into the amount of Stark shift - when the calibrated coefficients are provided in the analysis option, - or the calibration experiment results are available in the result database. - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXYAmpScan` - - """ - - stark_coefficients_names = [ - "stark_pos_coef_o1", - "stark_pos_coef_o2", - "stark_pos_coef_o3", - "stark_neg_coef_o1", - "stark_neg_coef_o2", - "stark_neg_coef_o3", - "stark_ferr", - ] - - @property - def plotter(self) -> vis.CurvePlotter: - """Curve plotter instance.""" - return self.options.plotter - - @classmethod - def _default_options(cls) -> Options: - """Default analysis options. - - Analysis Options: - plotter (Plotter): Plotter to visualize P1 landscape. - data_processor (DataProcessor): Data processor to compute P1 value. - stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to - convert tone amplitudes into amount of Stark shift. This dictionary must include - all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, - which are calibrated with :class:`.StarkRamseyXYAmpScan`. - Alternatively, it searches for these coefficients in the result database - when "latest" is set. This requires having the experiment service set in - the experiment data to analyze. - x_key (str): Key of the circuit metadata to represent x value. - """ - options = super()._default_options() - - p1spect_plotter = vis.CurvePlotter(vis.MplDrawer()) - p1spect_plotter.set_figure_options( - xlabel="Stark amplitude", - ylabel="P(1)", - xscale="quadratic", - ) - - options.update_options( - plotter=p1spect_plotter, - data_processor=dp.DataProcessor("counts", [dp.Probability("1")]), - stark_coefficients="latest", - x_key="xval", - ) - return options - - # pylint: disable=unused-argument - def _run_spect_analysis( - self, - xdata: np.ndarray, - ydata: np.ndarray, - ydata_err: np.ndarray, - ) -> List[AnalysisResultData]: - """Run further analysis on the spectroscopy data. - - .. note:: - A subclass can overwrite this method to conduct analysis. - - Args: - xdata: X values. This is either amplitudes or frequencies. - ydata: Y values. This is P1 values measured at different Stark tones. - ydata_err: Sampling error of the Y values. - - Returns: - A list of analysis results. - """ - return [] - - @classmethod - def retrieve_coefficients_from_service( - cls, - service: IBMExperimentService, - qubit: int, - backend: str, - ) -> Dict: - """Retrieve stark coefficient dictionary from the experiment service. - - Args: - service: A valid experiment service instance. - qubit: Qubit index. - backend: Name of the backend. - - Returns: - A dictionary of Stark coefficients to convert amplitude to frequency. - None value is returned when the dictionary is incomplete. - """ - out = {} - try: - for name in cls.stark_coefficients_names: - results = service.analysis_results( - device_components=[str(Qubit(qubit))], - result_type=name, - backend_name=backend, - sort_by=["creation_datetime:desc"], - ) - if len(results) == 0: - return None - result_data = getattr(results[0], "result_data") - out[name] = result_data["value"] - except (IBMApiError, ValueError, KeyError, AttributeError): - return None - return out - - @classmethod - def estimate_minmax_frequencies( - cls, - coefficients: Dict[str, float], - max_amplitudes: Tuple[float, float] = (-0.9, 0.9), - ) -> Tuple[float, float]: - """Inquire maximum and minimum Stark shift available within specified amplitude range. - - Args: - coefficients: A dictionary of Stark coefficients. - max_amplitudes: Minimum and maximum amplitude. - - Returns: - Tuple of minimum and maximum frequency. - - Raises: - KeyError: When coefficients are incomplete. - """ - missing = set(cls.stark_coefficients_names) - coefficients.keys() - if any(missing): - raise KeyError( - "Following coefficient data is missing in the " - f"stark 'coefficients' dictionary: {missing}." - ) - - names = cls.stark_coefficients_names # alias - pos_idxs = [2, 1, 0] - neg_idxs = [5, 4, 3] - - freqs = [] - for idxs, max_amp in zip((neg_idxs, pos_idxs), max_amplitudes): - # Solve for inflection points by computing the point where derivertive becomes zero. - solutions = np.roots( - [deriv * coefficients[names[idx]] for deriv, idx in zip([3, 2, 1], idxs)] - ) - inflection_points = solutions[ - (solutions.imag == 0) & (np.sign(solutions) == np.sign(max_amp)) - ] - if len(inflection_points) == 0: - amp = max_amp - else: - # When multiple inflection points are found, use the most outer one. - # There could be a small inflection point around amp=0, - # when the first order term is significant. - amp = min(max_amp, max(inflection_points, key=abs), key=abs) - polyfun = np.poly1d([coefficients[names[idx]] for idx in [*idxs, 6]]) - freqs.append(polyfun(amp)) - return tuple(freqs) - - def _convert_axis( - self, - xdata: np.ndarray, - coefficients: Dict[str, float], - ) -> np.ndarray: - """A helper method to convert x-axis. - - Args: - xdata: An array of Stark tone amplitude. - coefficients: Stark coefficients to convert amplitudes into frequencies. - - Returns: - An array of amount of Stark shift. - """ - names = self.stark_coefficients_names # alias - positive = np.poly1d([coefficients[names[idx]] for idx in [2, 1, 0, 6]]) - negative = np.poly1d([coefficients[names[idx]] for idx in [5, 4, 3, 6]]) - - new_xdata = np.where(xdata > 0, positive(xdata), negative(xdata)) - self.plotter.set_figure_options( - xlabel="Stark shift", - xval_unit="Hz", - xscale="linear", - ) - return new_xdata - - def _run_analysis( - self, - experiment_data: ExperimentData, - ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: - - x_key = self.options.x_key - - # Get calibrated Stark tone coefficients - if self.options.stark_coefficients == "latest" and experiment_data.service is not None: - # Get value from service - stark_coeffs = self.retrieve_coefficients_from_service( - service=experiment_data.service, - qubit=experiment_data.metadata["physical_qubits"][0], - backend=experiment_data.backend_name, - ) - elif isinstance(self.options.stark_coefficients, dict): - # Get value from experiment options - missing = set(self.stark_coefficients_names) - self.options.stark_coefficients.keys() - if any(missing): - raise KeyError( - "Following coefficient data is missing in the " - f"'stark_coefficients' dictionary: {missing}." - ) - stark_coeffs = self.options.stark_coefficients - else: - # No calibration is available - stark_coeffs = None - - # Compute P1 value and sampling error - data = experiment_data.data() - try: - xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float) - except KeyError as ex: - raise DataProcessorError( - f"X value key {x_key} is not defined in circuit metadata." - ) from ex - ydata_ufloat = self.options.data_processor(data) - ydata = unp.nominal_values(ydata_ufloat) - ydata_err = unp.std_devs(ydata_ufloat) - - # Convert x-axis of amplitudes into Stark shift by consuming calibrated parameters. - if stark_coeffs: - xdata = self._convert_axis(xdata, stark_coeffs) - - # Draw figures and create analysis results. - self.plotter.set_series_data( - series_name="stark_p1", - x_formatted=xdata, - y_formatted=ydata, - y_formatted_err=ydata_err, - x_interp=xdata, - y_interp=ydata, - ) - analysis_results = self._run_spect_analysis(xdata, ydata, ydata_err) - - return analysis_results, [self.plotter.figure()] diff --git a/qiskit_experiments/library/characterization/ramsey_xy.py b/qiskit_experiments/library/characterization/ramsey_xy.py index 1d0238b652..ccb1987481 100644 --- a/qiskit_experiments/library/characterization/ramsey_xy.py +++ b/qiskit_experiments/library/characterization/ramsey_xy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2023. +# (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 @@ -11,27 +11,16 @@ # that they have been altered from the originals. """Ramsey XY frequency characterization experiment.""" -import warnings -from typing import List, Tuple, Dict, Optional, Sequence +from typing import List, Optional, Sequence import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, ParameterExpression, Parameter +from qiskit.circuit import QuantumCircuit, Parameter from qiskit.providers.backend import Backend from qiskit.qobj.utils import MeasLevel -from qiskit.utils import optionals as _optional from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming from qiskit_experiments.framework.restless_mixin import RestlessMixin -from qiskit_experiments.library.characterization.analysis import ( - RamseyXYAnalysis, - StarkRamseyXYAmpScanAnalysis, -) - -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym +from qiskit_experiments.library.characterization.analysis import RamseyXYAnalysis class RamseyXY(BaseExperiment, RestlessMixin): @@ -208,612 +197,3 @@ def _metadata(self): if hasattr(self.run_options, run_opt): metadata[run_opt] = getattr(self.run_options, run_opt) return metadata - - -class StarkRamseyXY(BaseExperiment): - """A sign-sensitive experiment to measure the frequency of a qubit under a pulsed Stark tone. - - # section: overview - - This experiment is a variant of :class:`.RamseyXY` with a pulsed Stark tone - and consists of the following two circuits: - - .. parsed-literal:: - - (Ramsey X) The pulse before measurement rotates by pi-half around the X axis - - ┌────┐┌────────┐┌───┐┌───────────────┐┌────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV ├┤ X ├┤ StarkU(delay) ├┤ Rz(-π) ├┤ √X ├┤M├ - └────┘└────────┘└───┘└───────────────┘└────────┘└────┘└╥┘ - c: 1/═══════════════════════════════════════════════════════╩═ - 0 - - (Ramsey Y) The pulse before measurement rotates by pi-half around the Y axis - - ┌────┐┌────────┐┌───┐┌───────────────┐┌───────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV ├┤ X ├┤ StarkU(delay) ├┤ Rz(-3π/2) ├┤ √X ├┤M├ - └────┘└────────┘└───┘└───────────────┘└───────────┘└────┘└╥┘ - c: 1/══════════════════════════════════════════════════════════╩═ - 0 - - In principle, the sequence is a variant of :class:`.RamseyXY` circuit. - However, the delay in between √X gates is replaced with an off-resonant drive. - This off-resonant drive shifts the qubit frequency due to the - Stark effect and causes it to accumulate phase during the - Ramsey sequence. This frequency shift is a function of the - offset of the Stark tone frequency from the qubit frequency - and of the magnitude of the tone. - - Note that the Stark tone pulse (StarkU) takes the form of a flat-topped Gaussian envelope. - The magnitude of the pulse varies in time during its rising and falling edges. - It is difficult to characterize the net phase accumulation of the qubit during the - edges of the pulse when the frequency shift is varying with the pulse amplitude. - In order to simplify the analysis, an additional pulse (StarkV) - involving only the edges of StarkU is added to the sequence. - The sign of the phase accumulation is inverted for StarkV relative to that of StarkU - by inserting an X gate in between the two pulses. - - This technique allows the experiment to accumulate only the net phase - during the flat-top part of the StarkU pulse with constant magnitude. - - # section: analysis_ref - :py:class:`RamseyXYAnalysis` - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = None, - **experiment_options, - ): - """Create new experiment. - - Args: - physical_qubits: Index of physical qubit. - backend: Optional, the backend to run the experiment on. - experiment_options: Experiment options. See the class documentation or - ``self._default_experiment_options`` for descriptions. - """ - self._timing = None - - super().__init__( - physical_qubits=physical_qubits, - analysis=RamseyXYAnalysis(), - backend=backend, - ) - self.set_experiment_options(**experiment_options) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - stark_amp (float): A single float parameter to represent the magnitude of Stark tone - and the sign of expected Stark shift. - See :ref:`stark_tone_implementation` for details. - stark_channel (PulseChannel): Pulse channel to apply Stark tones. - If not provided, the same channel with the qubit drive is used. - See :ref:`stark_channel_consideration` for details. - stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. - This offset should be sufficiently large so that the Stark pulse - does not Rabi drive the qubit. - See :ref:`stark_frequency_consideration` for details. - stark_sigma (float): Gaussian sigma of the rising and falling edges - of the Stark tone, in seconds. - stark_risefall (float): Ratio of sigma to the duration of - the rising and falling edges of the Stark tone. - min_freq (float): Minimum frequency that this experiment is guaranteed to resolve. - Note that fitter algorithm :class:`.RamseyXYAnalysis` of this experiment - is still capable of fitting experiment data with lower frequency. - max_freq (float): Maximum frequency that this experiment can resolve. - delays (list[float]): The list of delays if set that will be scanned in the - experiment. If not set, then evenly spaced delays with interval - computed from ``min_freq`` and ``max_freq`` are used. - See :meth:`StarkRamseyXY.delays` for details. - """ - options = super()._default_experiment_options() - options.update_options( - stark_amp=0.0, - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - min_freq=5e6, - max_freq=100e6, - delays=None, - ) - options.set_validator("stark_freq_offset", (0, np.inf)) - options.set_validator("stark_channel", pulse.channels.PulseChannel) - return options - - def _set_backend(self, backend: Backend): - super()._set_backend(backend) - self._timing = BackendTiming(backend) - - def set_experiment_options(self, **fields): - _warning_circuit_length = 300 - - # Do validation for circuit number - min_freq = fields.get("min_freq", None) - max_freq = fields.get("max_freq", None) - delays = fields.get("delays", None) - if min_freq is not None and max_freq is not None: - if delays: - warnings.warn( - "Experiment option 'min_freq' and 'max_freq' are ignored " - "when 'delays' are explicitly specified.", - UserWarning, - ) - else: - n_expr_circs = 2 * int(2 * max_freq / min_freq) # delays * (x, y) - max_circs_per_job = None - if self._backend_data: - max_circs_per_job = self._backend_data.max_circuits() - if n_expr_circs > (max_circs_per_job or _warning_circuit_length): - warnings.warn( - f"Provided configuration generates {n_expr_circs} circuits. " - "You can set smaller 'max_freq' or larger 'min_freq' to reduce this number. " - "This experiment is still executable but your execution may consume " - "unnecessary long quantum device time, and result may suffer " - "device parameter drift in consequence of the long execution time.", - UserWarning, - ) - # Do validation for spectrum overlap to avoid real excitation - stark_freq_offset = fields.get("stark_freq_offset", None) - stark_sigma = fields.get("stark_sigma", None) - if stark_freq_offset is not None and stark_sigma is not None: - if stark_freq_offset < 1 / stark_sigma: - warnings.warn( - "Provided configuration may induce coherent state exchange between qubit levels " - "because of the potential spectrum overlap. You can avoid this by " - "increasing the 'stark_sigma' or 'stark_freq_offset'. " - "Note that this experiment is still executable.", - UserWarning, - ) - pass - - super().set_experiment_options(**fields) - - def parameters(self) -> np.ndarray: - """Delay values to use in circuits. - - .. note:: - - The delays are computed with the ``min_freq`` and ``max_freq`` experiment options. - The maximum point is computed from the ``min_freq`` to guarantee the result - contains at least one Ramsey oscillation cycle at this frequency. - The interval is computed from the ``max_freq`` to sample with resolution - such that the Nyquist frequency is twice ``max_freq``. - - Returns: - The list of delays to use for the different circuits based on the - experiment options. - - Raises: - ValueError: When ``min_freq`` is larger than ``max_freq``. - """ - opt = self.experiment_options # alias - - if opt.delays is None: - if opt.min_freq > opt.max_freq: - raise ValueError("Experiment option 'min_freq' must be smaller than 'max_freq'.") - # Delay is longer enough to capture 1 cycle of the minimum frequency. - # Fitter can still accurately fit samples shorter than 1 cycle. - max_period = 1 / opt.min_freq - # Inverse of interval should be greater than Nyquist frequency. - sampling_freq = 2 * opt.max_freq - interval = 1 / sampling_freq - return np.arange(0, max_period, interval) - return opt.delays - - def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: - """Create circuits with parameters for Ramsey XY experiment with Stark tone. - - Returns: - Quantum template circuits for Ramsey X and Ramsey Y experiment. - """ - opt = self.experiment_options # alias - param = Parameter("delay") - - # Pulse gates - stark_v = Gate("StarkV", 1, []) - stark_u = Gate("StarkU", 1, [param]) - - # Note that Stark tone yields negative (positive) frequency shift - # when the Stark tone frequency is higher (lower) than qubit f01 frequency. - # This choice gives positive frequency shift with positive Stark amplitude. - qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] - stark_freq = qubit_f01 - np.sign(opt.stark_amp) * opt.stark_freq_offset - stark_amp = np.abs(opt.stark_amp) - stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) - ramps_dt = self._timing.round_pulse(time=2 * opt.stark_risefall * opt.stark_sigma) - sigma_dt = ramps_dt / 2 / opt.stark_risefall - - with pulse.build() as stark_v_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.Gaussian( - duration=ramps_dt, - amp=stark_amp, - sigma=sigma_dt, - name="StarkV", - ), - stark_channel, - ) - - with pulse.build() as stark_u_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.GaussianSquare( - duration=ramps_dt + param, - amp=stark_amp, - sigma=sigma_dt, - risefall_sigma_ratio=opt.stark_risefall, - name="StarkU", - ), - stark_channel, - ) - - ram_x = QuantumCircuit(1, 1) - ram_x.sx(0) - ram_x.append(stark_v, [0]) - ram_x.x(0) - ram_x.append(stark_u, [0]) - ram_x.rz(-np.pi, 0) - ram_x.sx(0) - ram_x.measure(0, 0) - ram_x.metadata = {"series": "X"} - ram_x.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_x.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - ram_y = QuantumCircuit(1, 1) - ram_y.sx(0) - ram_y.append(stark_v, [0]) - ram_y.x(0) - ram_y.append(stark_u, [0]) - ram_y.rz(-np.pi * 3 / 2, 0) - ram_y.sx(0) - ram_y.measure(0, 0) - ram_y.metadata = {"series": "Y"} - ram_y.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_y.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - return ram_x, ram_y - - def circuits(self) -> List[QuantumCircuit]: - """Create circuits. - - Returns: - A list of circuits with a variable delay. - """ - timing = BackendTiming(self.backend, min_length=0) - - ramx_circ, ramy_circ = self.parameterized_circuits() - param = next(iter(ramx_circ.parameters)) - - circs = [] - for delay in self.parameters(): - valid_delay_dt = timing.round_pulse(time=delay) - net_delay_sec = timing.pulse_time(time=delay) - - ramx_circ_assigned = ramx_circ.assign_parameters({param: valid_delay_dt}, inplace=False) - ramx_circ_assigned.metadata["xval"] = net_delay_sec - - ramy_circ_assigned = ramy_circ.assign_parameters({param: valid_delay_dt}, inplace=False) - ramy_circ_assigned.metadata["xval"] = net_delay_sec - - circs.extend([ramx_circ_assigned, ramy_circ_assigned]) - - return circs - - def _metadata(self) -> Dict[str, any]: - """Return experiment metadata for ExperimentData.""" - metadata = super()._metadata() - metadata["stark_amp"] = self.experiment_options.stark_amp - metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset - - return metadata - - -class StarkRamseyXYAmpScan(BaseExperiment): - r"""A fast characterization of Stark frequency shift by varying Stark tone amplitude. - - # section: overview - - This experiment scans Stark tone amplitude at a fixed tone duration. - The experimental circuits are identical to the :class:`.StarkRamseyXY` experiment - except that the Stark pulse amplitude is the scanned parameter rather than the pulse width. - - .. parsed-literal:: - - (Ramsey X) The pulse before measurement rotates by pi-half around the X axis - - ┌────┐┌───────────────────┐┌───┐┌───────────────────┐┌────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV(stark_amp) ├┤ X ├┤ StarkU(stark_amp) ├┤ Rz(-π) ├┤ √X ├┤M├ - └────┘└───────────────────┘└───┘└───────────────────┘└────────┘└────┘└╥┘ - c: 1/══════════════════════════════════════════════════════════════════════╩═ - 0 - - (Ramsey Y) The pulse before measurement rotates by pi-half around the Y axis - - ┌────┐┌───────────────────┐┌───┐┌───────────────────┐┌───────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV(stark_amp) ├┤ X ├┤ StarkU(stark_amp) ├┤ Rz(-3π/2) ├┤ √X ├┤M├ - └────┘└───────────────────┘└───┘└───────────────────┘└───────────┘└────┘└╥┘ - c: 1/═════════════════════════════════════════════════════════════════════════╩═ - 0 - - The AC Stark effect can be used to shift the frequency of a qubit with a microwave drive. - To calibrate a specific frequency shift, the :class:`.StarkRamseyXY` experiment can be run - to scan the Stark pulse duration at every amplitude, but such a two dimensional scan of - the tone duration and amplitude may require many circuit executions. - To avoid this overhead, the :class:`.StarkRamseyXYAmpScan` experiment fixes the - tone duration and scans only amplitude. - - Recall that an observed Ramsey oscillation in each quadrature may be represented by - - .. math:: - - {\cal E}_X(\Omega, t_S) = A e^{-t_S/\tau} \cos \left( 2\pi f_S(\Omega) t_S \right), \\ - {\cal E}_Y(\Omega, t_S) = A e^{-t_S/\tau} \sin \left( 2\pi f_S(\Omega) t_S \right), - - where :math:`f_S(\Omega)` denotes the amount of Stark shift - at a constant tone amplitude :math:`\Omega`, and :math:`t_S` is the duration of the - applied tone. For a fixed tone duration, - one can still observe the Ramsey oscillation by scanning the tone amplitude. - However, since :math:`f_S` is usually a higher order polynomial of :math:`\Omega`, - one must manage to fit the y-data for trigonometric functions with - phase which non-linearly changes with the x-data. - The :class:`.StarkRamseyXYAmpScan` experiment thus drastically reduces the number of - circuits to run in return for greater complexity in the fitting model. - - # section: analysis_ref - :py:class:`StarkRamseyXYAmpScanAnalysis` - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXY` - :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = None, - **experiment_options, - ): - """Create new experiment. - - Args: - physical_qubits: Sequence with the index of the physical qubit. - backend: Optional, the backend to run the experiment on. - experiment_options: Experiment options. See the class documentation or - ``self._default_experiment_options`` for descriptions. - """ - self._timing = None - - super().__init__( - physical_qubits=physical_qubits, - analysis=StarkRamseyXYAmpScanAnalysis(), - backend=backend, - ) - self.set_experiment_options(**experiment_options) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - stark_channel (PulseChannel): Pulse channel on which to apply Stark tones. - If not provided, the same channel with the qubit drive is used. - See :ref:`stark_channel_consideration` for details. - stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. - This offset should be sufficiently large so that the Stark pulse - does not Rabi drive the qubit. - See :ref:`stark_frequency_consideration` for details. - stark_sigma (float): Gaussian sigma of the rising and falling edges - of the Stark tone, in seconds. - stark_risefall (float): Ratio of sigma to the duration of - the rising and falling edges of the Stark tone. - stark_length (float): Time to accumulate Stark shifted phase in seconds. - min_stark_amp (float): Minimum Stark tone amplitude. - max_stark_amp (float): Maximum Stark tone amplitude. - num_stark_amps (int): Number of Stark tone amplitudes to scan. - stark_amps (list[float]): The list of amplitude that will be scanned in the experiment. - If not set, then ``num_stark_amps`` evenly spaced amplitudes - between ``min_stark_amp`` and ``max_stark_amp`` are used. If ``stark_amps`` - is set, these parameters are ignored. - """ - options = super()._default_experiment_options() - options.update_options( - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - stark_length=50e-9, - min_stark_amp=-1.0, - max_stark_amp=1.0, - num_stark_amps=101, - stark_amps=None, - ) - options.set_validator("stark_freq_offset", (0, np.inf)) - options.set_validator("stark_channel", pulse.channels.PulseChannel) - return options - - def _set_backend(self, backend: Backend): - super()._set_backend(backend) - self._timing = BackendTiming(backend) - - def parameters(self) -> np.ndarray: - """Stark tone amplitudes to use in circuits. - - Returns: - The list of amplitudes to use for the different circuits based on the - experiment options. - """ - opt = self.experiment_options # alias - - if opt.stark_amps is None: - params = np.linspace(opt.min_stark_amp, opt.max_stark_amp, opt.num_stark_amps) - else: - params = np.asarray(opt.stark_amps, dtype=float) - - return params - - def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: - """Create circuits with parameters for Ramsey XY experiment with Stark tone. - - Returns: - Quantum template circuits for Ramsey X and Ramsey Y experiment. - """ - opt = self.experiment_options # alias - param = Parameter("stark_amp") - sym_param = param._symbol_expr - - # Pulse gates - stark_v = Gate("StarkV", 1, [param]) - stark_u = Gate("StarkU", 1, [param]) - - # Note that Stark tone yields negative (positive) frequency shift - # when the Stark tone frequency is higher (lower) than qubit f01 frequency. - # This choice gives positive frequency shift with positive Stark amplitude. - qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] - neg_sign_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=-sym.sign(sym_param), - ) - abs_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=sym.Abs(sym_param), - ) - stark_freq = qubit_f01 + neg_sign_of_amp * opt.stark_freq_offset - stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) - ramps_dt = self._timing.round_pulse(time=2 * opt.stark_risefall * opt.stark_sigma) - sigma_dt = ramps_dt / 2 / opt.stark_risefall - width_dt = self._timing.round_pulse(time=opt.stark_length) - - with pulse.build() as stark_v_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.Gaussian( - duration=ramps_dt, - amp=abs_of_amp, - sigma=sigma_dt, - ), - stark_channel, - ) - - with pulse.build() as stark_u_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.GaussianSquare( - duration=ramps_dt + width_dt, - amp=abs_of_amp, - sigma=sigma_dt, - risefall_sigma_ratio=opt.stark_risefall, - ), - stark_channel, - ) - - ram_x = QuantumCircuit(1, 1) - ram_x.sx(0) - ram_x.append(stark_v, [0]) - ram_x.x(0) - ram_x.append(stark_u, [0]) - ram_x.rz(-np.pi, 0) - ram_x.sx(0) - ram_x.measure(0, 0) - ram_x.metadata = {"series": "X"} - ram_x.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_x.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - ram_y = QuantumCircuit(1, 1) - ram_y.sx(0) - ram_y.append(stark_v, [0]) - ram_y.x(0) - ram_y.append(stark_u, [0]) - ram_y.rz(-np.pi * 3 / 2, 0) - ram_y.sx(0) - ram_y.measure(0, 0) - ram_y.metadata = {"series": "Y"} - ram_y.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_y.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - return ram_x, ram_y - - def circuits(self) -> List[QuantumCircuit]: - """Create circuits. - - Returns: - A list of circuits with a variable Stark tone amplitudes. - """ - ramx_circ, ramy_circ = self.parameterized_circuits() - param = next(iter(ramx_circ.parameters)) - - circs = [] - for amp in self.parameters(): - # Add metadata "direction" to ease the filtering of the data - # by curve analysis. Indeed, the fit parameters are amplitude sign dependent. - - ramx_circ_assigned = ramx_circ.assign_parameters({param: amp}, inplace=False) - ramx_circ_assigned.metadata["xval"] = amp - ramx_circ_assigned.metadata["direction"] = "pos" if amp > 0 else "neg" - - ramy_circ_assigned = ramy_circ.assign_parameters({param: amp}, inplace=False) - ramy_circ_assigned.metadata["xval"] = amp - ramy_circ_assigned.metadata["direction"] = "pos" if amp > 0 else "neg" - - circs.extend([ramx_circ_assigned, ramy_circ_assigned]) - - return circs - - def _metadata(self) -> Dict[str, any]: - """Return experiment metadata for ExperimentData.""" - metadata = super()._metadata() - metadata["stark_length"] = self._timing.pulse_time( - time=self.experiment_options.stark_length - ) - metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset - - return metadata diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 3873d03f60..0554599a25 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -13,24 +13,14 @@ T1 Experiment class. """ -from typing import List, Tuple, Dict, Optional, Union, Sequence +from typing import List, Optional, Union, Sequence import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, Parameter, ParameterExpression +from qiskit.circuit import QuantumCircuit from qiskit.providers.backend import Backend -from qiskit.utils import optionals as _optional -from qiskit_experiments.framework import BackendTiming, BaseExperiment, ExperimentData, Options -from qiskit_experiments.library.characterization.analysis.t1_analysis import ( - T1Analysis, - StarkP1SpectAnalysis, -) - -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym +from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options +from qiskit_experiments.library.characterization.analysis.t1_analysis import T1Analysis class T1(BaseExperiment): @@ -119,334 +109,3 @@ def _metadata(self): if hasattr(self.run_options, run_opt): metadata[run_opt] = getattr(self.run_options, run_opt) return metadata - - -class StarkP1Spectroscopy(BaseExperiment): - """P1 spectroscopy experiment with Stark tone. - - # section: overview - - This experiment measures a probability of the excitation state of the qubit - with a certain delay after excitation. - A Stark tone is applied during this delay to move the - qubit frequency to conduct a spectroscopy of qubit relaxation quantity. - - .. parsed-literal:: - - ┌───┐┌──────────────────┐┌─┐ - q: ┤ X ├┤ Stark(stark_amp) ├┤M├ - └───┘└──────────────────┘└╥┘ - c: 1/══════════════════════════╩═ - 0 - - Since the qubit relaxation rate may depend on the qubit frequency due to the - coupling to nearby energy levels, this experiment is useful to find out - lossy operation frequency that might be harmful to the gate fidelity [1]. - - # section: analysis_ref - :py:class:`.StarkP1SpectAnalysis` - - # section: reference - .. ref_arxiv:: 1 2105.15201 - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXY` - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXYAmpScan` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = None, - **experiment_options, - ): - """ - Initialize the T1 experiment class. - - Args: - physical_qubits: Sequence with the index of the physical qubit. - backend: Optional, the backend to run the experiment on. - experiment_options: Experiment options. See the class documentation or - ``self._default_experiment_options`` for descriptions. - """ - self._timing = None - - super().__init__( - physical_qubits=physical_qubits, - analysis=StarkP1SpectAnalysis(), - backend=backend, - ) - self.set_experiment_options(**experiment_options) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - t1_delay (float): The T1 delay time after excitation pulse. The delay must be - sufficiently greater than the edge duration determined by the stark_sigma. - stark_channel (PulseChannel): Pulse channel to apply Stark tones. - If not provided, the same channel with the qubit drive is used. - stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. - This must be greater than zero not to apply Rabi drive. - stark_sigma (float): Gaussian sigma of the rising and falling edges - of the Stark tone, in seconds. - stark_risefall (float): Ratio of sigma to the duration of - the rising and falling edges of the Stark tone. - min_xval (float): Minimum x value. - max_xval (float): Maximum x value. - num_xvals (int): Number of x-values to scan. - xval_type (str): Type of x-value. Either ``amplitude`` or ``frequency``. - Setting to frequency requires pre-calibration of Stark shift coefficients. - spacing (str): A policy for the spacing to create an amplitude list from - ``min_stark_amp`` to ``max_stark_amp``. Either ``linear`` or ``quadratic`` - must be specified. - xvals (list[float]): The list of x-values that will be scanned in the experiment. - If not set, then ``num_xvals`` parameters spaced according to - the ``spacing`` policy between ``min_xval`` and ``max_xval`` are used. - If ``xvals`` is set, these parameters are ignored. - service (IBMExperimentService): A valid experiment service instance that can - provide the Stark coefficients for the qubit to run experiment. - This is required only when ``stark_coefficients`` is ``latest`` and - ``xval_type`` is ``frequency``. This value is automatically set when - a backend is attached to this experiment instance. - stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to - convert tone amplitudes into amount of Stark shift. This dictionary must include - all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, - which are calibrated with :class:`.StarkRamseyXYAmpScan`. - Alternatively, a search for these coefficients in the result database is run - when "latest" is set. This requires having the experiment service available - in the ``backend`` set for the experiment. - """ - options = super()._default_experiment_options() - options.update_options( - t1_delay=20e-6, - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - min_xval=-1.0, - max_xval=1.0, - num_xvals=201, - xval_type="amplitude", - spacing="quadratic", - xvals=None, - service=None, - stark_coefficients="latest", - ) - options.set_validator("spacing", ["linear", "quadratic"]) - options.set_validator("xval_type", ["amplitude", "frequency"]) - options.set_validator("stark_freq_offset", (0, np.inf)) - options.set_validator("stark_channel", pulse.channels.PulseChannel) - return options - - def _set_backend(self, backend: Backend): - super()._set_backend(backend) - self._timing = BackendTiming(backend) - if self.experiment_options.service is None: - self.set_experiment_options( - service=ExperimentData.get_service_from_backend(backend), - ) - - def parameters(self) -> np.ndarray: - """Stark tone amplitudes to use in circuits. - - Returns: - The list of amplitudes to use for the different circuits based on the - experiment options. - """ - opt = self.experiment_options # alias - - if opt.xvals is None: - if opt.spacing == "linear": - params = np.linspace(opt.min_xval, opt.max_xval, opt.num_xvals) - elif opt.spacing == "quadratic": - min_sqrt = np.sign(opt.min_xval) * np.sqrt(np.abs(opt.min_xval)) - max_sqrt = np.sign(opt.max_xval) * np.sqrt(np.abs(opt.max_xval)) - lin_params = np.linspace(min_sqrt, max_sqrt, opt.num_xvals) - params = np.sign(lin_params) * lin_params**2 - else: - raise ValueError(f"Spacing option {opt.spacing} is not valid.") - else: - params = np.asarray(opt.xvals, dtype=float) - - if opt.xval_type == "frequency": - return self._frequencies_to_amplitudes(params) - return params - - def _frequencies_to_amplitudes(self, params: np.ndarray) -> np.ndarray: - """A helper method to convert frequency values to amplitude. - - Args: - params: Parameters representing a frequency of Stark shift. - - Returns: - Corresponding Stark tone amplitudes. - - Raises: - RuntimeError: When service or analysis results for Stark coefficients are not available. - TypeError: When attached analysis class is not valid. - KeyError: When stark_coefficients dictionary is provided but keys are missing. - ValueError: When specified Stark shift is not available. - """ - opt = self.experiment_options # alias - - if not isinstance(self.analysis, StarkP1SpectAnalysis): - raise TypeError( - f"Analysis class {self.analysis.__class__.__name__} is not a subclass of " - "StarkP1SpectAnalysis. Use proper analysis class to scan frequencies." - ) - coef_names = self.analysis.stark_coefficients_names - - if opt.stark_coefficients == "latest": - if opt.service is None: - raise RuntimeError( - "Experiment service is not available. Provide a dictionary of " - "Stark coefficients in the experiment options." - ) - coefficients = self.analysis.retrieve_coefficients_from_service( - service=opt.service, - qubit=self.physical_qubits[0], - backend=self._backend_data.name, - ) - if coefficients is None: - raise RuntimeError( - "Experiment results for the coefficients of the Stark shift is not found " - f"for the backend {self._backend_data.name} qubit {self.physical_qubits}." - ) - else: - missing = set(coef_names) - opt.stark_coefficients.keys() - if any(missing): - raise KeyError( - f"Following coefficient data is missing in the 'stark_coefficients': {missing}." - ) - coefficients = opt.stark_coefficients - positive = np.asarray([coefficients[coef_names[idx]] for idx in [2, 1, 0]]) - negative = np.asarray([coefficients[coef_names[idx]] for idx in [5, 4, 3]]) - offset = coefficients[coef_names[6]] - - amplitudes = np.zeros_like(params) - for idx, tgt_freq in enumerate(params): - stark_shift = tgt_freq - offset - if np.isclose(stark_shift, 0): - amplitudes[idx] = 0 - continue - if np.sign(stark_shift) > 0: - fit_coeffs = [*positive, -stark_shift] - else: - fit_coeffs = [*negative, -stark_shift] - amp_candidates = np.roots(fit_coeffs) - # Because the fit function is third order, we get three solutions here. - # Only one valid solution must exist because we assume - # a monotonic trend for Stark shift against tone amplitude in domain of definition. - criteria = np.all( - [ - # Frequency shift and tone have the same sign by definition - np.sign(amp_candidates.real) == np.sign(stark_shift), - # Tone amplitude is a real value - np.isclose(amp_candidates.imag, 0.0), - # The absolute value of tone amplitude must be less than 1.0 - np.abs(amp_candidates.real) < 1.0, - ], - axis=0, - ) - valid_amps = amp_candidates[criteria] - if len(valid_amps) == 0: - raise ValueError( - f"Stark shift at frequency value of {tgt_freq} Hz is not available on " - f"the backend {self._backend_data.name} qubit {self.physical_qubits}." - ) - if len(valid_amps) > 1: - # We assume a monotonic trend but sometimes a large third-order term causes - # inflection point and inverts the trend in larger amplitudes. - # In this case we would have more than one solutions, but we can - # take the smallerst amplitude before reaching to the inflection point. - before_inflection = np.argmin(np.abs(valid_amps.real)) - valid_amp = float(valid_amps[before_inflection].real) - else: - valid_amp = float(valid_amps.real) - amplitudes[idx] = valid_amp - - return amplitudes - - def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: - """Create circuits with parameters for P1 experiment with Stark shift. - - Returns: - Quantum template circuit for P1 experiment. - """ - opt = self.experiment_options # alias - param = Parameter("stark_amp") - sym_param = param._symbol_expr - - # Pulse gates - stark = Gate("Stark", 1, [param]) - - # Note that Stark tone yields negative (positive) frequency shift - # when the Stark tone frequency is higher (lower) than qubit f01 frequency. - # This choice gives positive frequency shift with positive Stark amplitude. - qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] - neg_sign_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=-sym.sign(sym_param), - ) - abs_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=sym.Abs(sym_param), - ) - stark_freq = qubit_f01 + neg_sign_of_amp * opt.stark_freq_offset - stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) - sigma_dt = opt.stark_sigma / self._backend_data.dt - delay_dt = self._timing.round_pulse(time=opt.t1_delay) - - with pulse.build() as stark_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.GaussianSquare( - duration=delay_dt, - amp=abs_of_amp, - sigma=sigma_dt, - risefall_sigma_ratio=opt.stark_risefall, - ), - stark_channel, - ) - - temp_t1 = QuantumCircuit(1, 1) - temp_t1.x(0) - temp_t1.append(stark, [0]) - temp_t1.measure(0, 0) - temp_t1.add_calibration( - gate=stark, - qubits=self.physical_qubits, - schedule=stark_schedule, - ) - - return (temp_t1,) - - def circuits(self) -> List[QuantumCircuit]: - """Create circuits. - - Returns: - A list of P1 circuits with a variable Stark tone amplitudes. - """ - (t1_circ,) = self.parameterized_circuits() - param = next(iter(t1_circ.parameters)) - - circs = [] - for amp in self.parameters(): - t1_assigned = t1_circ.assign_parameters({param: amp}, inplace=False) - t1_assigned.metadata = {"xval": amp} - circs.append(t1_assigned) - - return circs - - def _metadata(self) -> Dict[str, any]: - """Return experiment metadata for ExperimentData.""" - metadata = super()._metadata() - metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset - - return metadata diff --git a/qiskit_experiments/library/driven_freq_tuning/__init__.py b/qiskit_experiments/library/driven_freq_tuning/__init__.py new file mode 100644 index 0000000000..9cdd2faf99 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/__init__.py @@ -0,0 +1,66 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. + +""" +=============================================================================================== +Driven Frequency Tuning (:mod:`qiskit_experiments.library.driven_freq_tuning`) +=============================================================================================== + +.. currentmodule:: qiskit_experiments.library.driven_freq_tuning + +Experiments +=========== +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/experiment.rst + + StarkRamseyXY + StarkRamseyXYAmpScan + StarkP1Spectroscopy + + +Analysis +======== + +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/analysis.rst + + StarkRamseyXYAmpScanAnalysis + StarkP1SpectAnalysis + + +Utilities +========= + +.. autosummary:: + :toctree: ../stubs/ + + StarkCoefficients + convert_amp_to_freq + convert_freq_to_amp + retrieve_coefficients_from_backend + retrieve_coefficients_from_service +""" + +from .analyses import StarkRamseyXYAmpScanAnalysis, StarkP1SpectAnalysis +from .ramsey import StarkRamseyXY +from .ramsey_amp_scan import StarkRamseyXYAmpScan +from .p1_spect import StarkP1Spectroscopy + +from .coefficient_utils import ( + StarkCoefficients, + convert_amp_to_freq, + convert_freq_to_amp, + retrieve_coefficients_from_backend, + retrieve_coefficients_from_service, +) diff --git a/qiskit_experiments/library/driven_freq_tuning/analyses.py b/qiskit_experiments/library/driven_freq_tuning/analyses.py new file mode 100644 index 0000000000..67f510a6f6 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/analyses.py @@ -0,0 +1,584 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +"""Stark shift analyses.""" + +from __future__ import annotations + +from typing import List, Union + +import lmfit +import numpy as np +from uncertainties import unumpy as unp + +import qiskit_experiments.curve_analysis as curve +import qiskit_experiments.data_processing as dp +import qiskit_experiments.visualization as vis +from qiskit_experiments.data_processing.exceptions import DataProcessorError +from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options +from .coefficient_utils import ( + StarkCoefficients, + convert_amp_to_freq, + retrieve_coefficients_from_service, +) + + +class StarkRamseyXYAmpScanAnalysis(curve.CurveAnalysis): + r"""Ramsey XY analysis for the Stark shifted phase sweep. + + # section: overview + + This analysis is a variant of :class:`RamseyXYAnalysis`. In both cases, the X and Y + data are treated as the real and imaginary parts of a complex oscillating signal. + In :class:`RamseyXYAnalysis`, the data are fit assuming a phase varying linearly with + the x-data corresponding to a constant frequency and assuming an exponentially + decaying amplitude. By contrast, in this model, the phase is assumed to be + a third order polynomial :math:`\theta(x)` of the x-data. + Additionally, the amplitude is not assumed to follow a specific form. + Techniques to compute a good initial guess for the polynomial coefficients inside + a trigonometric function like this are not trivial. Instead, this analysis extracts the + raw phase and runs fits on the extracted data to a polynomial :math:`\theta(x)` directly. + + The measured P1 values for a Ramsey X and Y experiment can be written in the form of + a trignometric function taking the phase polynomial :math:`\theta(x)`: + + .. math:: + + P_X = \text{amp}(x) \cdot \cos \theta(x) + \text{offset},\\ + P_Y = \text{amp}(x) \cdot \sin \theta(x) + \text{offset}. + + Hence the phase polynomial can be extracted as follows + + .. math:: + + \theta(x) = \tan^{-1} \frac{P_Y}{P_X}. + + Because the arctangent is implemented by the ``atan2`` function + defined in :math:`[-\pi, \pi]`, the computed :math:`\theta(x)` is unwrapped to + ensure continuous phase evolution. + + We call attention to the fact that :math:`\text{amp}(x)` is also Stark tone amplitude + dependent because of the qubit frequency dependence of the dephasing rate. + In general :math:`\text{amp}(x)` is unpredictable due to dephasing from + two-level systems distributed randomly in frequency + or potentially due to qubit heating. This prevents us from precisely fitting + the raw :math:`P_X`, :math:`P_Y` data. Fitting only the phase data makes the + analysis robust to amplitude dependent dephasing. + + In this analysis, the phase polynomial is defined as + + .. math:: + + \theta(x) = 2 \pi t_S f_S(x) + + where + + .. math:: + + f_S(x) = c_1 x + c_2 x^2 + c_3 x^3 + f_{\rm err}, + + denotes the Stark shift. For the lowest order perturbative expansion of a single driven qubit, + the Stark shift is a quadratic function of :math:`x`, but linear and cubic terms + and a constant offset are also considered to account for + other effects, e.g. strong drive, collisions, TLS, and so forth, + and frequency mis-calibration, respectively. + + # section: fit_model + + .. math:: + + \theta^\nu(x) = c_1^\nu x + c_2^\nu x^2 + c_3^\nu x^3 + f_{\rm err}, + + where :math:`\nu \in \{+, -\}`. + The Stark shift is asymmetric with respect to :math:`x=0`, because of the + anti-crossings of higher energy levels. In a typical transmon qubit, + these levels appear only in :math:`f_S < 0` because of the negative anharmonicity. + To precisely fit the results, this analysis uses different model parameters + for positive (:math:`x > 0`) and negative (:math:`x < 0`) shift domains. + + # section: fit_parameters + + defpar c_1^+: + desc: The linear term coefficient of the positive Stark shift + (fit parameter: ``stark_pos_coef_o1``). + init_guess: 0. + bounds: None + + defpar c_2^+: + desc: The quadratic term coefficient of the positive Stark shift. + This parameter must be positive because Stark amplitude is chosen to + induce blue shift when its sign is positive. + Note that the quadratic term is the primary term + (fit parameter: ``stark_pos_coef_o2``). + init_guess: 1e6. + bounds: [0, inf] + + defpar c_3^+: + desc: The cubic term coefficient of the positive Stark shift + (fit parameter: ``stark_pos_coef_o3``). + init_guess: 0. + bounds: None + + defpar c_1^-: + desc: The linear term coefficient of the negative Stark shift. + (fit parameter: ``stark_neg_coef_o1``). + init_guess: 0. + bounds: None + + defpar c_2^-: + desc: The quadratic term coefficient of the negative Stark shift. + This parameter must be negative because Stark amplitude is chosen to + induce red shift when its sign is negative. + Note that the quadratic term is the primary term + (fit parameter: ``stark_neg_coef_o2``). + init_guess: -1e6. + bounds: [-inf, 0] + + defpar c_3^-: + desc: The cubic term coefficient of the negative Stark shift + (fit parameter: ``stark_neg_coef_o3``). + init_guess: 0. + bounds: None + + defpar f_{\rm err}: + desc: Constant phase accumulation which is independent of the Stark tone amplitude. + (fit parameter: ``stark_ferr``). + init_guess: 0 + bounds: None + + # section: see_also + + :class:`qiskit_experiments.library.characterization.analysis.ramsey_xy_analysis.RamseyXYAnalysis` + + """ + + def __init__(self): + + models = [ + lmfit.models.ExpressionModel( + expr="c1_pos * x + c2_pos * x**2 + c3_pos * x**3 + f_err", + name="FREQpos", + ), + lmfit.models.ExpressionModel( + expr="c1_neg * x + c2_neg * x**2 + c3_neg * x**3 + f_err", + name="FREQneg", + ), + ] + super().__init__(models=models) + + @classmethod + def _default_options(cls): + """Default analysis options. + + Analysis Options: + pulse_len (float): Duration of effective Stark pulse in units of sec. + """ + ramsey_plotter = vis.CurvePlotter(vis.MplDrawer()) + ramsey_plotter.set_figure_options( + xlabel="Stark tone amplitude", + ylabel=["Stark shift", "P1"], + yval_unit=["Hz", None], + series_params={ + "Fpos": { + "color": "#123FE8", + "symbol": "^", + "label": "", + "canvas": 0, + }, + "Fneg": { + "color": "#123FE8", + "symbol": "v", + "label": "", + "canvas": 0, + }, + "Xpos": { + "color": "#123FE8", + "symbol": "o", + "label": "Ramsey X", + "canvas": 1, + }, + "Ypos": { + "color": "#6312E8", + "symbol": "^", + "label": "Ramsey Y", + "canvas": 1, + }, + "Xneg": { + "color": "#E83812", + "symbol": "o", + "label": "Ramsey X", + "canvas": 1, + }, + "Yneg": { + "color": "#E89012", + "symbol": "^", + "label": "Ramsey Y", + "canvas": 1, + }, + }, + sharey=False, + ) + ramsey_plotter.set_options(subplots=(2, 1), style=vis.PlotStyle({"figsize": (10, 8)})) + + options = super()._default_options() + options.update_options( + data_subfit_map={ + "Xpos": {"series": "X", "direction": "pos"}, + "Ypos": {"series": "Y", "direction": "pos"}, + "Xneg": {"series": "X", "direction": "neg"}, + "Yneg": {"series": "Y", "direction": "neg"}, + }, + plotter=ramsey_plotter, + fit_category="freq", + pulse_len=None, + ) + + return options + + def _freq_phase_coef(self) -> float: + """Return a coefficient to convert frequency into phase value.""" + try: + return 2 * np.pi * self.options.pulse_len + except TypeError as ex: + raise TypeError( + "A float-value duration in units of sec of the Stark pulse must be provided. " + f"The pulse_len option value {self.options.pulse_len} is not valid." + ) from ex + + def _format_data( + self, + curve_data: curve.ScatterTable, + category: str = "freq", + ) -> curve.ScatterTable: + + curve_data = super()._format_data(curve_data, category="ramsey_xy") + ramsey_xy = curve_data[curve_data.category == "ramsey_xy"] + + # Create phase data by arctan(Y/X) + columns = list(curve_data.columns) + phase_data = np.empty((0, len(columns))) + y_mean = ramsey_xy.yval.mean() + + grouped = ramsey_xy.groupby("name") + for m_id, direction in enumerate(("pos", "neg")): + x_quadrature = grouped.get_group(f"X{direction}") + y_quadrature = grouped.get_group(f"Y{direction}") + if not np.array_equal(x_quadrature.xval, y_quadrature.xval): + raise ValueError( + "Amplitude values of X and Y quadrature are different. " + "Same values must be used." + ) + x_uarray = unp.uarray(x_quadrature.yval, x_quadrature.yerr) + y_uarray = unp.uarray(y_quadrature.yval, y_quadrature.yerr) + + amplitudes = x_quadrature.xval.to_numpy() + + # pylint: disable=no-member + phase = unp.arctan2(y_uarray - y_mean, x_uarray - y_mean) + phase_n = unp.nominal_values(phase) + phase_s = unp.std_devs(phase) + + # Unwrap phase + # We assume a smooth slope and correct 2pi phase jump to minimize the change of the slope. + unwrapped_phase = np.unwrap(phase_n) + if amplitudes[0] < 0: + # Preserve phase value closest to 0 amplitude + unwrapped_phase = unwrapped_phase + (phase_n[-1] - unwrapped_phase[-1]) + + # Store new data + tmp = np.empty((len(amplitudes), len(columns)), dtype=object) + tmp[:, columns.index("xval")] = amplitudes + tmp[:, columns.index("yval")] = unwrapped_phase / self._freq_phase_coef() + tmp[:, columns.index("yerr")] = phase_s / self._freq_phase_coef() + tmp[:, columns.index("name")] = f"FREQ{direction}" + tmp[:, columns.index("class_id")] = m_id + tmp[:, columns.index("shots")] = x_quadrature.shots + y_quadrature.shots + tmp[:, columns.index("category")] = category + phase_data = np.r_[phase_data, tmp] + + return curve_data.append_list_values(other=phase_data) + + def _generate_fit_guesses( + self, + user_opt: curve.FitOptions, + curve_data: curve.ScatterTable, + ) -> Union[curve.FitOptions, List[curve.FitOptions]]: + """Create algorithmic initial fit guess from analysis options and curve data. + + Args: + user_opt: Fit options filled with user provided guess and bounds. + curve_data: Formatted data collection to fit. + + Returns: + List of fit options that are passed to the fitter function. + """ + user_opt.bounds.set_if_empty(c2_pos=(0, np.inf), c2_neg=(-np.inf, 0)) + user_opt.p0.set_if_empty( + c1_pos=0, c2_pos=1e6, c3_pos=0, c1_neg=0, c2_neg=-1e6, c3_neg=0, f_err=0 + ) + return user_opt + + def _create_analysis_results( + self, + fit_data: curve.CurveFitResult, + quality: str, + **metadata, + ) -> List[AnalysisResultData]: + outcomes = super()._create_analysis_results(fit_data, quality, **metadata) + + # Combine fit coefficients + coeffs = StarkCoefficients( + pos_coef_o1=fit_data.ufloat_params["c1_pos"].nominal_value, + pos_coef_o2=fit_data.ufloat_params["c2_pos"].nominal_value, + pos_coef_o3=fit_data.ufloat_params["c3_pos"].nominal_value, + neg_coef_o1=fit_data.ufloat_params["c1_neg"].nominal_value, + neg_coef_o2=fit_data.ufloat_params["c2_neg"].nominal_value, + neg_coef_o3=fit_data.ufloat_params["c3_neg"].nominal_value, + offset=fit_data.ufloat_params["f_err"].nominal_value, + ) + outcomes.append( + AnalysisResultData( + name="stark_coefficients", + value=coeffs, + chisq=fit_data.reduced_chisq, + quality=quality, + extra=metadata, + ) + ) + return outcomes + + def _create_figures( + self, + curve_data: curve.ScatterTable, + ) -> List["matplotlib.figure.Figure"]: + + # plot unwrapped phase on first axis + for d in ("pos", "neg"): + sub_data = curve_data[(curve_data.name == f"FREQ{d}") & (curve_data.category == "freq")] + self.plotter.set_series_data( + series_name=f"F{d}", + x_formatted=sub_data.xval.to_numpy(), + y_formatted=sub_data.yval.to_numpy(), + y_formatted_err=sub_data.yerr.to_numpy(), + ) + + # plot raw RamseyXY plot on second axis + for name in ("Xpos", "Ypos", "Xneg", "Yneg"): + sub_data = curve_data[(curve_data.name == name) & (curve_data.category == "ramsey_xy")] + self.plotter.set_series_data( + series_name=name, + x_formatted=sub_data.xval.to_numpy(), + y_formatted=sub_data.yval.to_numpy(), + y_formatted_err=sub_data.yerr.to_numpy(), + ) + + # find base and amplitude guess + ramsey_xy = curve_data[curve_data.category == "ramsey_xy"] + offset_guess = 0.5 * (ramsey_xy.yval.min() + ramsey_xy.yval.max()) + amp_guess = 0.5 * np.ptp(ramsey_xy.yval) + + # plot frequency and Ramsey fit lines + line_data = curve_data[curve_data.category == "fitted"] + for direction in ("pos", "neg"): + sub_data = line_data[line_data.name == f"FREQ{direction}"] + if len(sub_data) == 0: + continue + xval = sub_data.xval.to_numpy() + yn = sub_data.yval.to_numpy() + ys = sub_data.yerr.to_numpy() + yval = unp.uarray(yn, ys) * self._freq_phase_coef() + + # Ramsey fit lines are predicted from the phase fit line. + # Note that this line doesn't need to match with the expeirment data + # because Ramsey P1 data may fluctuate due to phase damping. + + # pylint: disable=no-member + ramsey_cos = amp_guess * unp.cos(yval) + offset_guess + ramsey_sin = amp_guess * unp.sin(yval) + offset_guess + + self.plotter.set_series_data( + series_name=f"F{direction}", + x_interp=xval, + y_interp=yn, + ) + self.plotter.set_series_data( + series_name=f"X{direction}", + x_interp=xval, + y_interp=unp.nominal_values(ramsey_cos), + ) + self.plotter.set_series_data( + series_name=f"Y{direction}", + x_interp=xval, + y_interp=unp.nominal_values(ramsey_sin), + ) + + if np.isfinite(ys).all(): + self.plotter.set_series_data( + series_name=f"F{direction}", + y_interp_err=ys, + ) + self.plotter.set_series_data( + series_name=f"X{direction}", + y_interp_err=unp.std_devs(ramsey_cos), + ) + self.plotter.set_series_data( + series_name=f"Y{direction}", + y_interp_err=unp.std_devs(ramsey_sin), + ) + return [self.plotter.figure()] + + def _initialize( + self, + experiment_data: ExperimentData, + ): + super()._initialize(experiment_data) + + # Set scaling factor to convert phase to frequency + if "stark_length" in experiment_data.metadata: + self.set_options(pulse_len=experiment_data.metadata["stark_length"]) + + +class StarkP1SpectAnalysis(BaseAnalysis): + """Analysis class for StarkP1Spectroscopy. + + # section: overview + + The P1 landscape is hardly predictable because of the random appearance of + lossy TLS notches, and hence this analysis doesn't provide any + generic mathematical model to fit the measurement data. + A developer may subclass this to conduct own analysis. + + This analysis just visualizes the measured P1 values against Stark tone amplitudes. + The tone amplitudes can be converted into the amount of Stark shift + when the calibrated coefficients are provided in the analysis option, + or the calibration experiment results are available in the result database. + + # section: see_also + :class:`qiskit_experiments.library.driven_freq_tuning.StarkRamseyXYAmpScan` + + """ + + @property + def plotter(self) -> vis.CurvePlotter: + """Curve plotter instance.""" + return self.options.plotter + + @classmethod + def _default_options(cls) -> Options: + """Default analysis options. + + Analysis Options: + plotter (Plotter): Plotter to visualize P1 landscape. + data_processor (DataProcessor): Data processor to compute P1 value. + stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to + convert tone amplitudes into amount of Stark shift. This dictionary must include + all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, + which are calibrated with :class:`.StarkRamseyXYAmpScan`. + Alternatively, it searches for these coefficients in the result database + when "latest" is set. This requires having the experiment service set in + the experiment data to analyze. + x_key (str): Key of the circuit metadata to represent x value. + """ + options = super()._default_options() + + p1spect_plotter = vis.CurvePlotter(vis.MplDrawer()) + p1spect_plotter.set_figure_options( + xlabel="Stark amplitude", + ylabel="P(1)", + xscale="quadratic", + ) + + options.update_options( + plotter=p1spect_plotter, + data_processor=dp.DataProcessor("counts", [dp.Probability("1")]), + stark_coefficients=None, + x_key="xval", + ) + options.set_validator("stark_coefficients", StarkCoefficients) + + return options + + # pylint: disable=unused-argument + def _run_spect_analysis( + self, + xdata: np.ndarray, + ydata: np.ndarray, + ydata_err: np.ndarray, + ) -> list[AnalysisResultData]: + """Run further analysis on the spectroscopy data. + + .. note:: + A subclass can overwrite this method to conduct analysis. + + Args: + xdata: X values. This is either amplitudes or frequencies. + ydata: Y values. This is P1 values measured at different Stark tones. + ydata_err: Sampling error of the Y values. + + Returns: + A list of analysis results. + """ + return [] + + def _run_analysis( + self, + experiment_data: ExperimentData, + ) -> tuple[list[AnalysisResultData], list["matplotlib.figure.Figure"]]: + + x_key = self.options.x_key + + # Get calibrated Stark tone coefficients + if self.options.stark_coefficients is None and experiment_data.service is not None: + # Get value from service + stark_coeffs = retrieve_coefficients_from_service( + service=experiment_data.service, + backend_name=experiment_data.backend_name, + qubit=experiment_data.metadata["physical_qubits"][0], + ) + else: + stark_coeffs = self.options.stark_coefficients + + # Compute P1 value and sampling error + data = experiment_data.data() + try: + xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float) + except KeyError as ex: + raise DataProcessorError( + f"X value key {x_key} is not defined in circuit metadata." + ) from ex + ydata_ufloat = self.options.data_processor(data) + ydata = unp.nominal_values(ydata_ufloat) + ydata_err = unp.std_devs(ydata_ufloat) + + # Convert x-axis of amplitudes into Stark shift by consuming calibrated parameters. + if isinstance(stark_coeffs, StarkCoefficients): + xdata = convert_amp_to_freq( + amps=xdata, + coeffs=stark_coeffs, + ) + self.plotter.set_figure_options( + xlabel="Stark shift", + xval_unit="Hz", + xscale="linear", + ) + + # Draw figures and create analysis results. + self.plotter.set_series_data( + series_name="stark_p1", + x_formatted=xdata, + y_formatted=ydata, + y_formatted_err=ydata_err, + x_interp=xdata, + y_interp=ydata, + ) + analysis_results = self._run_spect_analysis(xdata, ydata, ydata_err) + + return analysis_results, [self.plotter.figure()] diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py b/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py new file mode 100644 index 0000000000..01ddffeb62 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py @@ -0,0 +1,234 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. +"""Coefficients characterizing Stark shift.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +from qiskit.providers.backend import Backend +from qiskit_ibm_experiment.service import IBMExperimentService +from qiskit_ibm_experiment.exceptions import IBMApiError + +from qiskit_experiments.framework.backend_data import BackendData +from qiskit_experiments.framework.experiment_data import ExperimentData + + +@dataclass +class StarkCoefficients: + """A dataclass representing a set of coefficients characterizing Stark shift.""" + + pos_coef_o1: float + """The first order shift coefficient on positive amplitude.""" + + pos_coef_o2: float + """The second order shift coefficient on positive amplitude.""" + + pos_coef_o3: float + """The third order shift coefficient on positive amplitude.""" + + neg_coef_o1: float + """The first order shift coefficient on negative amplitude.""" + + neg_coef_o2: float + """The second order shift coefficient on negative amplitude.""" + + neg_coef_o3: float + """The third order shift coefficient on negative amplitude.""" + + offset: float + """Offset frequency.""" + + def positive_coeffs(self) -> list[float]: + """Positive coefficients.""" + return [self.pos_coef_o3, self.pos_coef_o2, self.pos_coef_o1] + + def negative_coeffs(self) -> list[float]: + """Negative coefficients.""" + return [self.neg_coef_o3, self.neg_coef_o2, self.neg_coef_o1] + + def __str__(self): + # Short representation for dataframe + return "StarkCoefficients(...)" + + +def convert_freq_to_amp( + freqs: np.ndarray, + coeffs: StarkCoefficients, +) -> np.ndarray: + """A helper function to convert Stark frequency to amplitude. + + Args: + freqs: Target frequency shifts to compute required Stark amplitude. + coeffs: Calibrated Stark coefficients. + + Returns: + Estimated Stark amplitudes to induce input frequency shifts. + + Raises: + ValueError: When amplitude value cannot be solved. + """ + amplitudes = np.zeros_like(freqs) + for idx, freq in enumerate(freqs): + shift = freq - coeffs.offset + if np.isclose(shift, 0.0): + amplitudes[idx] = 0.0 + continue + if shift > 0: + fit = [*coeffs.positive_coeffs(), -shift] + else: + fit = [*coeffs.negative_coeffs(), -shift] + amp_candidates = np.roots(fit) + # Because the fit function is third order, we get three solutions here. + # Only one valid solution must exist because we assume + # a monotonic trend for Stark shift against tone amplitude in domain of definition. + criteria = np.all( + [ + # Frequency shift and tone have the same sign by definition + np.sign(amp_candidates.real) == np.sign(shift), + # Tone amplitude is a real value + np.isclose(amp_candidates.imag, 0.0), + # The absolute value of tone amplitude must be less than 1.0 + 10 mp + np.abs(amp_candidates.real) < 1.0 + 10 * np.finfo(float).eps, + ], + axis=0, + ) + valid_amps = amp_candidates[criteria] + if len(valid_amps) == 0: + raise ValueError(f"Stark shift at frequency value of {freq} Hz is not available.") + if len(valid_amps) > 1: + # We assume a monotonic trend but sometimes a large third-order term causes + # inflection point and inverts the trend in larger amplitudes. + # In this case we would have more than one solutions, but we can + # take the smallerst amplitude before reaching to the inflection point. + before_inflection = np.argmin(np.abs(valid_amps.real)) + valid_amp = float(valid_amps[before_inflection].real) + else: + valid_amp = float(valid_amps[0].real) + amplitudes[idx] = min(valid_amp, 1.0) + return amplitudes + + +def convert_amp_to_freq( + amps: np.ndarray, + coeffs: StarkCoefficients, +) -> np.ndarray: + """A helper function to convert Stark amplitude to frequency shift. + + Args: + amps: Amplitude values to convert into frequency shift. + coeffs: Calibrated Stark coefficients. + + Returns: + Calculated frequency shift at given Stark amplitude. + """ + pos_fit = np.poly1d([*coeffs.positive_coeffs(), coeffs.offset]) + neg_fit = np.poly1d([*coeffs.negative_coeffs(), coeffs.offset]) + + return np.where(amps > 0, pos_fit(amps), neg_fit(amps)) + + +def find_min_max_frequency( + min_amp: float, + max_amp: float, + coeffs: StarkCoefficients, +) -> tuple[float, float]: + """A helper function to estimate maximum frequency shift within given amplitude budget. + + Args: + min_amp: Minimum Stark amplitude. + max_amp: Maximum Stark amplitude. + coeffs: Calibrated Stark coefficients. + + Returns: + Minimum and maximum frequency shift available within the amplitude range. + """ + trim_amps = [] + for amp in [min_amp, max_amp]: + if amp > 0: + fit = coeffs.positive_coeffs() + else: + fit = coeffs.negative_coeffs() + # Solve for inflection points by computing the point where derivative becomes zero. + solutions = np.roots([deriv * coeff for deriv, coeff in zip((3, 2, 1), fit)]) + inflection_points = solutions[(solutions.imag == 0) & (np.sign(solutions) == np.sign(amp))] + if len(inflection_points) > 0: + # When multiple inflection points are found, use the most outer one. + # There could be a small inflection point around amp=0, + # when the first order term is significant. + amp = min([amp, max(inflection_points, key=abs)], key=abs) + trim_amps.append(amp) + return tuple(convert_amp_to_freq(np.asarray(trim_amps), coeffs)) + + +def retrieve_coefficients_from_service( + service: IBMExperimentService, + backend_name: str, + qubit: int, +) -> StarkCoefficients: + """Retrieve StarkCoefficients object from experiment service. + + Args: + service: IBM Experiment service instance interfacing with result database. + backend_name: Name of target backend. + qubit: Index of qubit. + + Returns: + StarkCoefficients object. + + Raises: + RuntimeError: When stark_coefficients entry doesn't exist in the service. + """ + try: + retrieved = service.analysis_results( + device_components=[f"Q{qubit}"], + result_type="stark_coefficients", + backend_name=backend_name, + sort_by=["creation_datetime:desc"], + ) + except (IBMApiError, ValueError) as ex: + raise RuntimeError( + f"Failed to retrieve the result of stark_coefficients: {ex.message}" + ) from ex + if len(retrieved) == 0: + raise RuntimeError( + "Analysis result of stark_coefficients is not found in the " + "experiment service. Run and save the result of StarkRamseyXYAmpScan." + ) + return retrieved[0].result_data["value"] + + +def retrieve_coefficients_from_backend( + backend: Backend, + qubit: int, +) -> StarkCoefficients: + """Retrieve StarkCoefficients object from the Qiskit backend. + + Args: + backend: Qiskit backend object. + qubit: Index of qubit. + + Returns: + StarkCoefficients object. + + Raises: + RuntimeError: When experiment service cannot be loaded from backend. + """ + name = BackendData(backend).name + service = ExperimentData.get_service_from_backend(backend) + + if service is None: + raise RuntimeError(f"Valid experiment service is not found for the backend {name}.") + + return retrieve_coefficients_from_service(service, name, qubit) diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py new file mode 100644 index 0000000000..a104087853 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py @@ -0,0 +1,270 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +"""P1 experiment at various qubit frequencies.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np +from qiskit import pulse +from qiskit.circuit import QuantumCircuit, Gate, Parameter, ParameterExpression +from qiskit.providers.backend import Backend +from qiskit.utils import optionals as _optional + +from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options +from .analyses import StarkP1SpectAnalysis + +from .coefficient_utils import ( + StarkCoefficients, + convert_freq_to_amp, + retrieve_coefficients_from_backend, +) + +if _optional.HAS_SYMENGINE: + import symengine as sym +else: + import sympy as sym + + +class StarkP1Spectroscopy(BaseExperiment): + """P1 spectroscopy experiment with Stark tone. + + # section: overview + + This experiment measures a probability of the excitation state of the qubit + with a certain delay after excitation. + A Stark tone is applied during this delay to move the + qubit frequency to conduct a spectroscopy of qubit relaxation quantity. + + .. parsed-literal:: + + ┌───┐┌──────────────────┐┌─┐ + q: ┤ X ├┤ Stark(stark_amp) ├┤M├ + └───┘└──────────────────┘└╥┘ + c: 1/══════════════════════════╩═ + 0 + + Since the qubit relaxation rate may depend on the qubit frequency due to the + coupling to nearby energy levels, this experiment is useful to find out + lossy operation frequency that might be harmful to the gate fidelity [1]. + + # section: analysis_ref + :class:`qiskit_experiments.library.driven_freq_tuning.StarkP1SpectAnalysis` + + # section: reference + .. ref_arxiv:: 1 2105.15201 + + # section: see_also + :class:`qiskit_experiments.library.driven_freq_tuning.ramsey.StarkRamseyXY` + :class:`qiskit_experiments.library.driven_freq_tuning.ramsey_amp_scan.StarkRamseyXYAmpScan` + + # section: manual + :doc:`/manuals/characterization/stark_experiment` + + """ + + def __init__( + self, + physical_qubits: Sequence[int], + backend: Backend | None = None, + **experiment_options, + ): + """ + Initialize new experiment class. + + Args: + physical_qubits: Sequence with the index of the physical qubit. + backend: Optional, the backend to run the experiment on. + experiment_options: Experiment options. See the class documentation or + ``self._default_experiment_options`` for descriptions. + """ + self._timing = None + + super().__init__( + physical_qubits=physical_qubits, + analysis=StarkP1SpectAnalysis(), + backend=backend, + ) + self.set_experiment_options(**experiment_options) + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + Experiment Options: + t1_delay (float): The T1 delay time after excitation pulse. The delay must be + sufficiently greater than the edge duration determined by the stark_sigma. + stark_channel (PulseChannel): Pulse channel to apply Stark tones. + If not provided, the same channel with the qubit drive is used. + stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. + This must be greater than zero not to apply Rabi drive. + stark_sigma (float): Gaussian sigma of the rising and falling edges + of the Stark tone, in seconds. + stark_risefall (float): Ratio of sigma to the duration of + the rising and falling edges of the Stark tone. + min_xval (float): Minimum x value. + max_xval (float): Maximum x value. + num_xvals (int): Number of x-values to scan. + xval_type (str): Type of x-value. Either ``amplitude`` or ``frequency``. + Setting to frequency requires pre-calibration of Stark shift coefficients. + spacing (str): A policy for the spacing to create an amplitude list from + ``min_stark_amp`` to ``max_stark_amp``. Either ``linear`` or ``quadratic`` + must be specified. + xvals (list[float]): The list of x-values that will be scanned in the experiment. + If not set, then ``num_xvals`` parameters spaced according to + the ``spacing`` policy between ``min_xval`` and ``max_xval`` are used. + If ``xvals`` is set, these parameters are ignored. + stark_coefficients (StarkCoefficients): Calibrated Stark shift coefficients. + This value is necessary when xval_type is "frequency". + When this value is None, a search for the "stark_coefficients" in the + result database is run. This requires having the experiment service + available in the backend set for the experiment. + """ + options = super()._default_experiment_options() + options.update_options( + t1_delay=20e-6, + stark_channel=None, + stark_freq_offset=80e6, + stark_sigma=15e-9, + stark_risefall=2, + min_xval=-1.0, + max_xval=1.0, + num_xvals=201, + xval_type="amplitude", + spacing="quadratic", + xvals=None, + stark_coefficients=None, + ) + options.set_validator("spacing", ["linear", "quadratic"]) + options.set_validator("xval_type", ["amplitude", "frequency"]) + options.set_validator("stark_freq_offset", (0, np.inf)) + options.set_validator("stark_channel", pulse.channels.PulseChannel) + options.set_validator("stark_coefficients", StarkCoefficients) + return options + + def _set_backend(self, backend: Backend): + super()._set_backend(backend) + self._timing = BackendTiming(backend) + + def parameters(self) -> np.ndarray: + """Stark tone amplitudes to use in circuits. + + Returns: + The list of amplitudes to use for the different circuits based on the + experiment options. + + Raises: + ValueError: When invalid xval spacing is specified. + """ + opt = self.experiment_options # alias + + if opt.xvals is None: + if opt.spacing == "linear": + params = np.linspace(opt.min_xval, opt.max_xval, opt.num_xvals) + elif opt.spacing == "quadratic": + min_sqrt = np.sign(opt.min_xval) * np.sqrt(np.abs(opt.min_xval)) + max_sqrt = np.sign(opt.max_xval) * np.sqrt(np.abs(opt.max_xval)) + lin_params = np.linspace(min_sqrt, max_sqrt, opt.num_xvals) + params = np.sign(lin_params) * lin_params**2 + else: + raise ValueError(f"Spacing option {opt.spacing} is not valid.") + else: + params = np.asarray(opt.xvals, dtype=float) + + if opt.xval_type == "frequency": + coeffs = opt.stark_coefficients + if coeffs is None: + coeffs = retrieve_coefficients_from_backend( + backend=self.backend, + qubit=self.physical_qubits[0], + ) + return convert_freq_to_amp(freqs=params, coeffs=coeffs) + return params + + def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: + """Create circuits with parameters for P1 experiment with Stark shift. + + Returns: + Quantum template circuit for P1 experiment. + """ + opt = self.experiment_options # alias + param = Parameter("stark_amp") + sym_param = param._symbol_expr + + # Pulse gates + stark = Gate("Stark", 1, [param]) + + # Note that Stark tone yields negative (positive) frequency shift + # when the Stark tone frequency is higher (lower) than qubit f01 frequency. + # This choice gives positive frequency shift with positive Stark amplitude. + qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] + neg_sign_of_amp = ParameterExpression( + symbol_map={param: sym_param}, + expr=-sym.sign(sym_param), + ) + abs_of_amp = ParameterExpression( + symbol_map={param: sym_param}, + expr=sym.Abs(sym_param), + ) + stark_freq = qubit_f01 + neg_sign_of_amp * opt.stark_freq_offset + stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) + sigma_dt = opt.stark_sigma / self._backend_data.dt + delay_dt = self._timing.round_pulse(time=opt.t1_delay) + + with pulse.build() as stark_schedule: + pulse.set_frequency(stark_freq, stark_channel) + pulse.play( + pulse.GaussianSquare( + duration=delay_dt, + amp=abs_of_amp, + sigma=sigma_dt, + risefall_sigma_ratio=opt.stark_risefall, + ), + stark_channel, + ) + + temp_t1 = QuantumCircuit(1, 1) + temp_t1.x(0) + temp_t1.append(stark, [0]) + temp_t1.measure(0, 0) + temp_t1.add_calibration( + gate=stark, + qubits=self.physical_qubits, + schedule=stark_schedule, + ) + + return (temp_t1,) + + def circuits(self) -> list[QuantumCircuit]: + """Create circuits. + + Returns: + A list of P1 circuits with a variable Stark tone amplitudes. + """ + (t1_circ,) = self.parameterized_circuits() + param = next(iter(t1_circ.parameters)) + + circs = [] + for amp in self.parameters(): + t1_assigned = t1_circ.assign_parameters({param: amp}, inplace=False) + t1_assigned.metadata = {"xval": amp} + circs.append(t1_assigned) + + return circs + + def _metadata(self) -> dict[str, any]: + """Return experiment metadata for ExperimentData.""" + metadata = super()._metadata() + metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset + + return metadata diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey.py b/qiskit_experiments/library/driven_freq_tuning/ramsey.py new file mode 100644 index 0000000000..e214a5c4b4 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey.py @@ -0,0 +1,359 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +"""Stark Ramsey experiment.""" + +from __future__ import annotations + +import warnings +from collections.abc import Sequence + +import numpy as np +from qiskit import pulse +from qiskit.circuit import QuantumCircuit, Gate, Parameter +from qiskit.providers.backend import Backend +from qiskit.utils import optionals as _optional + +from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming +from qiskit_experiments.library.characterization.analysis import RamseyXYAnalysis + +if _optional.HAS_SYMENGINE: + pass +else: + pass + + +class StarkRamseyXY(BaseExperiment): + """A sign-sensitive experiment to measure the frequency of a qubit under a pulsed Stark tone. + + # section: overview + + This experiment is a variant of :class:`.RamseyXY` with a pulsed Stark tone + and consists of the following two circuits: + + .. parsed-literal:: + + (Ramsey X) The pulse before measurement rotates by pi-half around the X axis + + ┌────┐┌────────┐┌───┐┌───────────────┐┌────────┐┌────┐┌─┐ + q: ┤ √X ├┤ StarkV ├┤ X ├┤ StarkU(delay) ├┤ Rz(-π) ├┤ √X ├┤M├ + └────┘└────────┘└───┘└───────────────┘└────────┘└────┘└╥┘ + c: 1/═══════════════════════════════════════════════════════╩═ + 0 + + (Ramsey Y) The pulse before measurement rotates by pi-half around the Y axis + + ┌────┐┌────────┐┌───┐┌───────────────┐┌───────────┐┌────┐┌─┐ + q: ┤ √X ├┤ StarkV ├┤ X ├┤ StarkU(delay) ├┤ Rz(-3π/2) ├┤ √X ├┤M├ + └────┘└────────┘└───┘└───────────────┘└───────────┘└────┘└╥┘ + c: 1/══════════════════════════════════════════════════════════╩═ + 0 + + In principle, the sequence is a variant of :class:`.RamseyXY` circuit. + However, the delay in between √X gates is replaced with an off-resonant drive. + This off-resonant drive shifts the qubit frequency due to the + Stark effect and causes it to accumulate phase during the + Ramsey sequence. This frequency shift is a function of the + offset of the Stark tone frequency from the qubit frequency + and of the magnitude of the tone. + + Note that the Stark tone pulse (StarkU) takes the form of a flat-topped Gaussian envelope. + The magnitude of the pulse varies in time during its rising and falling edges. + It is difficult to characterize the net phase accumulation of the qubit during the + edges of the pulse when the frequency shift is varying with the pulse amplitude. + In order to simplify the analysis, an additional pulse (StarkV) + involving only the edges of StarkU is added to the sequence. + The sign of the phase accumulation is inverted for StarkV relative to that of StarkU + by inserting an X gate in between the two pulses. + + This technique allows the experiment to accumulate only the net phase + during the flat-top part of the StarkU pulse with constant magnitude. + + # section: analysis_ref + :class:`qiskit_experiments.library.characterization.RamseyXYAnalysis` + + # section: see_also + :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` + + # section: manual + :doc:`/manuals/characterization/stark_experiment` + + """ + + def __init__( + self, + physical_qubits: Sequence[int], + backend: Backend | None = None, + **experiment_options, + ): + """Create new experiment. + + Args: + physical_qubits: Index of physical qubit. + backend: Optional, the backend to run the experiment on. + experiment_options: Experiment options. See the class documentation or + ``self._default_experiment_options`` for descriptions. + """ + self._timing = None + + super().__init__( + physical_qubits=physical_qubits, + analysis=RamseyXYAnalysis(), + backend=backend, + ) + self.set_experiment_options(**experiment_options) + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + Experiment Options: + stark_amp (float): A single float parameter to represent the magnitude of Stark tone + and the sign of expected Stark shift. + See :ref:`stark_tone_implementation` for details. + stark_channel (PulseChannel): Pulse channel to apply Stark tones. + If not provided, the same channel with the qubit drive is used. + See :ref:`stark_channel_consideration` for details. + stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. + This offset should be sufficiently large so that the Stark pulse + does not Rabi drive the qubit. + See :ref:`stark_frequency_consideration` for details. + stark_sigma (float): Gaussian sigma of the rising and falling edges + of the Stark tone, in seconds. + stark_risefall (float): Ratio of sigma to the duration of + the rising and falling edges of the Stark tone. + min_freq (float): Minimum frequency that this experiment is guaranteed to resolve. + Note that fitter algorithm :class:`.RamseyXYAnalysis` of this experiment + is still capable of fitting experiment data with lower frequency. + max_freq (float): Maximum frequency that this experiment can resolve. + delays (list[float]): The list of delays if set that will be scanned in the + experiment. If not set, then evenly spaced delays with interval + computed from ``min_freq`` and ``max_freq`` are used. + See :meth:`StarkRamseyXY.delays` for details. + """ + options = super()._default_experiment_options() + options.update_options( + stark_amp=0.0, + stark_channel=None, + stark_freq_offset=80e6, + stark_sigma=15e-9, + stark_risefall=2, + min_freq=5e6, + max_freq=100e6, + delays=None, + ) + options.set_validator("stark_freq_offset", (0, np.inf)) + options.set_validator("stark_channel", pulse.channels.PulseChannel) + return options + + def _set_backend(self, backend: Backend): + super()._set_backend(backend) + self._timing = BackendTiming(backend) + + def set_experiment_options(self, **fields): + _warning_circuit_length = 300 + + # Do validation for circuit number + min_freq = fields.get("min_freq", None) + max_freq = fields.get("max_freq", None) + delays = fields.get("delays", None) + if min_freq is not None and max_freq is not None: + if delays: + warnings.warn( + "Experiment option 'min_freq' and 'max_freq' are ignored " + "when 'delays' are explicitly specified.", + UserWarning, + ) + else: + n_expr_circs = 2 * int(2 * max_freq / min_freq) # delays * (x, y) + max_circs_per_job = None + if self._backend_data: + max_circs_per_job = self._backend_data.max_circuits() + if n_expr_circs > (max_circs_per_job or _warning_circuit_length): + warnings.warn( + f"Provided configuration generates {n_expr_circs} circuits. " + "You can set smaller 'max_freq' or larger 'min_freq' to reduce this number. " + "This experiment is still executable but your execution may consume " + "unnecessary long quantum device time, and result may suffer " + "device parameter drift in consequence of the long execution time.", + UserWarning, + ) + # Do validation for spectrum overlap to avoid real excitation + stark_freq_offset = fields.get("stark_freq_offset", None) + stark_sigma = fields.get("stark_sigma", None) + if stark_freq_offset is not None and stark_sigma is not None: + if stark_freq_offset < 1 / stark_sigma: + warnings.warn( + "Provided configuration may induce coherent state exchange between qubit levels " + "because of the potential spectrum overlap. You can avoid this by " + "increasing the 'stark_sigma' or 'stark_freq_offset'. " + "Note that this experiment is still executable.", + UserWarning, + ) + pass + + super().set_experiment_options(**fields) + + def parameters(self) -> np.ndarray: + """Delay values to use in circuits. + + .. note:: + + The delays are computed with the ``min_freq`` and ``max_freq`` experiment options. + The maximum point is computed from the ``min_freq`` to guarantee the result + contains at least one Ramsey oscillation cycle at this frequency. + The interval is computed from the ``max_freq`` to sample with resolution + such that the Nyquist frequency is twice ``max_freq``. + + Returns: + The list of delays to use for the different circuits based on the + experiment options. + + Raises: + ValueError: When ``min_freq`` is larger than ``max_freq``. + """ + opt = self.experiment_options # alias + + if opt.delays is None: + if opt.min_freq > opt.max_freq: + raise ValueError("Experiment option 'min_freq' must be smaller than 'max_freq'.") + # Delay is longer enough to capture 1 cycle of the minimum frequency. + # Fitter can still accurately fit samples shorter than 1 cycle. + max_period = 1 / opt.min_freq + # Inverse of interval should be greater than Nyquist frequency. + sampling_freq = 2 * opt.max_freq + interval = 1 / sampling_freq + return np.arange(0, max_period, interval) + return opt.delays + + def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: + """Create circuits with parameters for Ramsey XY experiment with Stark tone. + + Returns: + Quantum template circuits for Ramsey X and Ramsey Y experiment. + """ + opt = self.experiment_options # alias + param = Parameter("delay") + + # Pulse gates + stark_v = Gate("StarkV", 1, []) + stark_u = Gate("StarkU", 1, [param]) + + # Note that Stark tone yields negative (positive) frequency shift + # when the Stark tone frequency is higher (lower) than qubit f01 frequency. + # This choice gives positive frequency shift with positive Stark amplitude. + qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] + stark_freq = qubit_f01 - np.sign(opt.stark_amp) * opt.stark_freq_offset + stark_amp = np.abs(opt.stark_amp) + stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) + ramps_dt = self._timing.round_pulse(time=2 * opt.stark_risefall * opt.stark_sigma) + sigma_dt = ramps_dt / 2 / opt.stark_risefall + + with pulse.build() as stark_v_schedule: + pulse.set_frequency(stark_freq, stark_channel) + pulse.play( + pulse.Gaussian( + duration=ramps_dt, + amp=stark_amp, + sigma=sigma_dt, + name="StarkV", + ), + stark_channel, + ) + + with pulse.build() as stark_u_schedule: + pulse.set_frequency(stark_freq, stark_channel) + pulse.play( + pulse.GaussianSquare( + duration=ramps_dt + param, + amp=stark_amp, + sigma=sigma_dt, + risefall_sigma_ratio=opt.stark_risefall, + name="StarkU", + ), + stark_channel, + ) + + ram_x = QuantumCircuit(1, 1) + ram_x.sx(0) + ram_x.append(stark_v, [0]) + ram_x.x(0) + ram_x.append(stark_u, [0]) + ram_x.rz(-np.pi, 0) + ram_x.sx(0) + ram_x.measure(0, 0) + ram_x.metadata = {"series": "X"} + ram_x.add_calibration( + gate=stark_v, + qubits=self.physical_qubits, + schedule=stark_v_schedule, + ) + ram_x.add_calibration( + gate=stark_u, + qubits=self.physical_qubits, + schedule=stark_u_schedule, + ) + + ram_y = QuantumCircuit(1, 1) + ram_y.sx(0) + ram_y.append(stark_v, [0]) + ram_y.x(0) + ram_y.append(stark_u, [0]) + ram_y.rz(-np.pi * 3 / 2, 0) + ram_y.sx(0) + ram_y.measure(0, 0) + ram_y.metadata = {"series": "Y"} + ram_y.add_calibration( + gate=stark_v, + qubits=self.physical_qubits, + schedule=stark_v_schedule, + ) + ram_y.add_calibration( + gate=stark_u, + qubits=self.physical_qubits, + schedule=stark_u_schedule, + ) + + return ram_x, ram_y + + def circuits(self) -> list[QuantumCircuit]: + """Create circuits. + + Returns: + A list of circuits with a variable delay. + """ + timing = BackendTiming(self.backend, min_length=0) + + ramx_circ, ramy_circ = self.parameterized_circuits() + param = next(iter(ramx_circ.parameters)) + + circs = [] + for delay in self.parameters(): + valid_delay_dt = timing.round_pulse(time=delay) + net_delay_sec = timing.pulse_time(time=delay) + + ramx_circ_assigned = ramx_circ.assign_parameters({param: valid_delay_dt}, inplace=False) + ramx_circ_assigned.metadata["xval"] = net_delay_sec + + ramy_circ_assigned = ramy_circ.assign_parameters({param: valid_delay_dt}, inplace=False) + ramy_circ_assigned.metadata["xval"] = net_delay_sec + + circs.extend([ramx_circ_assigned, ramy_circ_assigned]) + + return circs + + def _metadata(self) -> dict[str, any]: + """Return experiment metadata for ExperimentData.""" + metadata = super()._metadata() + metadata["stark_amp"] = self.experiment_options.stark_amp + metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset + + return metadata diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py new file mode 100644 index 0000000000..d04eb607d1 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py @@ -0,0 +1,311 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +"""Stark Ramsey experiment directly scanning Stark amplitude.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np +from qiskit import pulse +from qiskit.circuit import QuantumCircuit, Gate, ParameterExpression, Parameter +from qiskit.providers.backend import Backend +from qiskit.utils import optionals as _optional + +from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming +from .analyses import StarkRamseyXYAmpScanAnalysis + +if _optional.HAS_SYMENGINE: + import symengine as sym +else: + import sympy as sym + + +class StarkRamseyXYAmpScan(BaseExperiment): + r"""A fast characterization of Stark frequency shift by varying Stark tone amplitude. + + # section: overview + + This experiment scans Stark tone amplitude at a fixed tone duration. + The experimental circuits are identical to the :class:`.StarkRamseyXY` experiment + except that the Stark pulse amplitude is the scanned parameter rather than the pulse width. + + .. parsed-literal:: + + (Ramsey X) The pulse before measurement rotates by pi-half around the X axis + + ┌────┐┌───────────────────┐┌───┐┌───────────────────┐┌────────┐┌────┐┌─┐ + q: ┤ √X ├┤ StarkV(stark_amp) ├┤ X ├┤ StarkU(stark_amp) ├┤ Rz(-π) ├┤ √X ├┤M├ + └────┘└───────────────────┘└───┘└───────────────────┘└────────┘└────┘└╥┘ + c: 1/══════════════════════════════════════════════════════════════════════╩═ + 0 + + (Ramsey Y) The pulse before measurement rotates by pi-half around the Y axis + + ┌────┐┌───────────────────┐┌───┐┌───────────────────┐┌───────────┐┌────┐┌─┐ + q: ┤ √X ├┤ StarkV(stark_amp) ├┤ X ├┤ StarkU(stark_amp) ├┤ Rz(-3π/2) ├┤ √X ├┤M├ + └────┘└───────────────────┘└───┘└───────────────────┘└───────────┘└────┘└╥┘ + c: 1/═════════════════════════════════════════════════════════════════════════╩═ + 0 + + The AC Stark effect can be used to shift the frequency of a qubit with a microwave drive. + To calibrate a specific frequency shift, the :class:`.StarkRamseyXY` experiment can be run + to scan the Stark pulse duration at every amplitude, but such a two dimensional scan of + the tone duration and amplitude may require many circuit executions. + To avoid this overhead, the :class:`.StarkRamseyXYAmpScan` experiment fixes the + tone duration and scans only amplitude. + + Recall that an observed Ramsey oscillation in each quadrature may be represented by + + .. math:: + + {\cal E}_X(\Omega, t_S) = A e^{-t_S/\tau} \cos \left( 2\pi f_S(\Omega) t_S \right), \\ + {\cal E}_Y(\Omega, t_S) = A e^{-t_S/\tau} \sin \left( 2\pi f_S(\Omega) t_S \right), + + where :math:`f_S(\Omega)` denotes the amount of Stark shift + at a constant tone amplitude :math:`\Omega`, and :math:`t_S` is the duration of the + applied tone. For a fixed tone duration, + one can still observe the Ramsey oscillation by scanning the tone amplitude. + However, since :math:`f_S` is usually a higher order polynomial of :math:`\Omega`, + one must manage to fit the y-data for trigonometric functions with + phase which non-linearly changes with the x-data. + The :class:`.StarkRamseyXYAmpScan` experiment thus drastically reduces the number of + circuits to run in return for greater complexity in the fitting model. + + # section: analysis_ref + :class:`qiskit_experiments.library.driven_freq_tuning.StarkRamseyXYAmpScanAnalysis` + + # section: see_also + :class:`qiskit_experiments.library.driven_freq_tuning.ramsey.StarkRamseyXY` + :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` + + # section: manual + :doc:`/manuals/characterization/stark_experiment` + + """ + + def __init__( + self, + physical_qubits: Sequence[int], + backend: Backend | None = None, + **experiment_options, + ): + """Create new experiment. + + Args: + physical_qubits: Sequence with the index of the physical qubit. + backend: Optional, the backend to run the experiment on. + experiment_options: Experiment options. See the class documentation or + ``self._default_experiment_options`` for descriptions. + """ + self._timing = None + + super().__init__( + physical_qubits=physical_qubits, + analysis=StarkRamseyXYAmpScanAnalysis(), + backend=backend, + ) + self.set_experiment_options(**experiment_options) + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + Experiment Options: + stark_channel (PulseChannel): Pulse channel on which to apply Stark tones. + If not provided, the same channel with the qubit drive is used. + See :ref:`stark_channel_consideration` for details. + stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. + This offset should be sufficiently large so that the Stark pulse + does not Rabi drive the qubit. + See :ref:`stark_frequency_consideration` for details. + stark_sigma (float): Gaussian sigma of the rising and falling edges + of the Stark tone, in seconds. + stark_risefall (float): Ratio of sigma to the duration of + the rising and falling edges of the Stark tone. + stark_length (float): Time to accumulate Stark shifted phase in seconds. + min_stark_amp (float): Minimum Stark tone amplitude. + max_stark_amp (float): Maximum Stark tone amplitude. + num_stark_amps (int): Number of Stark tone amplitudes to scan. + stark_amps (list[float]): The list of amplitude that will be scanned in the experiment. + If not set, then ``num_stark_amps`` evenly spaced amplitudes + between ``min_stark_amp`` and ``max_stark_amp`` are used. If ``stark_amps`` + is set, these parameters are ignored. + """ + options = super()._default_experiment_options() + options.update_options( + stark_channel=None, + stark_freq_offset=80e6, + stark_sigma=15e-9, + stark_risefall=2, + stark_length=50e-9, + min_stark_amp=-1.0, + max_stark_amp=1.0, + num_stark_amps=101, + stark_amps=None, + ) + options.set_validator("stark_freq_offset", (0, np.inf)) + options.set_validator("stark_channel", pulse.channels.PulseChannel) + return options + + def _set_backend(self, backend: Backend): + super()._set_backend(backend) + self._timing = BackendTiming(backend) + + def parameters(self) -> np.ndarray: + """Stark tone amplitudes to use in circuits. + + Returns: + The list of amplitudes to use for the different circuits based on the + experiment options. + """ + opt = self.experiment_options # alias + + if opt.stark_amps is None: + params = np.linspace(opt.min_stark_amp, opt.max_stark_amp, opt.num_stark_amps) + else: + params = np.asarray(opt.stark_amps, dtype=float) + + return params + + def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: + """Create circuits with parameters for Ramsey XY experiment with Stark tone. + + Returns: + Quantum template circuits for Ramsey X and Ramsey Y experiment. + """ + opt = self.experiment_options # alias + param = Parameter("stark_amp") + sym_param = param._symbol_expr + + # Pulse gates + stark_v = Gate("StarkV", 1, [param]) + stark_u = Gate("StarkU", 1, [param]) + + # Note that Stark tone yields negative (positive) frequency shift + # when the Stark tone frequency is higher (lower) than qubit f01 frequency. + # This choice gives positive frequency shift with positive Stark amplitude. + qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] + neg_sign_of_amp = ParameterExpression( + symbol_map={param: sym_param}, + expr=-sym.sign(sym_param), + ) + abs_of_amp = ParameterExpression( + symbol_map={param: sym_param}, + expr=sym.Abs(sym_param), + ) + stark_freq = qubit_f01 + neg_sign_of_amp * opt.stark_freq_offset + stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) + ramps_dt = self._timing.round_pulse(time=2 * opt.stark_risefall * opt.stark_sigma) + sigma_dt = ramps_dt / 2 / opt.stark_risefall + width_dt = self._timing.round_pulse(time=opt.stark_length) + + with pulse.build() as stark_v_schedule: + pulse.set_frequency(stark_freq, stark_channel) + pulse.play( + pulse.Gaussian( + duration=ramps_dt, + amp=abs_of_amp, + sigma=sigma_dt, + ), + stark_channel, + ) + + with pulse.build() as stark_u_schedule: + pulse.set_frequency(stark_freq, stark_channel) + pulse.play( + pulse.GaussianSquare( + duration=ramps_dt + width_dt, + amp=abs_of_amp, + sigma=sigma_dt, + risefall_sigma_ratio=opt.stark_risefall, + ), + stark_channel, + ) + + ram_x = QuantumCircuit(1, 1) + ram_x.sx(0) + ram_x.append(stark_v, [0]) + ram_x.x(0) + ram_x.append(stark_u, [0]) + ram_x.rz(-np.pi, 0) + ram_x.sx(0) + ram_x.measure(0, 0) + ram_x.metadata = {"series": "X"} + ram_x.add_calibration( + gate=stark_v, + qubits=self.physical_qubits, + schedule=stark_v_schedule, + ) + ram_x.add_calibration( + gate=stark_u, + qubits=self.physical_qubits, + schedule=stark_u_schedule, + ) + + ram_y = QuantumCircuit(1, 1) + ram_y.sx(0) + ram_y.append(stark_v, [0]) + ram_y.x(0) + ram_y.append(stark_u, [0]) + ram_y.rz(-np.pi * 3 / 2, 0) + ram_y.sx(0) + ram_y.measure(0, 0) + ram_y.metadata = {"series": "Y"} + ram_y.add_calibration( + gate=stark_v, + qubits=self.physical_qubits, + schedule=stark_v_schedule, + ) + ram_y.add_calibration( + gate=stark_u, + qubits=self.physical_qubits, + schedule=stark_u_schedule, + ) + + return ram_x, ram_y + + def circuits(self) -> list[QuantumCircuit]: + """Create circuits. + + Returns: + A list of circuits with a variable Stark tone amplitudes. + """ + ramx_circ, ramy_circ = self.parameterized_circuits() + param = next(iter(ramx_circ.parameters)) + + circs = [] + for amp in self.parameters(): + # Add metadata "direction" to ease the filtering of the data + # by curve analysis. Indeed, the fit parameters are amplitude sign dependent. + + ramx_circ_assigned = ramx_circ.assign_parameters({param: amp}, inplace=False) + ramx_circ_assigned.metadata["xval"] = amp + ramx_circ_assigned.metadata["direction"] = "pos" if amp > 0 else "neg" + + ramy_circ_assigned = ramy_circ.assign_parameters({param: amp}, inplace=False) + ramy_circ_assigned.metadata["xval"] = amp + ramy_circ_assigned.metadata["direction"] = "pos" if amp > 0 else "neg" + + circs.extend([ramx_circ_assigned, ramy_circ_assigned]) + + return circs + + def _metadata(self) -> dict[str, any]: + """Return experiment metadata for ExperimentData.""" + metadata = super()._metadata() + metadata["stark_length"] = self._timing.pulse_time( + time=self.experiment_options.stark_length + ) + metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset + + return metadata diff --git a/test/library/driven_freq_tuning/__init__.py b/test/library/driven_freq_tuning/__init__.py new file mode 100644 index 0000000000..4575d01965 --- /dev/null +++ b/test/library/driven_freq_tuning/__init__.py @@ -0,0 +1,12 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +"""Tests for driven frequency tuning.""" diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/driven_freq_tuning/test_stark_p1_spect.py similarity index 53% rename from test/library/characterization/test_stark_p1_spect.py rename to test/library/driven_freq_tuning/test_stark_p1_spect.py index 3ae4c17e76..f4fb1f2e13 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/driven_freq_tuning/test_stark_p1_spect.py @@ -23,7 +23,8 @@ from qiskit_experiments.framework import ExperimentData, AnalysisResultData from qiskit_experiments.library import StarkP1Spectroscopy -from qiskit_experiments.library.characterization.analysis import StarkP1SpectAnalysis +from qiskit_experiments.library.driven_freq_tuning.analyses import StarkP1SpectAnalysis +from qiskit_experiments.library.driven_freq_tuning import coefficient_utils as util from qiskit_experiments.test import FakeService @@ -48,49 +49,6 @@ def _run_spect_analysis( class TestStarkP1Spectroscopy(QiskitExperimentsTestCase): """Test case for the Stark P1 Spectroscopy experiment.""" - @staticmethod - def create_service_helper( - pos_coef_o1: float, - pos_coef_o2: float, - pos_coef_o3: float, - neg_coef_o1: float, - neg_coef_o2: float, - neg_coef_o3: float, - ferr: float, - qubit: int, - backend_name: str, - ): - """A helper method to create service with analysis results.""" - service = FakeService() - - service.create_experiment( - experiment_type="StarkRamseyXYAmpScan", - backend_name=backend_name, - experiment_id="123456789", - ) - - coeffs = { - "stark_pos_coef_o1": pos_coef_o1, - "stark_pos_coef_o2": pos_coef_o2, - "stark_pos_coef_o3": pos_coef_o3, - "stark_neg_coef_o1": neg_coef_o1, - "stark_neg_coef_o2": neg_coef_o2, - "stark_neg_coef_o3": neg_coef_o3, - "stark_ferr": ferr, - } - for i, (key, value) in enumerate(coeffs.items()): - service.create_analysis_result( - experiment_id="123456789", - result_data={"value": value}, - result_type=key, - device_components=[f"Q{qubit}"], - tags=[], - quality="Good", - verified=False, - result_id=str(i), - ) - return service - def test_linear_spaced_parameters(self): """Test generating parameters with linear spacing.""" exp = StarkP1Spectroscopy((0,)) @@ -126,96 +84,59 @@ def test_invalid_spacing(self): exp.set_experiment_options(spacing="invalid_option") def test_raises_scanning_frequency_without_service(self): - """Test raises error when frequency is set without having service or coefficients set.""" + """Test raises error when frequency is set without no coefficients. + + This covers following situations: + - stark_coefficients options is None + - backend object doesn't provide experiment service + """ exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) exp.set_experiment_options( xvals=[-100e6, -50e6, 0, 50e6, 100e6], xval_type="frequency", - stark_coefficients="latest", ) with self.assertRaises(RuntimeError): exp.parameters() - @named_data( - ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], - ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], - ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], - ) - @unpack - def test_scanning_frequency(self, po1, po2, po3, no1, no2, no3, ferr): - """Test scanning frequency with experiment service. - - This is a sort of round-trip test. - We generate amplitudes from frequencies through the experiment class. - These amplitudes are converted into frequencies again with the same coefficients. - The two sets of frequencies must be consistent. - """ - service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") - - exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) - - ref_freqs = np.linspace(-70e6, 70e6, 31) - exp.set_experiment_options( - xvals=ref_freqs, - xval_type="frequency", - service=service, - ) - - amplitudes = exp.parameters() - - # Compute frequency from parameter values with the same coefficient - analysis = StarkP1SpectAnalysis() - coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") - frequencies = analysis._convert_axis(amplitudes, coeffs) - - np.testing.assert_array_almost_equal(frequencies, ref_freqs) - def test_scanning_frequency_with_coeffs(self): - """Test scanning frequency with manually provided Stark coefficients. - - This is just a difference of API from the test_scanning_frequency. - Data driven test is omitted here. - """ - po1, po2, po3, no1, no2, no3, ferr = 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3 + """Test scanning frequency with manually provided Stark coefficients.""" + coeffs = util.StarkCoefficients( + pos_coef_o1=5e6, + pos_coef_o2=200e6, + pos_coef_o3=-50e6, + neg_coef_o1=5e6, + neg_coef_o2=-180e6, + neg_coef_o3=-40e6, + offset=100e3, + ) exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) ref_amps = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) - test_freqs = np.where( - ref_amps > 0, - po1 * ref_amps + po2 * ref_amps**2 + po3 * ref_amps**3 + ferr, - no1 * ref_amps + no2 * ref_amps**2 + no3 * ref_amps**3 + ferr, - ) + test_freqs = util.convert_amp_to_freq(ref_amps, coeffs) exp.set_experiment_options( xvals=test_freqs, xval_type="frequency", - stark_coefficients={ - "stark_pos_coef_o1": po1, - "stark_pos_coef_o2": po2, - "stark_pos_coef_o3": po3, - "stark_neg_coef_o1": no1, - "stark_neg_coef_o2": no2, - "stark_neg_coef_o3": no3, - "stark_ferr": ferr, - }, + stark_coefficients=coeffs, ) params = exp.parameters() np.testing.assert_array_almost_equal(params, ref_amps) def test_scaning_frequency_around_zero(self): """Test scanning frequency around zero.""" + coeffs = util.StarkCoefficients( + pos_coef_o1=5e6, + pos_coef_o2=100e6, + pos_coef_o3=10e6, + neg_coef_o1=-5e6, + neg_coef_o2=-100e6, + neg_coef_o3=-10e6, + offset=500e3, + ) exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) exp.set_experiment_options( xvals=[0, 500e3], xval_type="frequency", - stark_coefficients={ - "stark_pos_coef_o1": 5e6, - "stark_pos_coef_o2": 100e6, - "stark_pos_coef_o3": 10e6, - "stark_neg_coef_o1": -5e6, - "stark_neg_coef_o2": -100e6, - "stark_neg_coef_o3": -10e6, - "stark_ferr": 500e3, - }, + stark_coefficients=coeffs, ) params = exp.parameters() # Frequency offset is 500 kHz and we need negative shift to tune frequency at zero. @@ -278,50 +199,6 @@ def test_circuits(self): self.assertEqual(circs[0], qc1) self.assertEqual(circs[1], qc2) - def test_retrieve_coefficients(self): - """Test retrieving Stark coefficients from the experiment service.""" - po1, po2, po3, no1, no2, no3, ferr = 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3 - service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") - - retrieved_coeffs = StarkP1SpectAnalysis.retrieve_coefficients_from_service( - service=service, - qubit=0, - backend="fake_hanoi", - ) - self.assertDictEqual( - retrieved_coeffs, - { - "stark_pos_coef_o1": po1, - "stark_pos_coef_o2": po2, - "stark_pos_coef_o3": po3, - "stark_neg_coef_o1": no1, - "stark_neg_coef_o2": no2, - "stark_neg_coef_o3": no3, - "stark_ferr": ferr, - }, - ) - - @named_data( - ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], - ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], - ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], - ) - @unpack - def test_estimating_minmax_frequency(self, po1, po2, po3, no1, no2, no3, ferr): - """Test computing the minimum and maximum frequency within the amplitude budget.""" - service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") - analysis = StarkP1SpectAnalysis() - - coeffs = analysis.retrieve_coefficients_from_service(service, 0, "fake_hanoi") - # pylint: disable=unbalanced-tuple-unpacking - minf, maxf = analysis.estimate_minmax_frequencies(coeffs, (-0.9, 0.9)) - - amps = np.linspace(-0.9, 0.9, 101) - freqs = analysis._convert_axis(amps, coeffs) - - self.assertAlmostEqual(minf, min(freqs), delta=1e6) - self.assertAlmostEqual(maxf, max(freqs), delta=1e6) - def test_running_analysis_without_service(self): """Test running analysis without setting service to the experiment data. @@ -349,15 +226,42 @@ def test_running_analysis_with_service(self, po1, po2, po3, no1, no2, no3, ferr) This must convert x-axis into frequencies with the Stark coefficients. """ - service = self.create_service_helper(po1, po2, po3, no1, no2, no3, ferr, 0, "fake_hanoi") + mock_experiment_id = "6453f3d1-04ef-4e3b-82c6-1a92e3e066eb" + mock_result_id = "d067ae34-96db-4e8e-adc8-030305d3d404" + mock_backend = FakeHanoiV2().name + + coeffs = util.StarkCoefficients( + pos_coef_o1=po1, + pos_coef_o2=po2, + pos_coef_o3=po3, + neg_coef_o1=no1, + neg_coef_o2=no2, + neg_coef_o3=no3, + offset=ferr, + ) + + service = FakeService() + service.create_experiment( + experiment_type="StarkRamseyXYAmpScan", + backend_name=mock_backend, + experiment_id=mock_experiment_id, + ) + service.create_analysis_result( + experiment_id=mock_experiment_id, + result_data={"value": coeffs}, + result_type="stark_coefficients", + device_components=["Q0"], + tags=[], + quality="Good", + verified=False, + result_id=mock_result_id, + ) + analysis = StarkP1SpectAnalysisReturnXvals() xvals = np.linspace(-1, 1, 11) - ref_xvals = np.where( - xvals > 0, - po1 * xvals + po2 * xvals**2 + po3 * xvals**3 + ferr, - no1 * xvals + no2 * xvals**2 + no3 * xvals**3 + ferr, - ) + ref_fvals = util.convert_amp_to_freq(xvals, coeffs) + exp_data = ExperimentData( service=service, backend=FakeHanoiV2(), @@ -365,9 +269,9 @@ def test_running_analysis_with_service(self, po1, po2, po3, no1, no2, no3, ferr) exp_data.metadata.update({"physical_qubits": [0]}) for x in xvals: exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) - analysis.run(exp_data, replace_results=True) - test_xvals = exp_data.analysis_results("xvals").value - np.testing.assert_array_almost_equal(test_xvals, ref_xvals) + analysis.run(exp_data, replace_results=True).block_for_results() + test_fvals = exp_data.analysis_results("xvals").value + np.testing.assert_array_almost_equal(test_fvals, ref_fvals) def test_running_analysis_with_user_provided_coeffs(self): """Test running analysis by manually providing Stark coefficients. @@ -376,30 +280,25 @@ def test_running_analysis_with_user_provided_coeffs(self): This is just a difference of API from the test_running_analysis_with_service. Data driven test is omitted here. """ - po1, po2, po3, no1, no2, no3, ferr = 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3 + coeffs = util.StarkCoefficients( + pos_coef_o1=5e6, + pos_coef_o2=200e6, + pos_coef_o3=-50e6, + neg_coef_o1=5e6, + neg_coef_o2=-180e6, + neg_coef_o3=-40e6, + offset=100e3, + ) analysis = StarkP1SpectAnalysisReturnXvals() - analysis.set_options( - stark_coefficients={ - "stark_pos_coef_o1": po1, - "stark_pos_coef_o2": po2, - "stark_pos_coef_o3": po3, - "stark_neg_coef_o1": no1, - "stark_neg_coef_o2": no2, - "stark_neg_coef_o3": no3, - "stark_ferr": ferr, - } - ) + analysis.set_options(stark_coefficients=coeffs) xvals = np.linspace(-1, 1, 11) - ref_xvals = np.where( - xvals > 0, - po1 * xvals + po2 * xvals**2 + po3 * xvals**3 + ferr, - no1 * xvals + no2 * xvals**2 + no3 * xvals**3 + ferr, - ) + ref_fvals = util.convert_amp_to_freq(xvals, coeffs) + exp_data = ExperimentData() for x in xvals: exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) - analysis.run(exp_data, replace_results=True) - test_xvals = exp_data.analysis_results("xvals").value - np.testing.assert_array_almost_equal(test_xvals, ref_xvals) + analysis.run(exp_data, replace_results=True).block_for_results() + test_fvals = exp_data.analysis_results("xvals").value + np.testing.assert_array_almost_equal(test_fvals, ref_fvals) diff --git a/test/library/characterization/test_stark_ramsey_xy.py b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py similarity index 84% rename from test/library/characterization/test_stark_ramsey_xy.py rename to test/library/driven_freq_tuning/test_stark_ramsey_xy.py index 795fde5297..33188dcf92 100644 --- a/test/library/characterization/test_stark_ramsey_xy.py +++ b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py @@ -21,7 +21,8 @@ from qiskit.providers.fake_provider import FakeHanoiV2 from qiskit_experiments.library import StarkRamseyXY, StarkRamseyXYAmpScan -from qiskit_experiments.library.characterization.analysis import StarkRamseyXYAmpScanAnalysis +from qiskit_experiments.library.driven_freq_tuning.analyses import StarkRamseyXYAmpScanAnalysis +from qiskit_experiments.library.driven_freq_tuning import coefficient_utils as util from qiskit_experiments.framework import ExperimentData @@ -242,24 +243,33 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): exp_data = ExperimentData() exp_data.metadata.update({"stark_length": 50e-9}) + ref_coeffs = util.StarkCoefficients( + pos_coef_o1=c1p, + pos_coef_o2=c2p, + pos_coef_o3=c3p, + neg_coef_o1=c1n, + neg_coef_o2=c2n, + neg_coef_o3=c3n, + offset=ferr, + ) + yvals = util.convert_amp_to_freq(xvals, ref_coeffs) + # Generate fake data based on fit model. - for x in xvals: + for x, y in zip(xvals, yvals): if x >= 0.0: - fs = c1p * x + c2p * x**2 + c3p * x**3 + ferr direction = "pos" else: - fs = c1n * x + c2n * x**2 + c3n * x**3 + ferr direction = "neg" # Add some sampling error - ramx_count = rng.binomial(shots, amp * np.cos(const * fs) + off) + ramx_count = rng.binomial(shots, amp * np.cos(const * y) + off) exp_data.add_data( { "counts": {"0": shots - ramx_count, "1": ramx_count}, "metadata": {"xval": x, "series": "X", "direction": direction}, } ) - ramy_count = rng.binomial(shots, amp * np.sin(const * fs) + off) + ramy_count = rng.binomial(shots, amp * np.sin(const * y) + off) exp_data.add_data( { "counts": {"0": shots - ramy_count, "1": ramy_count}, @@ -271,38 +281,18 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): analysis.run(exp_data, replace_results=True) self.assertExperimentDone(exp_data) - # Check the fitted parameter can approximate the same polynominal - x_pos = np.linspace(0, 1, 51) - x_neg = np.linspace(-1, 0, 51) - ref_yvals_pos = c1p * x_pos + c2p * x_pos**2 + c3p * x_pos**3 + ferr - ref_yvals_neg = c1n * x_neg + c2n * x_neg**2 + c3n * x_neg**3 + ferr - - # Note that these parameter values are not necessary the same with input values - # as long as they can approximate the original phase polynominal. - c1p_est = exp_data.analysis_results("stark_pos_coef_o1").value.n - c2p_est = exp_data.analysis_results("stark_pos_coef_o2").value.n - c3p_est = exp_data.analysis_results("stark_pos_coef_o3").value.n - c1n_est = exp_data.analysis_results("stark_neg_coef_o1").value.n - c2n_est = exp_data.analysis_results("stark_neg_coef_o2").value.n - c3n_est = exp_data.analysis_results("stark_neg_coef_o3").value.n - ferr_est = exp_data.analysis_results("stark_ferr").value.n - - test_yvals_pos = c1p_est * x_pos + c2p_est * x_pos**2 + c3p_est * x_pos**3 + ferr_est - test_yvals_neg = c1n_est * x_neg + c2n_est * x_neg**2 + c3n_est * x_neg**3 + ferr_est - - # Check similality of reconstructed polynominals - # Curves must be agree within the torelance of 1.5 * 1 MHz. - np.testing.assert_array_almost_equal( - test_yvals_pos, - ref_yvals_pos, - decimal=-6, - err_msg="Reconstructed phase polynominal on positive frequency shift side " - "doesn't match with the original curve.", - ) + # Check the fitted parameter can approximate the same polynominal. + # Note that coefficient values don't need to exactly match as long as + # frequency shift is predictable. + # Since the fit model is just an empirical polynomial, + # comparing coefficients don't physically sound. + # Curves must be agreed within the tolerance of 1.5 * 1 MHz. + fit_coeffs = exp_data.analysis_results("stark_coefficients").value + fit_yvals = util.convert_amp_to_freq(xvals, fit_coeffs) + np.testing.assert_array_almost_equal( - test_yvals_neg, - ref_yvals_neg, + yvals, + fit_yvals, decimal=-6, - err_msg="Reconstructed phase polynominal on negative frequency shift side " - "doesn't match with the original curve.", + err_msg="Reconstructed phase polynominal doesn't match with the actual phase shift.", ) diff --git a/test/library/driven_freq_tuning/test_utils.py b/test/library/driven_freq_tuning/test_utils.py new file mode 100644 index 0000000000..11a30460c3 --- /dev/null +++ b/test/library/driven_freq_tuning/test_utils.py @@ -0,0 +1,161 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Test for Stark coefficients utility.""" + +from test.base import QiskitExperimentsTestCase + +from ddt import ddt, named_data, data, unpack +import numpy as np + +from qiskit_experiments.library.driven_freq_tuning import coefficient_utils as util +from qiskit_experiments.test import FakeService + + +@ddt +class TestStarkUtil(QiskitExperimentsTestCase): + """Test cases for Stark coefficient utilities.""" + + def test_coefficients(self): + """Test getting group of coefficients.""" + coeffs = util.StarkCoefficients( + pos_coef_o1=1e6, + pos_coef_o2=2e6, + pos_coef_o3=3e6, + neg_coef_o1=-1e6, + neg_coef_o2=-2e6, + neg_coef_o3=-3e6, + offset=0, + ) + self.assertListEqual(coeffs.positive_coeffs(), [3e6, 2e6, 1e6]) + self.assertListEqual(coeffs.negative_coeffs(), [-3e6, -2e6, -1e6]) + + @named_data( + ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], + ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], + ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], + ) + @unpack + def test_roundtrip_convert_freq_amp( + self, + pos_o1: float, + pos_o2: float, + pos_o3: float, + neg_o1: float, + neg_o2: float, + neg_o3: float, + offset: float, + ): + """Test round-trip conversion between frequency shift and Stark amplitude.""" + coeffs = util.StarkCoefficients( + pos_coef_o1=pos_o1, + pos_coef_o2=pos_o2, + pos_coef_o3=pos_o3, + neg_coef_o1=neg_o1, + neg_coef_o2=neg_o2, + neg_coef_o3=neg_o3, + offset=offset, + ) + target_freqs = np.linspace(-70e6, 70e6, 11) + test_amps = util.convert_freq_to_amp(target_freqs, coeffs) + test_freqs = util.convert_amp_to_freq(test_amps, coeffs) + + np.testing.assert_array_almost_equal(test_freqs, target_freqs, decimal=2) + + @data( + [-0.5, 0.5], + [-0.9, 0.9], + [0.25, 1.0], + ) + @unpack + def test_calculate_min_max_shift(self, min_amp, max_amp): + """Test estimating maximum frequency shift within given Stark amplitude budget.""" + + # These coefficients induce inflection points around ±0.75, for testing + coeffs = util.StarkCoefficients( + pos_coef_o1=10e6, + pos_coef_o2=100e6, + pos_coef_o3=-90e6, + neg_coef_o1=80e6, + neg_coef_o2=-180e6, + neg_coef_o3=-200e6, + offset=100e3, + ) + # This numerical solution is correct up to amp resolution of 0.001 + nop = int((max_amp - min_amp) / 0.001) + amps = np.linspace(min_amp, max_amp, nop) + freqs = util.convert_amp_to_freq(amps, coeffs) + + # This finds strict solution, unless it has a bug + min_freq, max_freq = util.find_min_max_frequency( + min_amp=min_amp, + max_amp=max_amp, + coeffs=coeffs, + ) + + # Allow 1kHz tolerance because ref is approximate value + self.assertAlmostEqual(min_freq, np.min(freqs), delta=1e3) + self.assertAlmostEqual(max_freq, np.max(freqs), delta=1e3) + + def test_get_coeffs_from_service(self): + """Test retrieve the saved Stark coefficients from the experiment service.""" + mock_experiment_id = "6453f3d1-04ef-4e3b-82c6-1a92e3e066eb" + mock_result_id = "d067ae34-96db-4e8e-adc8-030305d3d404" + mock_backend = "mock_backend" + + ref_coeffs = util.StarkCoefficients( + pos_coef_o1=1e6, + pos_coef_o2=2e6, + pos_coef_o3=3e6, + neg_coef_o1=-1e6, + neg_coef_o2=-2e6, + neg_coef_o3=-3e6, + offset=0, + ) + + service = FakeService() + service.create_experiment( + experiment_type="StarkRamseyXYAmpScan", + backend_name=mock_backend, + experiment_id=mock_experiment_id, + ) + service.create_analysis_result( + experiment_id=mock_experiment_id, + result_data={"value": ref_coeffs}, + result_type="stark_coefficients", + device_components=["Q0"], + tags=[], + quality="Good", + verified=False, + result_id=mock_result_id, + ) + + retrieved = util.retrieve_coefficients_from_service( + service=service, + backend_name=mock_backend, + qubit=0, + ) + + self.assertEqual(retrieved, ref_coeffs) + + def test_get_coeffs_no_data(self): + """Test raises when Stark coefficients don't exist in the result database.""" + mock_backend = "mock_backend" + + service = FakeService() + + with self.assertRaises(RuntimeError): + util.retrieve_coefficients_from_service( + service=service, + backend_name=mock_backend, + qubit=0, + ) From 353f3770e4203986d05365f84e3cb7a3f3ae10f8 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 29 Jan 2024 11:29:57 +0900 Subject: [PATCH 08/15] Wording Co-authored-by: Will Shanks --- .../library/driven_freq_tuning/coefficient_utils.py | 6 ++---- test/library/driven_freq_tuning/test_stark_p1_spect.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py b/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py index 01ddffeb62..20b58ea81c 100644 --- a/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py +++ b/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py @@ -91,8 +91,6 @@ def convert_freq_to_amp( fit = [*coeffs.negative_coeffs(), -shift] amp_candidates = np.roots(fit) # Because the fit function is third order, we get three solutions here. - # Only one valid solution must exist because we assume - # a monotonic trend for Stark shift against tone amplitude in domain of definition. criteria = np.all( [ # Frequency shift and tone have the same sign by definition @@ -110,8 +108,8 @@ def convert_freq_to_amp( if len(valid_amps) > 1: # We assume a monotonic trend but sometimes a large third-order term causes # inflection point and inverts the trend in larger amplitudes. - # In this case we would have more than one solutions, but we can - # take the smallerst amplitude before reaching to the inflection point. + # In this case we would have more than one solution, but we can + # take the smallest amplitude before reaching to the inflection point. before_inflection = np.argmin(np.abs(valid_amps.real)) valid_amp = float(valid_amps[before_inflection].real) else: diff --git a/test/library/driven_freq_tuning/test_stark_p1_spect.py b/test/library/driven_freq_tuning/test_stark_p1_spect.py index f4fb1f2e13..fe845a0255 100644 --- a/test/library/driven_freq_tuning/test_stark_p1_spect.py +++ b/test/library/driven_freq_tuning/test_stark_p1_spect.py @@ -121,7 +121,7 @@ def test_scanning_frequency_with_coeffs(self): params = exp.parameters() np.testing.assert_array_almost_equal(params, ref_amps) - def test_scaning_frequency_around_zero(self): + def test_scanning_frequency_around_zero(self): """Test scanning frequency around zero.""" coeffs = util.StarkCoefficients( pos_coef_o1=5e6, From 7675cbbbb093ce9ef4d2814aa732c6164ac05f5f Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 29 Jan 2024 12:07:02 +0900 Subject: [PATCH 09/15] Code structure change - Convert util functions to class methods. Change file name to restrict the context. - Split analysis file --- .../library/driven_freq_tuning/__init__.py | 13 +- .../library/driven_freq_tuning/coefficient.py | 266 ++++++++++++++++++ .../driven_freq_tuning/coefficient_utils.py | 232 --------------- .../library/driven_freq_tuning/p1_spect.py | 7 +- .../driven_freq_tuning/p1_spect_analysis.py | 161 +++++++++++ .../driven_freq_tuning/ramsey_amp_scan.py | 2 +- ...nalyses.py => ramsey_amp_scan_analysis.py} | 150 +--------- .../{test_utils.py => test_coeffs.py} | 24 +- .../driven_freq_tuning/test_stark_p1_spect.py | 18 +- .../test_stark_ramsey_xy.py | 12 +- 10 files changed, 473 insertions(+), 412 deletions(-) create mode 100644 qiskit_experiments/library/driven_freq_tuning/coefficient.py delete mode 100644 qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py rename qiskit_experiments/library/driven_freq_tuning/{analyses.py => ramsey_amp_scan_analysis.py} (75%) rename test/library/driven_freq_tuning/{test_utils.py => test_coeffs.py} (88%) diff --git a/qiskit_experiments/library/driven_freq_tuning/__init__.py b/qiskit_experiments/library/driven_freq_tuning/__init__.py index 9cdd2faf99..f59a353f36 100644 --- a/qiskit_experiments/library/driven_freq_tuning/__init__.py +++ b/qiskit_experiments/library/driven_freq_tuning/__init__.py @@ -39,28 +39,25 @@ StarkP1SpectAnalysis -Utilities -========= +Stark Coefficient +================= .. autosummary:: :toctree: ../stubs/ StarkCoefficients - convert_amp_to_freq - convert_freq_to_amp retrieve_coefficients_from_backend retrieve_coefficients_from_service """ -from .analyses import StarkRamseyXYAmpScanAnalysis, StarkP1SpectAnalysis +from .ramsey_amp_scan_analysis import StarkRamseyXYAmpScanAnalysis +from .p1_spect_analysis import StarkP1SpectAnalysis from .ramsey import StarkRamseyXY from .ramsey_amp_scan import StarkRamseyXYAmpScan from .p1_spect import StarkP1Spectroscopy -from .coefficient_utils import ( +from .coefficient import ( StarkCoefficients, - convert_amp_to_freq, - convert_freq_to_amp, retrieve_coefficients_from_backend, retrieve_coefficients_from_service, ) diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficient.py b/qiskit_experiments/library/driven_freq_tuning/coefficient.py new file mode 100644 index 0000000000..19f7542e38 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/coefficient.py @@ -0,0 +1,266 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. +"""Coefficients characterizing Stark shift.""" + +from __future__ import annotations +from typing import Any + +import numpy as np + +from qiskit.providers.backend import Backend +from qiskit_ibm_experiment.service import IBMExperimentService +from qiskit_ibm_experiment.exceptions import IBMApiError + +from qiskit_experiments.framework.backend_data import BackendData +from qiskit_experiments.framework.experiment_data import ExperimentData + + +class StarkCoefficients: + """A collection of coefficients characterizing Stark shift.""" + + def __init__( + self, + pos_coef_o1: float, + pos_coef_o2: float, + pos_coef_o3: float, + neg_coef_o1: float, + neg_coef_o2: float, + neg_coef_o3: float, + offset: float, + ): + """Create new coefficients object. + + Args: + pos_coef_o1: The first order shift coefficient on positive amplitude. + pos_coef_o2: The second order shift coefficient on positive amplitude. + pos_coef_o3: The third order shift coefficient on positive amplitude. + neg_coef_o1: The first order shift coefficient on negative amplitude. + neg_coef_o2: The second order shift coefficient on negative amplitude. + neg_coef_o3: The third order shift coefficient on negative amplitude. + offset: Offset frequency. + """ + self.pos_coef_o1 = pos_coef_o1 + self.pos_coef_o2 = pos_coef_o2 + self.pos_coef_o3 = pos_coef_o3 + self.neg_coef_o1 = neg_coef_o1 + self.neg_coef_o2 = neg_coef_o2 + self.neg_coef_o3 = neg_coef_o3 + self.offset = offset + + def positive_coeffs(self) -> list[float]: + """Positive coefficients.""" + return [self.pos_coef_o3, self.pos_coef_o2, self.pos_coef_o1] + + def negative_coeffs(self) -> list[float]: + """Negative coefficients.""" + return [self.neg_coef_o3, self.neg_coef_o2, self.neg_coef_o1] + + def convert_freq_to_amp( + self, + freqs: np.ndarray, + ) -> np.ndarray: + """A helper function to convert Stark frequency to amplitude. + + Args: + freqs: Target frequency shifts to compute required Stark amplitude. + + Returns: + Estimated Stark amplitudes to induce input frequency shifts. + + Raises: + ValueError: When amplitude value cannot be solved. + """ + amplitudes = np.zeros_like(freqs) + for idx, freq in enumerate(freqs): + shift = freq - self.offset + if np.isclose(shift, 0.0): + amplitudes[idx] = 0.0 + continue + if shift > 0: + fit = [*self.positive_coeffs(), -shift] + else: + fit = [*self.negative_coeffs(), -shift] + amp_candidates = np.roots(fit) + # Because the fit function is third order, we get three solutions here. + criteria = np.all( + [ + # Frequency shift and tone have the same sign by definition + np.sign(amp_candidates.real) == np.sign(shift), + # Tone amplitude is a real value + np.isclose(amp_candidates.imag, 0.0), + # The absolute value of tone amplitude must be less than 1.0 + 10 mp + np.abs(amp_candidates.real) < 1.0 + 10 * np.finfo(float).eps, + ], + axis=0, + ) + valid_amps = amp_candidates[criteria] + if len(valid_amps) == 0: + raise ValueError(f"Stark shift at frequency value of {freq} Hz is not available.") + if len(valid_amps) > 1: + # We assume a monotonic trend but sometimes a large third-order term causes + # inflection point and inverts the trend in larger amplitudes. + # In this case we would have more than one solution, but we can + # take the smallest amplitude before reaching to the inflection point. + before_inflection = np.argmin(np.abs(valid_amps.real)) + valid_amp = float(valid_amps[before_inflection].real) + else: + valid_amp = float(valid_amps[0].real) + amplitudes[idx] = min(valid_amp, 1.0) + return amplitudes + + def convert_amp_to_freq( + self, + amps: np.ndarray, + ) -> np.ndarray: + """A helper function to convert Stark amplitude to frequency shift. + + Args: + amps: Amplitude values to convert into frequency shift. + + Returns: + Calculated frequency shift at given Stark amplitude. + """ + pos_fit = np.poly1d([*self.positive_coeffs(), self.offset]) + neg_fit = np.poly1d([*self.negative_coeffs(), self.offset]) + + return np.where(amps > 0, pos_fit(amps), neg_fit(amps)) + + def find_min_max_frequency( + self, + min_amp: float, + max_amp: float, + ) -> tuple[float, float]: + """A helper function to estimate maximum frequency shift within given amplitude budget. + + Args: + min_amp: Minimum Stark amplitude. + max_amp: Maximum Stark amplitude. + + Returns: + Minimum and maximum frequency shift available within the amplitude range. + """ + trim_amps = [] + for amp in [min_amp, max_amp]: + if amp > 0: + fit = self.positive_coeffs() + else: + fit = self.negative_coeffs() + # Solve for inflection points by computing the point where derivative becomes zero. + solutions = np.roots([deriv * coeff for deriv, coeff in zip((3, 2, 1), fit)]) + inflection_points = solutions[ + (solutions.imag == 0) & (np.sign(solutions) == np.sign(amp)) + ] + if len(inflection_points) > 0: + # When multiple inflection points are found, use the most outer one. + # There could be a small inflection point around amp=0, + # when the first order term is significant. + amp = min([amp, max(inflection_points, key=abs)], key=abs) + trim_amps.append(amp) + return tuple(self.convert_amp_to_freq(np.asarray(trim_amps))) + + def __str__(self): + # Short representation for dataframe + return "StarkCoefficients(...)" + + def __eq__(self, other): + return all( + [ + self.pos_coef_o1 == other.pos_coef_o1, + self.pos_coef_o2 == other.pos_coef_o2, + self.pos_coef_o3 == other.pos_coef_o3, + self.neg_coef_o1 == other.neg_coef_o1, + self.neg_coef_o2 == other.neg_coef_o2, + self.neg_coef_o3 == other.neg_coef_o3, + ] + ) + + def __json_encode__(self) -> dict[str, Any]: + return { + "class": "StarkCoefficients", + "data": { + "pos_coef_o1": self.pos_coef_o1, + "pos_coef_o2": self.pos_coef_o2, + "pos_coef_o3": self.pos_coef_o3, + "neg_coef_o1": self.neg_coef_o1, + "neg_coef_o2": self.neg_coef_o2, + "neg_coef_o3": self.neg_coef_o3, + "offset": self.offset, + }, + } + + @classmethod + def __json_decode__(cls, value: dict[str, Any]) -> "StarkCoefficients": + if not value.get("class", None) == "StarkCoefficients": + raise ValueError("JSON decoded value for StarkCoefficients is not valid class type.") + return StarkCoefficients(**value.get("data", {})) + + +def retrieve_coefficients_from_service( + service: IBMExperimentService, + backend_name: str, + qubit: int, +) -> StarkCoefficients: + """Retrieve StarkCoefficients object from experiment service. + + Args: + service: IBM Experiment service instance interfacing with result database. + backend_name: Name of target backend. + qubit: Index of qubit. + + Returns: + StarkCoefficients object. + + Raises: + RuntimeError: When stark_coefficients entry doesn't exist in the service. + """ + try: + retrieved = service.analysis_results( + device_components=[f"Q{qubit}"], + result_type="stark_coefficients", + backend_name=backend_name, + sort_by=["creation_datetime:desc"], + ) + except (IBMApiError, ValueError) as ex: + raise RuntimeError( + f"Failed to retrieve the result of stark_coefficients: {ex.message}" + ) from ex + if len(retrieved) == 0: + raise RuntimeError( + "Analysis result of stark_coefficients is not found in the " + "experiment service. Run and save the result of StarkRamseyXYAmpScan." + ) + return retrieved[0].result_data["value"] + + +def retrieve_coefficients_from_backend( + backend: Backend, + qubit: int, +) -> StarkCoefficients: + """Retrieve StarkCoefficients object from the Qiskit backend. + + Args: + backend: Qiskit backend object. + qubit: Index of qubit. + + Returns: + StarkCoefficients object. + + Raises: + RuntimeError: When experiment service cannot be loaded from backend. + """ + name = BackendData(backend).name + service = ExperimentData.get_service_from_backend(backend) + + if service is None: + raise RuntimeError(f"Valid experiment service is not found for the backend {name}.") + + return retrieve_coefficients_from_service(service, name, qubit) diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py b/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py deleted file mode 100644 index 20b58ea81c..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/coefficient_utils.py +++ /dev/null @@ -1,232 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2024. -# -# 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. -"""Coefficients characterizing Stark shift.""" - -from __future__ import annotations - -from dataclasses import dataclass - -import numpy as np - -from qiskit.providers.backend import Backend -from qiskit_ibm_experiment.service import IBMExperimentService -from qiskit_ibm_experiment.exceptions import IBMApiError - -from qiskit_experiments.framework.backend_data import BackendData -from qiskit_experiments.framework.experiment_data import ExperimentData - - -@dataclass -class StarkCoefficients: - """A dataclass representing a set of coefficients characterizing Stark shift.""" - - pos_coef_o1: float - """The first order shift coefficient on positive amplitude.""" - - pos_coef_o2: float - """The second order shift coefficient on positive amplitude.""" - - pos_coef_o3: float - """The third order shift coefficient on positive amplitude.""" - - neg_coef_o1: float - """The first order shift coefficient on negative amplitude.""" - - neg_coef_o2: float - """The second order shift coefficient on negative amplitude.""" - - neg_coef_o3: float - """The third order shift coefficient on negative amplitude.""" - - offset: float - """Offset frequency.""" - - def positive_coeffs(self) -> list[float]: - """Positive coefficients.""" - return [self.pos_coef_o3, self.pos_coef_o2, self.pos_coef_o1] - - def negative_coeffs(self) -> list[float]: - """Negative coefficients.""" - return [self.neg_coef_o3, self.neg_coef_o2, self.neg_coef_o1] - - def __str__(self): - # Short representation for dataframe - return "StarkCoefficients(...)" - - -def convert_freq_to_amp( - freqs: np.ndarray, - coeffs: StarkCoefficients, -) -> np.ndarray: - """A helper function to convert Stark frequency to amplitude. - - Args: - freqs: Target frequency shifts to compute required Stark amplitude. - coeffs: Calibrated Stark coefficients. - - Returns: - Estimated Stark amplitudes to induce input frequency shifts. - - Raises: - ValueError: When amplitude value cannot be solved. - """ - amplitudes = np.zeros_like(freqs) - for idx, freq in enumerate(freqs): - shift = freq - coeffs.offset - if np.isclose(shift, 0.0): - amplitudes[idx] = 0.0 - continue - if shift > 0: - fit = [*coeffs.positive_coeffs(), -shift] - else: - fit = [*coeffs.negative_coeffs(), -shift] - amp_candidates = np.roots(fit) - # Because the fit function is third order, we get three solutions here. - criteria = np.all( - [ - # Frequency shift and tone have the same sign by definition - np.sign(amp_candidates.real) == np.sign(shift), - # Tone amplitude is a real value - np.isclose(amp_candidates.imag, 0.0), - # The absolute value of tone amplitude must be less than 1.0 + 10 mp - np.abs(amp_candidates.real) < 1.0 + 10 * np.finfo(float).eps, - ], - axis=0, - ) - valid_amps = amp_candidates[criteria] - if len(valid_amps) == 0: - raise ValueError(f"Stark shift at frequency value of {freq} Hz is not available.") - if len(valid_amps) > 1: - # We assume a monotonic trend but sometimes a large third-order term causes - # inflection point and inverts the trend in larger amplitudes. - # In this case we would have more than one solution, but we can - # take the smallest amplitude before reaching to the inflection point. - before_inflection = np.argmin(np.abs(valid_amps.real)) - valid_amp = float(valid_amps[before_inflection].real) - else: - valid_amp = float(valid_amps[0].real) - amplitudes[idx] = min(valid_amp, 1.0) - return amplitudes - - -def convert_amp_to_freq( - amps: np.ndarray, - coeffs: StarkCoefficients, -) -> np.ndarray: - """A helper function to convert Stark amplitude to frequency shift. - - Args: - amps: Amplitude values to convert into frequency shift. - coeffs: Calibrated Stark coefficients. - - Returns: - Calculated frequency shift at given Stark amplitude. - """ - pos_fit = np.poly1d([*coeffs.positive_coeffs(), coeffs.offset]) - neg_fit = np.poly1d([*coeffs.negative_coeffs(), coeffs.offset]) - - return np.where(amps > 0, pos_fit(amps), neg_fit(amps)) - - -def find_min_max_frequency( - min_amp: float, - max_amp: float, - coeffs: StarkCoefficients, -) -> tuple[float, float]: - """A helper function to estimate maximum frequency shift within given amplitude budget. - - Args: - min_amp: Minimum Stark amplitude. - max_amp: Maximum Stark amplitude. - coeffs: Calibrated Stark coefficients. - - Returns: - Minimum and maximum frequency shift available within the amplitude range. - """ - trim_amps = [] - for amp in [min_amp, max_amp]: - if amp > 0: - fit = coeffs.positive_coeffs() - else: - fit = coeffs.negative_coeffs() - # Solve for inflection points by computing the point where derivative becomes zero. - solutions = np.roots([deriv * coeff for deriv, coeff in zip((3, 2, 1), fit)]) - inflection_points = solutions[(solutions.imag == 0) & (np.sign(solutions) == np.sign(amp))] - if len(inflection_points) > 0: - # When multiple inflection points are found, use the most outer one. - # There could be a small inflection point around amp=0, - # when the first order term is significant. - amp = min([amp, max(inflection_points, key=abs)], key=abs) - trim_amps.append(amp) - return tuple(convert_amp_to_freq(np.asarray(trim_amps), coeffs)) - - -def retrieve_coefficients_from_service( - service: IBMExperimentService, - backend_name: str, - qubit: int, -) -> StarkCoefficients: - """Retrieve StarkCoefficients object from experiment service. - - Args: - service: IBM Experiment service instance interfacing with result database. - backend_name: Name of target backend. - qubit: Index of qubit. - - Returns: - StarkCoefficients object. - - Raises: - RuntimeError: When stark_coefficients entry doesn't exist in the service. - """ - try: - retrieved = service.analysis_results( - device_components=[f"Q{qubit}"], - result_type="stark_coefficients", - backend_name=backend_name, - sort_by=["creation_datetime:desc"], - ) - except (IBMApiError, ValueError) as ex: - raise RuntimeError( - f"Failed to retrieve the result of stark_coefficients: {ex.message}" - ) from ex - if len(retrieved) == 0: - raise RuntimeError( - "Analysis result of stark_coefficients is not found in the " - "experiment service. Run and save the result of StarkRamseyXYAmpScan." - ) - return retrieved[0].result_data["value"] - - -def retrieve_coefficients_from_backend( - backend: Backend, - qubit: int, -) -> StarkCoefficients: - """Retrieve StarkCoefficients object from the Qiskit backend. - - Args: - backend: Qiskit backend object. - qubit: Index of qubit. - - Returns: - StarkCoefficients object. - - Raises: - RuntimeError: When experiment service cannot be loaded from backend. - """ - name = BackendData(backend).name - service = ExperimentData.get_service_from_backend(backend) - - if service is None: - raise RuntimeError(f"Valid experiment service is not found for the backend {name}.") - - return retrieve_coefficients_from_service(service, name, qubit) diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py index a104087853..3404a10f20 100644 --- a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py @@ -22,11 +22,10 @@ from qiskit.utils import optionals as _optional from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options -from .analyses import StarkP1SpectAnalysis +from .p1_spect_analysis import StarkP1SpectAnalysis -from .coefficient_utils import ( +from .coefficient import ( StarkCoefficients, - convert_freq_to_amp, retrieve_coefficients_from_backend, ) @@ -188,7 +187,7 @@ def parameters(self) -> np.ndarray: backend=self.backend, qubit=self.physical_qubits[0], ) - return convert_freq_to_amp(freqs=params, coeffs=coeffs) + return coeffs.convert_freq_to_amp(freqs=params) return params def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py new file mode 100644 index 0000000000..609375fbba --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py @@ -0,0 +1,161 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +"""P1 spectroscopy analyses.""" + +from __future__ import annotations + +import numpy as np +from uncertainties import unumpy as unp + +import qiskit_experiments.data_processing as dp +import qiskit_experiments.visualization as vis +from qiskit_experiments.data_processing.exceptions import DataProcessorError +from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options +from .coefficient import ( + StarkCoefficients, + retrieve_coefficients_from_service, +) + + +class StarkP1SpectAnalysis(BaseAnalysis): + """Analysis class for StarkP1Spectroscopy. + + # section: overview + + The P1 landscape is hardly predictable because of the random appearance of + lossy TLS notches, and hence this analysis doesn't provide any + generic mathematical model to fit the measurement data. + A developer may subclass this to conduct own analysis. + + This analysis just visualizes the measured P1 values against Stark tone amplitudes. + The tone amplitudes can be converted into the amount of Stark shift + when the calibrated coefficients are provided in the analysis option, + or the calibration experiment results are available in the result database. + + # section: see_also + :class:`qiskit_experiments.library.driven_freq_tuning.StarkRamseyXYAmpScan` + + """ + + @property + def plotter(self) -> vis.CurvePlotter: + """Curve plotter instance.""" + return self.options.plotter + + @classmethod + def _default_options(cls) -> Options: + """Default analysis options. + + Analysis Options: + plotter (Plotter): Plotter to visualize P1 landscape. + data_processor (DataProcessor): Data processor to compute P1 value. + stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to + convert tone amplitudes into amount of Stark shift. This dictionary must include + all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, + which are calibrated with :class:`.StarkRamseyXYAmpScan`. + Alternatively, it searches for these coefficients in the result database + when "latest" is set. This requires having the experiment service set in + the experiment data to analyze. + x_key (str): Key of the circuit metadata to represent x value. + """ + options = super()._default_options() + + p1spect_plotter = vis.CurvePlotter(vis.MplDrawer()) + p1spect_plotter.set_figure_options( + xlabel="Stark amplitude", + ylabel="P(1)", + xscale="quadratic", + ) + + options.update_options( + plotter=p1spect_plotter, + data_processor=dp.DataProcessor("counts", [dp.Probability("1")]), + stark_coefficients=None, + x_key="xval", + ) + options.set_validator("stark_coefficients", StarkCoefficients) + + return options + + # pylint: disable=unused-argument + def _run_spect_analysis( + self, + xdata: np.ndarray, + ydata: np.ndarray, + ydata_err: np.ndarray, + ) -> list[AnalysisResultData]: + """Run further analysis on the spectroscopy data. + + .. note:: + A subclass can overwrite this method to conduct analysis. + + Args: + xdata: X values. This is either amplitudes or frequencies. + ydata: Y values. This is P1 values measured at different Stark tones. + ydata_err: Sampling error of the Y values. + + Returns: + A list of analysis results. + """ + return [] + + def _run_analysis( + self, + experiment_data: ExperimentData, + ) -> tuple[list[AnalysisResultData], list["matplotlib.figure.Figure"]]: + + x_key = self.options.x_key + + # Get calibrated Stark tone coefficients + if self.options.stark_coefficients is None and experiment_data.service is not None: + # Get value from service + stark_coeffs = retrieve_coefficients_from_service( + service=experiment_data.service, + backend_name=experiment_data.backend_name, + qubit=experiment_data.metadata["physical_qubits"][0], + ) + else: + stark_coeffs = self.options.stark_coefficients + + # Compute P1 value and sampling error + data = experiment_data.data() + try: + xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float) + except KeyError as ex: + raise DataProcessorError( + f"X value key {x_key} is not defined in circuit metadata." + ) from ex + ydata_ufloat = self.options.data_processor(data) + ydata = unp.nominal_values(ydata_ufloat) + ydata_err = unp.std_devs(ydata_ufloat) + + # Convert x-axis of amplitudes into Stark shift by consuming calibrated parameters. + if isinstance(stark_coeffs, StarkCoefficients): + xdata = stark_coeffs.convert_amp_to_freq(amps=xdata) + self.plotter.set_figure_options( + xlabel="Stark shift", + xval_unit="Hz", + xscale="linear", + ) + + # Draw figures and create analysis results. + self.plotter.set_series_data( + series_name="stark_p1", + x_formatted=xdata, + y_formatted=ydata, + y_formatted_err=ydata_err, + x_interp=xdata, + y_interp=ydata, + ) + analysis_results = self._run_spect_analysis(xdata, ydata, ydata_err) + + return analysis_results, [self.plotter.figure()] diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py index d04eb607d1..528c3fd8bd 100644 --- a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py @@ -22,7 +22,7 @@ from qiskit.utils import optionals as _optional from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming -from .analyses import StarkRamseyXYAmpScanAnalysis +from .ramsey_amp_scan_analysis import StarkRamseyXYAmpScanAnalysis if _optional.HAS_SYMENGINE: import symengine as sym diff --git a/qiskit_experiments/library/driven_freq_tuning/analyses.py b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py similarity index 75% rename from qiskit_experiments/library/driven_freq_tuning/analyses.py rename to qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py index 67f510a6f6..3b92c4bfbc 100644 --- a/qiskit_experiments/library/driven_freq_tuning/analyses.py +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py @@ -9,7 +9,7 @@ # 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. -"""Stark shift analyses.""" +"""Ramsey amplitude scan analysis.""" from __future__ import annotations @@ -20,15 +20,9 @@ from uncertainties import unumpy as unp import qiskit_experiments.curve_analysis as curve -import qiskit_experiments.data_processing as dp import qiskit_experiments.visualization as vis -from qiskit_experiments.data_processing.exceptions import DataProcessorError -from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options -from .coefficient_utils import ( - StarkCoefficients, - convert_amp_to_freq, - retrieve_coefficients_from_service, -) +from qiskit_experiments.framework import ExperimentData, AnalysisResultData +from .coefficient import StarkCoefficients class StarkRamseyXYAmpScanAnalysis(curve.CurveAnalysis): @@ -444,141 +438,3 @@ def _initialize( # Set scaling factor to convert phase to frequency if "stark_length" in experiment_data.metadata: self.set_options(pulse_len=experiment_data.metadata["stark_length"]) - - -class StarkP1SpectAnalysis(BaseAnalysis): - """Analysis class for StarkP1Spectroscopy. - - # section: overview - - The P1 landscape is hardly predictable because of the random appearance of - lossy TLS notches, and hence this analysis doesn't provide any - generic mathematical model to fit the measurement data. - A developer may subclass this to conduct own analysis. - - This analysis just visualizes the measured P1 values against Stark tone amplitudes. - The tone amplitudes can be converted into the amount of Stark shift - when the calibrated coefficients are provided in the analysis option, - or the calibration experiment results are available in the result database. - - # section: see_also - :class:`qiskit_experiments.library.driven_freq_tuning.StarkRamseyXYAmpScan` - - """ - - @property - def plotter(self) -> vis.CurvePlotter: - """Curve plotter instance.""" - return self.options.plotter - - @classmethod - def _default_options(cls) -> Options: - """Default analysis options. - - Analysis Options: - plotter (Plotter): Plotter to visualize P1 landscape. - data_processor (DataProcessor): Data processor to compute P1 value. - stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to - convert tone amplitudes into amount of Stark shift. This dictionary must include - all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, - which are calibrated with :class:`.StarkRamseyXYAmpScan`. - Alternatively, it searches for these coefficients in the result database - when "latest" is set. This requires having the experiment service set in - the experiment data to analyze. - x_key (str): Key of the circuit metadata to represent x value. - """ - options = super()._default_options() - - p1spect_plotter = vis.CurvePlotter(vis.MplDrawer()) - p1spect_plotter.set_figure_options( - xlabel="Stark amplitude", - ylabel="P(1)", - xscale="quadratic", - ) - - options.update_options( - plotter=p1spect_plotter, - data_processor=dp.DataProcessor("counts", [dp.Probability("1")]), - stark_coefficients=None, - x_key="xval", - ) - options.set_validator("stark_coefficients", StarkCoefficients) - - return options - - # pylint: disable=unused-argument - def _run_spect_analysis( - self, - xdata: np.ndarray, - ydata: np.ndarray, - ydata_err: np.ndarray, - ) -> list[AnalysisResultData]: - """Run further analysis on the spectroscopy data. - - .. note:: - A subclass can overwrite this method to conduct analysis. - - Args: - xdata: X values. This is either amplitudes or frequencies. - ydata: Y values. This is P1 values measured at different Stark tones. - ydata_err: Sampling error of the Y values. - - Returns: - A list of analysis results. - """ - return [] - - def _run_analysis( - self, - experiment_data: ExperimentData, - ) -> tuple[list[AnalysisResultData], list["matplotlib.figure.Figure"]]: - - x_key = self.options.x_key - - # Get calibrated Stark tone coefficients - if self.options.stark_coefficients is None and experiment_data.service is not None: - # Get value from service - stark_coeffs = retrieve_coefficients_from_service( - service=experiment_data.service, - backend_name=experiment_data.backend_name, - qubit=experiment_data.metadata["physical_qubits"][0], - ) - else: - stark_coeffs = self.options.stark_coefficients - - # Compute P1 value and sampling error - data = experiment_data.data() - try: - xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float) - except KeyError as ex: - raise DataProcessorError( - f"X value key {x_key} is not defined in circuit metadata." - ) from ex - ydata_ufloat = self.options.data_processor(data) - ydata = unp.nominal_values(ydata_ufloat) - ydata_err = unp.std_devs(ydata_ufloat) - - # Convert x-axis of amplitudes into Stark shift by consuming calibrated parameters. - if isinstance(stark_coeffs, StarkCoefficients): - xdata = convert_amp_to_freq( - amps=xdata, - coeffs=stark_coeffs, - ) - self.plotter.set_figure_options( - xlabel="Stark shift", - xval_unit="Hz", - xscale="linear", - ) - - # Draw figures and create analysis results. - self.plotter.set_series_data( - series_name="stark_p1", - x_formatted=xdata, - y_formatted=ydata, - y_formatted_err=ydata_err, - x_interp=xdata, - y_interp=ydata, - ) - analysis_results = self._run_spect_analysis(xdata, ydata, ydata_err) - - return analysis_results, [self.plotter.figure()] diff --git a/test/library/driven_freq_tuning/test_utils.py b/test/library/driven_freq_tuning/test_coeffs.py similarity index 88% rename from test/library/driven_freq_tuning/test_utils.py rename to test/library/driven_freq_tuning/test_coeffs.py index 11a30460c3..954a243357 100644 --- a/test/library/driven_freq_tuning/test_utils.py +++ b/test/library/driven_freq_tuning/test_coeffs.py @@ -17,7 +17,7 @@ from ddt import ddt, named_data, data, unpack import numpy as np -from qiskit_experiments.library.driven_freq_tuning import coefficient_utils as util +from qiskit_experiments.library.driven_freq_tuning import coefficient as util from qiskit_experiments.test import FakeService @@ -39,6 +39,19 @@ def test_coefficients(self): self.assertListEqual(coeffs.positive_coeffs(), [3e6, 2e6, 1e6]) self.assertListEqual(coeffs.negative_coeffs(), [-3e6, -2e6, -1e6]) + def test_roundtrip_coefficients(self): + """Test serializing and deserializing the coefficient object.""" + coeffs = util.StarkCoefficients( + pos_coef_o1=1e6, + pos_coef_o2=2e6, + pos_coef_o3=3e6, + neg_coef_o1=-1e6, + neg_coef_o2=-2e6, + neg_coef_o3=-3e6, + offset=0, + ) + self.assertRoundTripSerializable(coeffs) + @named_data( ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], @@ -66,8 +79,8 @@ def test_roundtrip_convert_freq_amp( offset=offset, ) target_freqs = np.linspace(-70e6, 70e6, 11) - test_amps = util.convert_freq_to_amp(target_freqs, coeffs) - test_freqs = util.convert_amp_to_freq(test_amps, coeffs) + test_amps = coeffs.convert_freq_to_amp(target_freqs) + test_freqs = coeffs.convert_amp_to_freq(test_amps) np.testing.assert_array_almost_equal(test_freqs, target_freqs, decimal=2) @@ -93,13 +106,12 @@ def test_calculate_min_max_shift(self, min_amp, max_amp): # This numerical solution is correct up to amp resolution of 0.001 nop = int((max_amp - min_amp) / 0.001) amps = np.linspace(min_amp, max_amp, nop) - freqs = util.convert_amp_to_freq(amps, coeffs) + freqs = coeffs.convert_amp_to_freq(amps) # This finds strict solution, unless it has a bug - min_freq, max_freq = util.find_min_max_frequency( + min_freq, max_freq = coeffs.find_min_max_frequency( min_amp=min_amp, max_amp=max_amp, - coeffs=coeffs, ) # Allow 1kHz tolerance because ref is approximate value diff --git a/test/library/driven_freq_tuning/test_stark_p1_spect.py b/test/library/driven_freq_tuning/test_stark_p1_spect.py index fe845a0255..02751f1ba5 100644 --- a/test/library/driven_freq_tuning/test_stark_p1_spect.py +++ b/test/library/driven_freq_tuning/test_stark_p1_spect.py @@ -23,8 +23,8 @@ from qiskit_experiments.framework import ExperimentData, AnalysisResultData from qiskit_experiments.library import StarkP1Spectroscopy -from qiskit_experiments.library.driven_freq_tuning.analyses import StarkP1SpectAnalysis -from qiskit_experiments.library.driven_freq_tuning import coefficient_utils as util +from qiskit_experiments.library.driven_freq_tuning.p1_spect_analysis import StarkP1SpectAnalysis +from qiskit_experiments.library.driven_freq_tuning.coefficient import StarkCoefficients from qiskit_experiments.test import FakeService @@ -100,7 +100,7 @@ def test_raises_scanning_frequency_without_service(self): def test_scanning_frequency_with_coeffs(self): """Test scanning frequency with manually provided Stark coefficients.""" - coeffs = util.StarkCoefficients( + coeffs = StarkCoefficients( pos_coef_o1=5e6, pos_coef_o2=200e6, pos_coef_o3=-50e6, @@ -112,7 +112,7 @@ def test_scanning_frequency_with_coeffs(self): exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) ref_amps = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) - test_freqs = util.convert_amp_to_freq(ref_amps, coeffs) + test_freqs = coeffs.convert_amp_to_freq(ref_amps) exp.set_experiment_options( xvals=test_freqs, xval_type="frequency", @@ -123,7 +123,7 @@ def test_scanning_frequency_with_coeffs(self): def test_scanning_frequency_around_zero(self): """Test scanning frequency around zero.""" - coeffs = util.StarkCoefficients( + coeffs = StarkCoefficients( pos_coef_o1=5e6, pos_coef_o2=100e6, pos_coef_o3=10e6, @@ -230,7 +230,7 @@ def test_running_analysis_with_service(self, po1, po2, po3, no1, no2, no3, ferr) mock_result_id = "d067ae34-96db-4e8e-adc8-030305d3d404" mock_backend = FakeHanoiV2().name - coeffs = util.StarkCoefficients( + coeffs = StarkCoefficients( pos_coef_o1=po1, pos_coef_o2=po2, pos_coef_o3=po3, @@ -260,7 +260,7 @@ def test_running_analysis_with_service(self, po1, po2, po3, no1, no2, no3, ferr) analysis = StarkP1SpectAnalysisReturnXvals() xvals = np.linspace(-1, 1, 11) - ref_fvals = util.convert_amp_to_freq(xvals, coeffs) + ref_fvals = coeffs.convert_amp_to_freq(xvals) exp_data = ExperimentData( service=service, @@ -280,7 +280,7 @@ def test_running_analysis_with_user_provided_coeffs(self): This is just a difference of API from the test_running_analysis_with_service. Data driven test is omitted here. """ - coeffs = util.StarkCoefficients( + coeffs = StarkCoefficients( pos_coef_o1=5e6, pos_coef_o2=200e6, pos_coef_o3=-50e6, @@ -294,7 +294,7 @@ def test_running_analysis_with_user_provided_coeffs(self): analysis.set_options(stark_coefficients=coeffs) xvals = np.linspace(-1, 1, 11) - ref_fvals = util.convert_amp_to_freq(xvals, coeffs) + ref_fvals = coeffs.convert_amp_to_freq(xvals) exp_data = ExperimentData() for x in xvals: diff --git a/test/library/driven_freq_tuning/test_stark_ramsey_xy.py b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py index 33188dcf92..62bcb736cf 100644 --- a/test/library/driven_freq_tuning/test_stark_ramsey_xy.py +++ b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py @@ -21,8 +21,10 @@ from qiskit.providers.fake_provider import FakeHanoiV2 from qiskit_experiments.library import StarkRamseyXY, StarkRamseyXYAmpScan -from qiskit_experiments.library.driven_freq_tuning.analyses import StarkRamseyXYAmpScanAnalysis -from qiskit_experiments.library.driven_freq_tuning import coefficient_utils as util +from qiskit_experiments.library.driven_freq_tuning.ramsey_amp_scan_analysis import ( + StarkRamseyXYAmpScanAnalysis, +) +from qiskit_experiments.library.driven_freq_tuning.coefficient import StarkCoefficients from qiskit_experiments.framework import ExperimentData @@ -243,7 +245,7 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): exp_data = ExperimentData() exp_data.metadata.update({"stark_length": 50e-9}) - ref_coeffs = util.StarkCoefficients( + ref_coeffs = StarkCoefficients( pos_coef_o1=c1p, pos_coef_o2=c2p, pos_coef_o3=c3p, @@ -252,7 +254,7 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): neg_coef_o3=c3n, offset=ferr, ) - yvals = util.convert_amp_to_freq(xvals, ref_coeffs) + yvals = ref_coeffs.convert_amp_to_freq(xvals) # Generate fake data based on fit model. for x, y in zip(xvals, yvals): @@ -288,7 +290,7 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): # comparing coefficients don't physically sound. # Curves must be agreed within the tolerance of 1.5 * 1 MHz. fit_coeffs = exp_data.analysis_results("stark_coefficients").value - fit_yvals = util.convert_amp_to_freq(xvals, fit_coeffs) + fit_yvals = fit_coeffs.convert_amp_to_freq(xvals) np.testing.assert_array_almost_equal( yvals, From 75211508c282853ddcfc35792fa15fc9c7bb0c62 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 29 Jan 2024 12:07:25 +0900 Subject: [PATCH 10/15] Fix retrieve logic --- .../library/driven_freq_tuning/coefficient.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficient.py b/qiskit_experiments/library/driven_freq_tuning/coefficient.py index 19f7542e38..27dd7af7c7 100644 --- a/qiskit_experiments/library/driven_freq_tuning/coefficient.py +++ b/qiskit_experiments/library/driven_freq_tuning/coefficient.py @@ -20,6 +20,7 @@ from qiskit_ibm_experiment.service import IBMExperimentService from qiskit_ibm_experiment.exceptions import IBMApiError +from qiskit_experiments.framework.json import ExperimentDecoder from qiskit_experiments.framework.backend_data import BackendData from qiskit_experiments.framework.experiment_data import ExperimentData @@ -223,11 +224,14 @@ def retrieve_coefficients_from_service( RuntimeError: When stark_coefficients entry doesn't exist in the service. """ try: + if isinstance(qubit, (list, tuple)) and len(qubit) == 1: + qubit = qubit[0] retrieved = service.analysis_results( device_components=[f"Q{qubit}"], result_type="stark_coefficients", backend_name=backend_name, sort_by=["creation_datetime:desc"], + json_decoder=ExperimentDecoder, ) except (IBMApiError, ValueError) as ex: raise RuntimeError( @@ -238,7 +242,15 @@ def retrieve_coefficients_from_service( "Analysis result of stark_coefficients is not found in the " "experiment service. Run and save the result of StarkRamseyXYAmpScan." ) - return retrieved[0].result_data["value"] + + result_data_dict = retrieved[0].result_data + if "_value" in result_data_dict: + # In IBM Experiment service, the result_data["value"] returns + # a display value for the experiment service webpage. + # Original data is stored in "_value". + # TODO: this must be handled by experiment service. + return result_data_dict["_value"] + return result_data_dict["value"] def retrieve_coefficients_from_backend( From 3e9a576c58c153bedc3980c16fcde96158d9cbbd Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Jan 2024 00:54:41 +0900 Subject: [PATCH 11/15] Update tutorial to show example code --- .../characterization/stark_experiment.rst | 105 ++++++++++++++++++ .../stark_experiment_example.png | Bin 0 -> 1203075 bytes .../driven_freq_tuning/p1_spect_analysis.py | 4 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 docs/manuals/characterization/stark_experiment_example.png diff --git a/docs/manuals/characterization/stark_experiment.rst b/docs/manuals/characterization/stark_experiment.rst index 691171cd67..1c8c723088 100644 --- a/docs/manuals/characterization/stark_experiment.rst +++ b/docs/manuals/characterization/stark_experiment.rst @@ -211,6 +211,111 @@ In Qiskit Experiments, the experiment option ``stark_amp`` usually refers to the height of this GaussianSquare flat-top. +Workflow +-------- + +In this example, you'll learn how to measure a spectrum of qubit relaxation property +with fixed frequency transmons. +As you already know, we give an offset to the qubit frequency with a Stark tone, +and the workflow starts from characterizing the amount of the Stark shift against +the Stark amplitude :math:`\bar{\Omega}` that you can experimentally control. + +.. jupyter-input:: + + from qiskit_experiments.library.driven_freq_tuning import StarkRamseyXYAmpScan + + exp = StarkRamseyXYAmpScan((0,), backend=backend) + exp_data = exp.run().block_for_results() + coefficients = exp_data.analysis_results("stark_coefficients").value + +You first need to run the :class:`.StarkRamseyXYAmpScan` experiment that scans :math:`\bar{\Omega}` +and estimates the amount of the resultant frequency shift. +This experiment fits the frequency shift to a polynomial model which is a function of :math:`\bar{\Omega}`. +You can obtain the :class:`.StarkCoefficients` object that contains +all polynomial coefficients to map and reverse-map the :math:`\bar{\Omega}` to corresponding frequency value. + + +This object may be necessary for the following spectroscopy experiment. +Since Stark coefficients are stable for a relatively long time, +you may want to save the coefficient values and load them later when you run the experiment. +If you have an access to the Experiment service, you can just save the experiment result. + +.. jupyter-input:: + + exp_data.save() + +.. jupyter-output:: + + You can view the experiment online at https://quantum.ibm.com/experiments/23095777-be28-4036-9c98-89d3a915b820 + + +Otherwise, you can dump the coefficient object into a file with JSON format. + +.. jupyter-input:: + + import json + from qiskit_experiments.framework import ExperimentEncoder + + with open("coefficients.json", "w") as fp: + json.dump(ret_coeffs, fp, cls=ExperimentEncoder) + +The saved object can be retrieved either from the service or file, as follows. + +.. jupyter-input:: + + # When you have access to Experiment service + from qiskit_experiments.library.driven_freq_tuning import retrieve_coefficients_from_backend + + coefficients = retrieve_coefficients_from_backend(backend, (0,)) + + # Alternatively you can load from file + from qiskit_experiments.framework import ExperimentDecoder + + with open("coefficients.json", "r") as fp: + coefficients = json.load(fp, cls=ExperimentDecoder) + +Now you can measure the spectrum of qubit relaxation property. +The :class:`.StarkP1Spectroscopy` experiment also scans :math:`\bar{\Omega}`, +but instead of measuring the frequency shift, it measures the excited state population P1 +after certain delay, :code:`t1_delay` in the experiment options, following the state population. +You can scan the :math:`\bar{\Omega}` values either in the "frequency" or "amplitude" domain, +but the :code:`stark_coefficients` options must be set when you prefer the frequency sweep. + +.. jupyter-input:: + + from qiskit_experiments.library.driven_freq_tuning import StarkP1Spectroscopy + + exp = StarkP1Spectroscopy((0,), backend=backend) + + exp.set_experiment_options( + t1_delay=20e-6, + min_xval=-20e6, + max_xval=20e6, + xval_type="frequency", + spacing="linear", + stark_coefficients=coefficients, + ) + + exp_data = exp.run().block_for_results() + +You may find notches in the P1 spectrum, which may indicate the existence of TLS +in the vicinity of your qubit drive frequency. + +.. jupyter-input:: + + exp_data.figure(0) + +.. image:: ./stark_experiment_example.png + +Note that this experiment doesn't yield any analysis result because a landscape of P1 spectrum +is hardly predicted due to random occurrence of the TLS or frequency collision. +If you have own protocol to extract meaningful quantities from the data, +you can write a custom analysis class and give it to the experiment instance before execution. +See :class:`.StarkP1SpectAnalysis` for more details. + +This protocol can be parallelized among many qubits unless crosstalk matters. + + References ---------- diff --git a/docs/manuals/characterization/stark_experiment_example.png b/docs/manuals/characterization/stark_experiment_example.png new file mode 100644 index 0000000000000000000000000000000000000000..5d99b87fe587ac3708bf42d7ff14a7ba125366a0 GIT binary patch literal 1203075 zcmeEP2bf)DwcXQu%Vd&C@4b*-Ae2A?1OX`l;XMUGKv0k(0*WXPQ3#5lyr-xjD!oaE z2%$p?Nl5Q)k}1jb-rHO2zxTg$W-@7$nLD@aJ>Q*k?y3J-=bU@i+5g^aFS_9Tvl3$a z#)?S7g0tscBog|(NXYxqk(e@le(_EWVs1J6@*j&Ne$G$GvYTJH1{3?-KL4`YFTVM@ z+wZ*Uwrl0iJMWx$!;gOW`eihv@Z2q*%IfFjWC2!wWf3$y`>fFhs>C<2OrBA^H;0*Zhe0VSt; zDJTMpfFhs>C<2OrBA^KP3<5_crx(wvy3@u2_)N34A&P(^pa>`eihv@Z2q*%9j=+(L zYM>*a$EOG=0*Zhlpa>`eihv^EGYBX-^_f$l4N(LX0YyL&Py`eKML-egL~?rDr91wm zu>g8WDFTXsBA^H;0*Zhlpa^sh0-Z=yJEuXq6-7W1Py`eKML-cy1QdbbML@}E@Sb}; zaz#K9Py`eKML-cy1bhyG@NQ^PpI2W0#IIuk!otENAt6CRLPEM6zNV%o2@TbE2m)>E zNO%MHE8u;7^D(HakL-7DNrRaKSL*Vngh zKs(>MgQ&LC4Bwwbvbbc)5=l)>?efNc^2sMMbLLEmiHYg*efjzh4jecjIXO8pWy%y^ zZ>m??w|ezz88Bdg^y<~iEBo{1o$yC)<;sZzytax=ZwwjDcmn3vjN!-jcnpT4}K zk3arcX3w52(b3Vq+)S^uZU6rLlAD_=lO|2_%Km(PC#zPil0kz8Nm^Q(uQ#>J+gDjx zDSWP+a>^-PzOSsTEcwPazHwChQF3}zFrlC=Jf;H&4)h8s*s^7d3?4jKVq;lVH@Gb_ zl68bdh71`J+(5XGo%?5}y#uVVp%^IF%V zRs<9QML-cy1QY>9pmPwYT@-nTPqWp|IZWM(BA^H;0*Zhlpa>`eih#c$pybqF&Xl%O z5l{pa0YyL&Py`f#AV9$H$?5wg;s4ZFfFQV1^$-;SML-cy1QY>9Am|YAd!ib2XG{-R z5l{pa0YyL&Py`f#07gK`X#k&GJv2o?5l{pa0YyL&Pz3x9f#4yhKQ0^rsl>suVBXw? z{&tqMy^4S$pa>`eihv@Z2m~Pl!9!Go@Z{hIrX9KoL*`6ahs*5jZpgN=^@rjm}X76ahs*5l{pa0YyL&@G=5QPCE|xkqlb_{eaHig5YSCiF{^u+Wf0E~_&F6oJ}Bk#{IV4bYRThouN80*Zhlpa}R10&6pi z9DAZVhuEkilt_Uasihv^EM+l6EOZ3riO_FAC)BkKbAg`~>kv#VVvWluC9^Lk}4UNVd z+DO70(kP*zq+wwKqAF4gVj3M0Ce^i#^4HII%UR=kYxjPao+;nmxRO&JIX87xgD%oo z03SIS+6+ZN5l{paflfhS&hVZ>B6{E3o6()0Yse`#Tvgj3bxkgBDQ3faD(Kp8lDY=h zZkzF~1Q}I{@Sx_F*2wE?b7WlKL^CrkGE7GFPH-)#{{tQYC8_~`iuC{$0YyL&Py`eK z?;`N}+H6_8aX%8CA|)z3L~809C5#DC=+3XLZ!)B`y%rT!)JbV&y)A^_r2*qqnMzNLmLH!vQU>;jl?cSHss#@M zdba)TTsM`kcuZpf{Ozo1dldmiKoJNU1f~p1k-HX*HlH&__d>!`qv__a13_h?l=%Yz zjSL3`K~j{PqZ)idImv+(C@qL-nC#B4ko(@+B28F7H4c?>)Pe>9C8|Mls`Wq>0YyL& zPy`f#u0fzrQnWDcbnAg~d1+Oq+barBDPv5nhZMtowQ621*f>X)y z;nE9U)ZDUaboKA)nnvqZeHH=j;`iB8qYYC86ahs*5l{sD2Z7BwW%AUQ84`(1{3MXj zLJ0D=wK`<#Q);S#Y5Oz~R`63ZL=1pyh_A&Jh(X07HHcMxWp$S1m)9Z`KtOt-lZrsr zSnPwksn(KXBjt941E>WC0!mH;<2<%=6v?l@vRq>U+5tub!FTc$;VwALd&H5C2)HXfDDtV4}dR-U5Em4tL zsHjD3E5Zi)rNqhX;XR~Zats%jM0W)$^F{ypJpxKp{r(i{0Vo2BfFhs>C<5L?fZg~t z5Z-TV>$6J?fvkj-l#_%SC#h%QW;tlc~b|a%BS1&BsL)m9#^Ktv~>S-!Ehu?E!&kZanWItjL?AsgbBn)g-Hy$`KggQ zC^c68YsNscP5l=j2-Gf$yh9mkfSk;Z9Y*%!JxeqepkvtST8e-opa>`eiy%x&Ho7~% zQvUGKcF8QRmdct22%miP1*i@E`Q65YmNIgIwBN7 zr!`VuTL)H)I;d`#xh}tu#8%f)ud2aF1aKR%k>RKWP$$nW-zS$&8zAB8P7RogQ;BN8 zoJu_qML-cy1QY>9pz9HcMOi1hQty0iy%ZpeDH86`S`fq7s8Fe>X?77)w=0$IQf4x- zgj6YlQd=CXB16++ zB}7Xc2x>9PPvw+U8{yQhKYHDRHxN*A>J5~2ABun?pa>`eia-D%@c1XYOnQB9Xh9WM zA`=SakzURLDN(Wyo>Ze99KoL*`6aht`6#}d?nhw&(PfrlZlIl8n zZ$qxUyFS-wL(y}|?s<|`8oS{8rNl@y;!0`Mhq~{Z<0LL3T>7JKDLt;%m?ag3)O`*= z*mA&3o`n>D?kBsn1|bAhnC2q0B&e)|YU5VzHnKpBn@~ITnv;hZ53ZfAL;puW$*KRJ zLp=gTKoL*`6aht`(-ELI^2WKt<-*DRWOYWdtlU$K1SQ6QHX1@&1wAMdMv_Mw2lRy2 zAYPQ#7XqcZ=(eXqKZ$NqEIMx8&eJH=F{MR-+!h(JRFQ<}aHAQO$Ta=PP{g%5D2mf% zYSu)GKgjP*bBD=U$zbPy;AIL>^a9FiVKcOp=7M#R+`3(%csK=-W(C<2N=*C5b{ ze)V6xu?gMnWs(vX>GphvLi;HKp44WL%+{r&3h>?PO;y&^%gd{>q!Q$|1j$T{RgI5| zFma+RPG@RCV*b5i+iClF8OT+)+`-YUSP{qXcl@+nW!!sLt~< z1T+@a&rX;&R}oMI6ahs*5$H+;sJ3rCqb%XHA*;l6+cP1GWU|>^-ie;kx`qZc#SvHP z@OGXsAX)zR@+orbOKT1BQjH8bs39r`!|N=|+J1ZndW0YyL& zPy`eKZzAx$f31R46iKH@5eTWNR|=L$!s?0vZ8jPF%^-X&N~H(4aP{6|`N=Em<+k}F z<+hjBNiMqb+1=0WaQlo;J!yEVTrjDxiDP}?+F72B{C>%Hd28K%Nri`Xdv3W&jN(kL zLq-2ax1#@^!b(Yjf`14I^8patQf#*#Uf5m;PdICAZ&U(EG2sPhs+u1mpybq#PF6rQ z^`GneBx)=`K%G)O7)3x42s{LSe$H5k=QpADXoEcV@h;hqcv6zfwp=fS6WvD1C;B@b zlcWjLR%8^)`fD%>zIak_JML-cy1QY>9pvw^$-!BoK(^~n_i|b_T{xah}rI*rGwY2@+b8Zx} z^_fUTi-G>>5M=IeJy0%xU%CexsV?G~h6+>lG2upF|I3{P!Vm4#k!kXivqqV5{_6p< zY2#Q8@kA^u^-v?hSTo(kl*^GG=U3E7Zh1Angh)g9KoL*`x(b0;R%f|lMBO`BfsFmk;!4Deh8t4Y*zA&#YH$Da zRE8rf-%3q!8Ole|dO^!{AcXjz_|8=G{ZRiXHRZIEN2JMzTl35|Sqhb(2z+u;29;5% zNdEVbS8z~xY+dV<(6T-8QQ^`LDgsrAas6m(F4PEyNl&Az>fDQt`ziuHO-`HO`d_tb zmF(ZYU;6dyCsU_R#YNZp0zMS5T(`8e^iZ?S^vK9aiHnOn>@qsDyAim(AwET80lNDv z=>8P}MId+(U>7(wPic%j-B(nBGNVndOnM0NJB{C*Ph~!%`Iv47Nu-yv0YjEJrBTCl zSX!LKLrUscZ2p*D^4`Y%vTRqO*(N=t3=OD3oGHDjiRkucH$IITtk&GJtOyBckp3ws z!3uI)fOy)yg;h#aElB<_tX&j&hwEL(2V1*#?OGW-cC5^tIa4mU-~yRGeY%VvKVCL& zY)SPyT(fg>a->I(9*6z>=tn;~+$MCMBA^H;0*Zhl5EKY77?k3t)OY%DODCl7fp~$rw_nZLrhR?J=~&9 z$xly=l0?J`STk%H|IE-mk*#N@7qh$26pw4yuA@WWMxf>F`cOLy3kzkzf(24pStE2ccDMFU)!t*C<2N=_ad+#wMExKA>SGaB0Y~(;-~i$(x-Nod)RDJ z>l@%zO^S^`W>ll`ma@;CPSiMLLNVo@!Ki7Es8Q6H70nif&t_PE;aNxyeD0zOGp zUwP#f2}gwXx4!kQR`~GyEw|hvoPQ*u+G<6|7p_}UQX;qAcAG>+MLFi^s3M>UC<2Or zA`mFVtLr7N9090|e1%aY)rKJ$ zC4V;5@=q*N0>Zovk^=W2oqsYa2z>Fy7wv6Yr}+>9K1WU~Dk^0A_U$rx@?=R#L7I%i z($dmQOe(wQYinyeL@xjS_rIGhTJNl5^E#>sC<2OrBA^Ha4Fa>_q2z~N$Y)pVgC-M8 zF*Si$^3QrD$pPV+G-R(?dMC!n8Dr9f;Q*`mqQq2msGL4JO~&_0LaMzhrRB`A>84xV z2_!S8j>phM5YieM2%*$_&mSj$hBzw)RymzbB~J}Oem{rQm~sUnkfSyumbTHf_a`r} zmA_sw{dk(8>v{Hp<5fsd8yg!XJ3Cv34jp<-Fzwm1M?U@ZQ<*hu zmQ0*D@sR5_pl;i~eftiY&kzH~#CEkY6_NXLrX+UtK7Dojd>(RB^dH0PkAL>!H&HkoR;w9LPzo~r2 z9qL~B!^gYj_4Rq^)(5>K@xwpEO9>o^A30_N6Q?j<0J8WpLIG&WNLCtzuK1$zTKTLz z&rIXLlZPZqT$n4B-%dmZzSP&+Z^u3?N4co0pIO&x#l+}HDT7cd+e*!;5S(k~bdjX8 z9;?y7!gqRB<02!Zq^d!l{A{P3nX12|7D)b%@*X+VwPBa*j3GJB(VEW@RonQs9#nfk z%*>~C1jr|zc)~o)uD<$eySa9Ud3kvw#Qi4{il{)x^KO!qutT1n` z9o$5~tj3#$AHHCy@d42wWo2cuWy_YJhlAvUc<8m^x)k^uMdQo98e%7b$MWUN^>-8( zgY$Q^5hU*AT^{Nw6{yg(v8Wm8O)$o1y4b0?gt!#HM&n-t*uvOUk*PghYW!RvA+kY+ zB_7N;H?3Y~*6+-J^p-B{=h`B2vZS1qrO=*z<;}9b5Z66?NNBxja6A&LLg891cHI)v zh~J5b@CN)|gvh6xvSsE84JP3_l&Y#hZr=PI&CSg<*W%GY?sMccBEnURjNYO4%#v4% znI4Prhl<(zF)=Z6$t9N@cGbRp`$|z!QLAORzj^cKNv~eLT21eYFYmtluAFw-X%ZXT ze*8^W?9*4bNpCql+OucR_SGhOwOwC+`K9SS=+mc$A2; ze2$!2k11uQ+A~WUtp~NeW!jszc=2N44a&GvZ+Ji3YlBYnBLqq-KNbm)SO7nQU7M;1 zC<5L?fL-6zJZ%7Bee}Z}vgJU9i8`gyeigdQtwrFar?IKg@V!AuT4EOdUq5#d+#60E z=E;b+%X@Qp66CAK_e(lxmz21=<{vA~^jOGKWfW8)k*m>E52yx_yYo;XV%Kfs-2#uuFNQwRT)Ki&bqqsJu}=RvFs)f2}f3cgGqWi068jOu^43X z^V8PS;Z3Ei)r+gLOs!LwC)Kr7c5yYj`B7R5r1g!RwbG+#Ke$GP%pH|xiW9WmmVWPE z1bmL1GFy;c>94)^ntb)uS0<^+Ld5vbf`S6M?6S-3q@x;g9d`GOfJ^`6lTSXXb#x&` zKoL*`6ahsbP!K4qZITi)`R+oggZDH7UEZ!#DAOnIIb3>JqaYWR5*sa#Tslem z9;J4vX9L|da^va4;SDZBhJTS!C$P<6@c{`5L!HzHsfBK8BLZ(Ln`%s@E4z#ieFuS~ z3k2|Tdnv|G``OQaX0nty&bZUN@4lO(a^sCRnxXyo+u#1yBs0DL{`+=PtKmQY`A;KB zMXxEzubLvzX$TCR-)Drz0(2Ujx*x_Ty|HoG$2 zo$f|FE}0<3?s7gmZ#{FA-;mVzH|NUdI|}5Isr}{KGX_cTq-e+UAF@;EiCwmH4 ze{fPigU9s2uH@^kKLfglV?n^@yr^va+_`h*CqMa#kbu(jNolCHYuB1UZ<5uMPCBX8 zG49>FS5~iHEr*IG3^P6T)Kld< z-}#O#Sg_!rLt!d!Qc{wP8#nHtdCab2ZPV+ozh0Q4ucinn0*Zhlpa=vb0#AIo2ST9r z(kn3ZLhZk-vO?D8RLW;?wK51*&G!)SS#nCK&O7fs^Enc1B%l1W zXH5Bj@WBV$o2Anf0q-CXmHu*$i0T4(2W{PpBA^Hy0ReK-uXYz9TPWPrEe(VAj}_51 ztD;YzdZ;Nekw#bQHy4hVQE;PLOOA2rLE7)WK3t6wS?~%=Y(%&u#zsg{1!|;%sWKRs zx>M9IV78bNRo^_5K22186Eba;BA^H;0*Zhl;9m&TgPqnRV~AN$H0nxOvwh*56otA` zcP$ttr;hY*?_v!|s6E0Yl$>&rQ;`aIkK?EU05V=l*?%>kM8GGrrF;@8ZImLQ2q*%I zfFj_h2t-3;DLp9$p2H?7tU}#W3QWOR+ceBgSfiocblLO)NOy0NS5{}q>b=E&dbA<~ zQdsS#*#w@(Kr_o)6-Xk%sg-bR3F{3NKw<#nd}+w31M)JPE32z2@PPKcDXnI*D#;{o&8kX`ESCU)BXP9D({ z#}g$5CKGjg{FNOCsJ~CY9kL}%Q`_m0|E&|%c-?@0|))WCnKoL*`6ak+^ z;G)U>jKwZ9N%8@Op1Uapa>`eihu)w?D9sTC-nRi`bvL?@_t%E zv|K)OfLuLiuhgDK#D2MP?r=GKe7aps zhdzRU5>+2L5!wtzKoL*`6aht`s}Q(yVw_BwG7Tl7>SfMDADP`S{_>%P6I<;vBmnwW z$M;K;7p|RUj_b8G+4AAmJn0#aSX6jSt5Ne5A*hHy4U>epD0yjBrb!Cp!oRy@q9n&g zw!%ZdbPobbPP+$p-K!#?2q*%IfFjVP2yD+Sm-ShtQdCqd#l^Lf7j-~5jk=(;@UReB zyy<`$KYesB=@Azg#Ekl;@zE}NGjTwY#DIv>N~-Eiw?5^zwii^(wD=fF18FtcQ<(oA zlESP6mstV-#`Z~+v3(QmR~>o@0VSti0$O*Y2y_Pm9TpRH!1RG<)5=UF(Rf3onie0QN7D68?zs;Fs@rMn6w zH8x5{_Kuewd6n|v<~$kED_-Kcn^WI z#-$5CyE694-%z@AS#Gtsv_M^IpqI}WC<~|d_g;g#X;1r#s!Tj;L!(RomCcAiL4tuP zagqxOs!jXLQU0}FI6tejS}q#aU#>rOsKh`UR;_yw2!KeF?g38sst70oihv@Z2>3bz ztPJ(YXW6oNSG6Huw!LBS2>$LrTjjYG`|PBE9a2(@akuG-F%rh|rm#kI^RwJ(6|_(b zD{CbT{rslvD*$_L#a<&#e0+m>F4ffB1mZ4;fpB3{jCmxdj0Y%0Jy^BwK|s6s zy9amOt0Lfk2>ko)pWP=iSQgBiyU_nyuPs&tf(`+uo)1SlI*Hc(?`)B*qH1Y^PmsnS zOd7q4NzoBPGC%^G4ssjFcF~l6k`xn$+NU+9LV-QjW*mDXKCD_`trFg4!o}$Wdc@kv zI`lFEEoa2bJJ#JP0*Zhlpa>`eia_Thz!=nf-`pgbMU^0)QScqQ`?uY@A}OnBXoPpQ zUV0&eX!fww&ToVdZE+w-=C3cDD5(&9EkV{4y{0r$^)P&4x4t!!(tX9%Qim>o#;U%u zI?MAZa(iFxvO4TK1hk94>#)@=D*}puBA^H;0$xX89g5L!gDQUyDA7m2OUiU0R8MMU zIfzs34dQy&f>DS=4O}X)E~`X#e7J()D0{2!Y>!uwAT|Z+Cu$ z(O9afXYx;zFk{`C9ZLog``yJS$f)!Lp>jP-FIhb%yKEo~w;d>#_c!I5L)leOA$7nc zJ|aZQ>Rd?J7}U1M%H*kXboXC-@=%#KrdQk9`n`)0P@>w!0PE%z0YyL&Py`f#u0|j& zAxa{lKu=?`Pd&Tw(VgAwAf_gp9rK%!Aw<>wU%s`;EYE+BAi0WVlmg8zoi;%DaoS_A zek{LVRppV}vh&#WZ{cDWKc%U*9w?Lh&L1b=oYc=Qrb90vpybpGAnPs^0YyL&Py`f# zu0-HFvj&;Xf3~AQ-r2BUnve+91g2#pPdV(L6eFR~K`lkr{(sLL2#;WIPm}r~t3Mcw zp48{J)=D^-XuV~k=D-;W(JV-fG8T&Y15@K5N7W#YeY8{N4DTta2$@jx4gyL}y@Rvv zMG;U04o2V~PYlPSKw|+8Mn_EKs=dYX^Vc>Y$)^#rN?}sr&PeBG*`dt@s`{En z6qrwv8%`T0ago7C2mbHlol;rXgrunmIpC0!3JqzNW{^{a?iytShNrUTDMKIf%WGxD zo+3GCLg4Df9_OXk>8ooNf2TvL8&m`o0YyL&Pz1b>z@I?8YS4AxGa*`{Sxnv)V(Ag7 zrv~WEA*u54B_{+WF=Yn-8YGd$L9Cxuw46q-DQliuuQNjC<5JrKmp2iZb!B{J*Ii^^ihD{8jH<0fn|lEdw*zJypc@e zyy=5e3|Y0y1^TcIS^XrYw$P~=bpufjFLPW}Btj)b(zJWutCxq8 zQ?J6UJ5vM{fsP<>?-{S%tFZtbK~C2RMg*$r8cpnJSV$PU`0G6tl9(J72ajO`y8Y3M zEjgvt675c?s;q4*LKWD^k!;2wtf9iKX_^!pVF-N!6b&kCL1KXqwNb76xxA)cqTq4u z5g#eD0$eZF=gxfXqR2axt@_+)(FQ33ihv@Z2q*$aMqtbSGTB>LWfnAgNnltd5c3Ri z_yeP%I3FJwDs>@E(myRh9=dpf+jV&4?FZTt3@AN&TrYWM>2AYDDSZ$T77BN17|Lr= z0-+uZmhM$#n&HEA{st)d|KQYNCcBDyTr|SVda-Ijrfvr5p7eE*bT^2MgTl2rj0 zGsrwiC`l*Xq$x2GQjH*9PP0}6xAY%B+97}ae7Bv1nyC@;>@_nz-*u$(5m0j4`Izb! z6#+${8xVMTUG&Ww3(yU~>n^(u0jB5g%yX4Vn%F-{_#vDx;uWDV3fD+ZN#aFkk2q*%IfFhs>bQ%IXKuqs> zeWPc4P)Cy8`Bf%`pIJaQfYOfj0hzjRT#^imtdar*U$O>junK$L_ToCpE~%D^iYkeU zYBZ&@=n-Ys6iK}e%1w=pk{Sf)va6q(sm!oqrq$-`GP!7Se=Ki+_M8J9>3jr~oOV8@ zxE@g=!%3UyDw)3rhK_oL~8{2`gzj!9T5R~ z@ZzJxW%=$xx%;(^l8w}SQyq!Kl)3uuF8)M#2UBAsr3_uv|24I@BsP}G#EBCn%ALl4 z#2p3T0%Ot>%u!YB4U^&Nagv^%e9%!ohPYFP0rW)Lza^?nn2Lo2Ks|z2pZ{u~Ts~uf zr@rh#8_)@zf`A55bP9~R5k)`|Py`eKMZh}8r-6h&2N*+SelC#gKYm&s3hzytn|a9 z^RFhW7uCPcm$p_BPy`eKMIgu#_E&aFzF}7Ou*kMxweSeC#%Hg@Xc^rr zMvC)G?GidX1Om*?AJ->IR%R3#eN>j7s&+5C2XViRps5jHve!Sm8gx~JVH8gL**Rn6 zL{;^7Lek-JC8`~UPFGX}6ahs*5l{rYk3c+1AE8Iz7>meTW7#OiqB66ebPod42BjF8BgUWBp)22-yLZ|+Qv3Ia zkq0iEAoIuewzGA36bKO8yU!VGlA#!nYON8hKQ1al*6b@r_kNv}Lm&0&Wqak}CEJW} z|D*xQ$n=l2i|g<>5m3+RaRR8TD*}o@&?9ilPv07@u>e7T8FUW-qmYe$;pD#Z^zwbs z?{t^oL{lQe!=(}AH4a|ZsRL8wKbsDiPenzATri}2+Uiex{c2B<@t(5#{<@Qg$lL1= z$mcurTzaU8Ma_d?Ki>ixo4G`@l%o>W>b=Es?qgrbILLrGB~qWhdWH)6AIHg8qIw+Q z>8gr=BA^H;0*XL)AaLuvk*2ii3twfrcaLm+Jkh83?;*2?^)PXgwq2!Ci+EL+uBV-* z!-Em{&!$}2w7(@zmldTLlS*$Y%b-R9U?b-RV#<6d#{O2V1PJsWj~ zUU(+z$k8KEyD0JwWvEAwmoBLYC<2N=Kp?>W;rBKiII3fztBwlsYQBNMo`Onwaixou zMu8?ZyC@UbX^5OVp%2pbBc!&j37oV^7EDN&6Z$7hW@e`38>8Vnu>dt7GK)91CaSyg%B2d^DVoX55Njbw3sItK(eSNd zab=5pl>Pc7scM~zz;L)oNmR*HYvEcguBh|uz`P`&%b&getd#K7ml?=R3)iw!9)vnR zqC~ah*y);zfFhs>_$30V@saYxmD5byx9672lb`RAMjFY<-j5aNMcEw(KV_mixzxL#za>kfmKGo@|M%HMJ9bWKG-5l{sD8Ubc^-03!xuwu@22U?*Y0r z2g`zaa~F1K107h2$NuYgwm?4zitY(fMnJQvsS&z45yl(zCcJYe4eu$Zj!f&o)^!zc zAYh4VbVQhmgY?EegJB;mBlY?kmv@@?3GaKpwAtFH#DKA>=J$YJRLpE_Y=#b)YdL0F zsU05zN>q;zKV4f9Py_-5fxmpZOWxm{>p2=epsLWF#~OyQj(AKi@bdMu5ogvCqv=`6 zk4Dz)E0OGyYDobZrPU%;nGe+l)ODorBQ<9)Za{?oE1 z-1{~@n`!(kA618Y1Cu>QO{oz4YXT{ymEa{W63emf>9DjG&#Pwz9XTchl$;(DT)MC# zpa=vi0vAl~C#Sf*goTL3d~02fe6lUi)QP+dC1b|*P4ak7qr&}@n0}2e{VI@9+R|Nx z=7C#-hi@&2XpMV%K9WB|LPCwc&c33`Kt0`lcU;#$vl^M>E$oP~kO#^r%89OLT(8&s zbCnsNG%#6ioIBi%>%Wd65D6EkjnAeBb@}eXRvTg{Oenhbkq>2h88|b)yvD?wUOc70 z^iGUwwY+{gb_A519y?~bh9aN{1Rw$fd&CMqAHZAq;Nq=Pj0b)>Jg2oqRr0?d?|=uf zm)v{)IN$M@UiyttCy%VDC zWF2-h0%wj*H}S%3246OwUY5a8V<+@aLHQ_Gl2qShJ!ck|;~nnGuaLhXi7*Vpr_xfr zOCmsI`C&2-dmCsm_&#S?s@yhzr1`G@jt>DPs>g?)uB`|t0s({o6CCb;XS39!FE2SZ z3X)5WhL|Qmy62;e8NvM-v_&ot%Lud@v?-CO8-$>7A@1?4g-eV9stb>hil;^1%I-75L; zR5J{w8oveBl4GDZ3o+CIsd2I~%cbr=6jD?_I%A|9Fy*EM-Td*MUKhKb4v!uI&Eh|L ztaM35KoJOV1YTN|1wz^&Jy2Mi2@iZg@xv~CO2)jiAxHAcYyBDpK4foyWt}|s(N4tA z@e$BMOnG4Sh(BeQ@oad3&K{@5+ws16U;4yX*=^O<;O3tlUrU~Yd(zQR_izdEr@q~ z8@i?`e0fE4VtP+W4u}D~Hb)Bhi*np&n=46pLbaUr|m{Kp4neP5$>z-TjMEkJ- z|5&ycv312JTc7cC)gb0h122~az$Ouqyn_g2IrDbT$+~u@T^RNl=>uhNf zoHy5IBX%xC3P5HmYFoN3SuM(z&fHU2VdSTH{46ql7Rahv_aZ#RGFYL z!<-rXl$lz-yU0w_f5(qN61wr($3UFJk*yyM63Qx4DZoF1Wo8kO+B+#qQW5q;a>|Nc z3Fv8|6rI}fBcLcAKZd#mML-b<5Cm8z^7SHD&2mfBcxa7yKvmV(!;=~+qk6YgmGOnc z&Mm7&f=0b$mezwnH%hoeyuEGv_~0zVvLrI~4WZC%Y$=h+hjB@jt9)Y(s!6588%Ilk z0J&QCBfxV)K1#yGmki1v(U_z|3k89qOc>v-j4r2{s{cBQ098BsCdbHr#FO?!H-9Wi zC&FWR@33cuq;=kybSbKA3Hf2-;g~*&9o?m_=R}}(QRE%^)8j-$#}xrZAgB?ze8xao zf`Z#jU&lkpV^v@Wb$I%Jd-fnnjrXlS{->AkHF{Xp)zx4oaPKZFF)79Db8O2lpZVTQ z2?LX9#?v(|A<|@j@F1fimS6okJgF{SfKc;fE(Kd92kMFARox=brQj1I15;zcltQH% z*Cbtf)~qmZ4Vctu=uIh8wa6UbFaqiOw<0b7?$T`#C&78sV?O6k5`}XZ{r#8qJcvgO~vp~{gqUFD54U&_Gr+R`(=`KB?f3hrx7^f2k}wk)}_75YqXrSq(S)U56b zjFzV}tK{WPrSPhS81GIN`Wzo!vR%H|(L)wpJi$m2aDz9TI!rd_lo@Z3-9Z@KZ47W^ zpdGcfxPKdbXP42T_Yqi=St7Y^-6HONT;D`lxu;0hXP219;C;gMGn1fx_oN}{cUI{I z1I%&$V;sf9?{D{PWzv8o;9Q4Zhgz9}SRi_qmQ|5}Ev z^Dy^$qFPs1Cq+d?(xXQYiHvlmFn09ttE#G`tgKAZ($Y{GLT}!V;;8E>0)dFYkIonY zwe1n+&~o8XocWs%J%@QcJdfW-ce%Ih{1f^LKeT1&j(=)thEWA)eCc>(as2npfx>VE zYyXF`SL}XFiVBr_5El{|5^ZYaPz&dSExGc290Rvi>?Ws-McrHI>dH@wi4<1L*$e?l zYY#%He6V95oX-8WCCYYr9d#euSE3dpFwY9Yp9UU!~UV5oK^2j5Gs2)q}o`3#%S+r=8 ztXsFvtQHQz!&$Rt$%`+(D1H079stL(JzZ82Py_-SffCgJbQ)`dc85K^Y_I(3)144w zWZYW_Qn;H<{NbnD^AHy|QKk$^G0obNQw|TStA!+=^tzTo(6u=wT4Ez3(0yMmyYnlJ z`nWCPZo4Ddy&sD%%X|=@T1Q;t7rP2%5XdRJk=42n0TR=DQSyt|Ac{tKj7d;^lcVJJ zGe<$|sKs$ZPo7%WBXIKzYoXKT`dzRUJgD9Fd-PaP_lrh-x@O$Fy%M8jc(2wHAg7E> zQ*wHk)8v!nlw|bmv(J__Yu3mQe((dCF=K`-TeeL8@|VBJ)~#FRi!Z*Ah=`-R0e|s} zU&yb1^(z@Vbg2CFr$3dHloZ*wZ=Wn)yx4eTm7E^t>d;w=K#(F(j94?Lu{+h<#u(AZ zKig%Jm=dGIqymJQ9-ZFk!p*O!kzc*DMV`BMriq1%0=cw(z0|;=*)WALLu&ucwL*(3v$NmyO;qkNJLUQQ>m<{T` z<3`|3gkQYBX}_tr%MAYVYS(qiI=m!C@1oS%!54?QGLJ@u4acinYIJY`FkEHUe!amE?)_SgrTj7@fz#*-PK0nK)~{@+mt=U^MnMwC8e`>h-KB_z!c)ps?#Bc6-3(tk_d*t_6yBs(B9q z-Zu=RS@hmkS+lp;v*R^c#i)78M79u#F^8`CbQXoXX!157LY{I+iPunza)7OUM96eBS73%AUnT3+lC}KzLu~%*;D!p z=bkZ*j5{sIInP0+zmn7T(CgZ1|5}mgAr)?wK{lSw@hL8KE0-+M~{|)0|&Nh1H0*m4<9ZoSFS_} zl*Yr7%aW23`SjCIW%~5#GHB2s$<57`ZQHh)F8;A&#~yawRy)-%ihv>zbO;>eIqiln ze%4bo9!#L$Xh%5iW|izhv_kwW3V!qKp7OU<#Zpx5BCI@VT=oa243W!b4B%Ifk(^o^ zmKF!S)I#&{q$iFERaNep^$4D$5&#RnlRRBGxu0E4emHlSiHGHcVtAULU9nfTL#Aui zkW@K;QXh~fR~Q!8Kdk+@s`(*}1@J(`(257)jdL2W$2!Io<7IExUtrzns*VX!MS*0Wv0X+5iuq*TC$<8;4&1&kR}P?&C2&R40*m-rj6$+3%ws} z_7w{g+SCq>K+CJ{p_lRM9L9+@G&D$`K7HEVFJnh*YHB1qJG{M6Ab}=_0?DHqB>Lr6aht`6#@r)QCoNUw_3Q1zMMF$2Rx2EwFQ~QZT31{vD^*bo(rGh6NaqaiX&%ate6Y=^43r^W_KO__@`oimjklNc zC%}uUc6bEXUH<|+YyABE%4u@x)B(mzUIj1Q;Iste0q^NZhKh7&OwqgEKPC2{B%H(V zP3Lw70vV_n#ru#i1}RX%0Pi$rUMlg0cN!BLS$T>*AM9C(K~F{q+^Yv1`nK-XbkGKY z5CG@=Eo(E|`(K$HHD$^ax#W^dUL zyzoMakB>Lq{7jbOcA3e4&pr1@X=$l2K6UQgxpo!L@W6osa?(jBdA@TbBO}9P{U;|U zx0>D+UnqqY6BA>?9J*rr{En>iC|ak`K3OK~1xy z14@Pw9H??F9~TiKsWBl^TH7p_jSQ3Ny&Y-fSoiC^CV75aJ+8-4#K($MLIof(G6a63 zW(fzOy>@)K3{7ewyI93*=8wOlCBXUr*40Z6oZR(zBjL*g^=}(^rZ8@w)>LdDsapQ6rpMti=5Hu_9;2dst5mPoE`~Nc;_t@s{*LBUHp?Sl*CA06UTmD6%!-!t!@Gy~ zmeZ1=LU3=u1CDe2-LVle0r#CR7=Jg|dri*K)yCfJ8*aG4NPQk@Uq06tz&CW-p%Rzu zbjFn)ja5`s$Qy6GA?KfezFB^<0APeTrU3hGpqVfxWLwt>#eYCw$mTW8Z91KYf<9OL6yd|$n z3M%VymR!$7zL3NM>oUAV@Xwf;6)H)w;d0}tgXH`P?F-0zc7PEQ1WGuLA3xqXIcUfE z8!}+P07*^lcsD!aho9ZOUP_Tf7-|$X*v0QUN3OHi8s2JDdW#8(kY35*a`9>7CDBK8 zQ2Com2VPhFrnW{vjGEHX@;Iqo6nRI>Pfy!Tb<_8xBv+ph75dvVJDq9$hcmk!E@zpl zUH;57&or!a>C&af2X7g&ozJQOQ>RX~U(JxqaGTz6rE*_vcUQQGS{;-he5tIgG_Sng zz}*kvK;}!h#KpyVe4V*R=!E2RGVe;Hs*Zgly8vr&LWtdoPdFyzD1E|qBYg9iuaWrb0LX+r$_qNEF z=!d7sCan&>*RY6ax#Jrnq(@SM`~m&-c@+&FmmQZYtZXo%s&~D;)2zdP6j1%;dE=x2 z{me<|f2J$76yx<^T9iHLfh?rI}agNyEQE#}2 zj-{zX5yCd3p~WT5@?HO4lOwV4$1y7}5q-w&6K2UT_79gM`-ZM*i(okBjqIH$XN~JE zWl)%SW=(;a_rtlv57~_JatP+43&xw*@F5%H12a17cl3KF50i&K*nw^dIJ-^MDY`1G zzq`$G@?18ekN?scgQPArS_<4@Gt7KsKzVl=yXCs-ceL#>F;VDf&F9D|UupQd%V5;@ zjMMpWK9Z0r-KDfcxtv%|im#Kl5jvgWoI*CLy#!ld@wMc5br$B$5k3J=ulAbiRIy4R4JybIY> zHbV;@JgALF3rR2^u*~LRoGJrZ>p|M#tpfSC8orP z$VGwd=RpI^8r`gF-bY~J)cz(o`|;0qo7V@%Wl*XAPz1FKB=-3g`;0iMZEgRQ7F0L}jW`%tA8Ym1S6^*f^t<2vu2st( ze)wVY{i>_3YBim;OBsQ?YSpS%(9z(v3u zWX?k3ZBMG*FNe-rq_%97bG{ufZ{tPf^epJ#e7UO-o;Y;pgWZsLT8oSbHC@W=4o``V zFy1|;_LGlCID-DU9Bs?~GP&}^LB{L(n~TQF`2LU%z*E@iHNrDldd5eYZtCk#9V(gF zKTBXO+g&6j2m|;!vsiwEPy-e(r#Ep-pTmc1nH>k%zs2yJuFWbnwMt_m;THvH;lP{S z_xw`MmxOkV-sdE|T+k!E6z5c}`w+PQoz3#|*EgEa2U~K@xnrO(t7Fy|&qTrEXm)`<=rqcr@9a9OfpQ ze0W;3%KCB0!kIm+r<@1x>^*O6Lgk_m=>relKCDY4Wnzq60lMvx#iUmeTPcFmv}E=3G* zGrVpjqpVxXu!wMc?}@nGd|=%2`T$7qq5z1EF|OAbrm=RWTK6IFB*Idh*F9eXfBwcs z*;`m?UcyLFWAWlq4T8!_8MbaR1%eatJMtc0Q21i@t?2{0?;w15&#u>DA8Q~3b*U3Y zJ*e&5x67hMi-c*a_uhN2yz94-!sg_x%SSd?0t-b(aYj`0a0h zYf^vdW&P@_uOvM^-MJYZR|FIRMZhS}TNoHiW~~CGwY!Xj&KlE(4`pTTc}~&AKOE(v zYz!;)i;UsWjLJKdG>iVP2*lxBF~*zKE*O%L z3~7spE}0-VoHpFNfKls>FMuhS&MF+%cwaNokoU#&Q2V#Hrk$@t7Xm)%MP<`SGVii!Vels}uWQvl3ZzEkJW_{^No*C6w6$h?MB?CKWw|!~z69I#Z?QPs&}Yf%;To;; z6ahs*5$GxeSijR6J&Tr{vWvgF%+|N${z()MWo7O}DSB$yy~s>^N*GNZm@MB%GR9v% z+XYhS3KyXNjT!!Zag4t>7j>YGCoe<>rN+rN#F^4F#2C~Xb~uAz(hEysL{BTX9SgM; zVpCu3YELruX%o!o=w%lj!wTp{C9$$Gs%HP#9w{veFXsoFojpEX-dLN1?)`dGZpa=# zzw;%Caf~$cG62u23G2WlX7Tu5_=0OOMK5&n9pihv^EV+dG+#bym~=n{5o zaui6RwZnN%4?w%rWiCR<@yw!XdFsmyLk>->r<)7%p?US3q4M`DrpXX^@~m9~*_>vG zk}_^C%%NM#L=V>8?7=QdkWI#}dc1!4?(B|cS9NcAL(zL_%o0<(kLvmcsm8-Q6Y zX`*jV>g!rS|93kAOmt&7P2108Bms_2Pc$zgyl*J8M&r4qibEyh#s{UwYNFI3&Y*fx z4*`r$Rs>j}E0`s!)Km>;zos=LMsfRb zQ4vT2Iyj-H5T0s2u)jTXP|!;3Ty^4LLqxy(kFP9h1`FzI{^nhlH4~EsKDKY7?0|+X zUmB=M`!=xWhjt~lF#^O$%{LLC4A@UzS!Y6F!gznUZ-s)2I_Lq1nw|~cYybg~U|581756Z9uRv$PU%q8aE`q>%{DAJbb# z^-hp`-`ZrRorG#s7lQ~njmaMDR{rt)Q8F9z9|oyqMJw9LBU0s&Z=GP`SwGuRh-yYTRlf1&2?vydLL z#)L8oxnFVOAUmnmkmps+cM&*oSgPE$V2u0$1sR;hKlmDV)$GBh*6G^J!&g>f9QZ^? zxdodkISsZmuE(ngC<2E?;832^?u|cf@s=J+|CF((wmX;)JZH>m4Z4m$+?pp8?YB04 zNDpBgq|<(h?EKYkGRo;rLcVlopRRk+I#p zZ_1d2NPS`?!dYVo!OP#DZNCQ1P>E{LoN7H#ML-ca8U$>j6upW4Q(9_dJ4xHoY^O7q zah5baR4l5qnp7oXLD?P5icd7go$?{MF0<6rERxE$cImYKGT)(1O5*#yf37s*{{4Ey z$kiteG27(!vJm6?_@@ZdgfX7<$50A*bxoEbs&owQgC-=&D-AvVrh1O~f`24s*Zk8} zSS8rXol^hO7AJUi#a;`EXSlb}r9aCFWBUf5_sj!?pN~BC#U9!1?#1AtP_m5`qxb^1 zKD$I%5Q9=veUoG41|%{5W9c6G9OSh>zF&{;vxfFC#Ff88L20#%BJWU!8kDDA4_XmW z1P+A&wUcPn@^Py3cb6-bfu7EE_e1sbm({emTbVi>1>sR^WVI3W%Q>EQ&g>?;+_-ia z5&M~U=U19cDQZ@+fIN-*P)>XD%ROMHZdWZRYBbzF%&?+`VaM^nlde>z@+Tl2-KIaA zb~!f`MB`-nZOxu*$}u>vzqZTici%?fkC#tAsDa;k@++{=eADIe)=hIvT+Bb0?vz$Yj<4e%4Jho=ZA0>_4c4Hip=8ahkB zFbIs=N-Qze9cGpC?Y?bRlKpOnI2BO+u^bJ_t&n>72EI(^iR`j>YHTI;;qKh(F8AKE`le! z!<Om!?z)G3DO|3OCKh$Mdbp=K zJ*)3*$T7?DUo0NDB&eD08mjaZew9%qbKC)zKYn>V$aD+QCP}LW{-#uqBO_=wvQO?l zXN+gN{ywrjuiP90MKKvW;WT3Kk7XHVQW^qwm7F@!@y~GzEL;0@d%n3RYH{831*{CQ zIgR+8ap{glAQ&}F{=GU2<=F2gt210F@<&?ylIi_gEn+>QttR428AKJWF|D5QvsBA(KxrtF!A1yf zQhbp{PfapP6djFOa>`}1N~#PwrDxX?O^QESn>HjF!E~-~6jZf#=b57n(OkZ}5c4z4 zFzn>d(5AUt6B@d)p^O*yXMZ_Gom& zu}gVJUPY@IP|pTBX(X?#Rz86sDZQCgVRxESh?LV=(y2XjW=JJOhna44I8Kd`^yOk7 z?2acTWa>P6l8?&zIlMjD=!T@aJTpJ&k)@3QhSp{bPPL<+E_=qgE=1-PjfLYaIc3Oz zHI_vp;r*YtH_M0U&SX7LdW`8^T8A{~ADuBmE}qhwq}z4tXU5Yq76> zaQ8P_VgZ)zDw5mbU4HPQ38;hG5?9=^ypH(;0;Sb;^7Gd>nx>ljhxZ_EBlbu%B8?D* z+Fa>`u3O8@?3Wyk`!-B!LtHv;{&1X~-9{yrNRWHfd=q>W@0jdP1Gk zIPUD?|J@~%Dq}t=!rAK>>SdDLT6<${wjsdQE5zy4n6l)PYX0>4 zIl~Y}g6!KkN~dS_vz-MpG_A!0`|gJQ@;RoZ#zo?xO^;FoJiQU-1>yG}Y!h~mGMwNZ z_k*6X!6m35zct-uWN0KL>$6Jap@kD=I%-@pKFw*)I8Entngu6WoHhb& zWI_y%%ClQuf6ef$y}2&OB&gYt6VC!V;ui>zfb!bpy?Fa8>x~))8_sK+`fFx{6C73o zZ)-Hp|Mwt*%ljo5t>srI7>{0$SrJeKf*ye{Kn9tJ!>(wjca%9frnHiqtX9F}$ZUQ- zY&yf}O=K4~$rs64IlO@M{;`mKK4LQ;g4gyrcwE>vr`fJ`=QNI6i7D2@wB>0iuxQEY zMi5BuhjuJSz@!{2-k6He03N@`1Bwk@K4YM4b`xN3J01^Jr(i4-dropELCr#WDweWh zVin_P2czITg;hyJuYY>ANwOl*w1l$=5A=9+X@`e|8t)>#Oe|8r0sCX^PnsdIm{E@8 z5|X))5QJv*Nx*W&^6m4+N=!^kLW)_Ti~lh$x9v*+;|RZUc(K`p^=?AXanf)K)`r4M z&EqG=Ol*se2r(XYdZ$Ua+p%-zT>i(FSb%mG&`G|90I%Wu5z4Y@f2q0Otx5+uXiLO) zOPl$^bu&U-E}HBLlWCa~JYz~!gXdi9ktza;KsyK&!V_oB>97m~X>=MNFie(8fJdY= z>;r0w;^9QEXBoQl*u5X(c5E zlNdh02cET4jzO-7go$eW)Inu=9C|ce{!JueflyKD;EuCK%Qq+WHD!_hy{5G)!UV*w z-aK!FDIs<13u`6U?Ug2F{$y*uDXH^KbivcAfG?E*Y^_uhJQD{D@L9_0%UuQVM%97H z)*6CJLKgxO%l3pehk}SVnQ~emY|52Tz%g<{v79=_yF~R3cs*@M1CNPBgv6CELC&Nn z=SvY^Mw*%$3_(S}f+yaTB%Bzt)R3D0B0zo96-bO^a$wX5x=@WwjlF*&Kcroxky!{yB} zbL0?4XX`^Coz`s3YXgWD^Rzgh#MtHNMp%CDb`Qkq4`Q_j6UU5RXR~zCGBCdE-&S!W; zXebG)t8LU+Wi-QYm+X>hLwk8uqKA61-<>^}O*a~s)Q5d%Q;zY1-gw$@lUVi^lJfor zrGh$=6C8hZWGG4q#R}`~vMbouxul{5>%$(c9GKau{_{Bmb|dba_cy~d4%K7NTZ-2) z2`cZYyZ*f%y%(Y8MddWSO!5WuNXDc_))Ec`>l_zAARwW~pa>`eL5Tpph=2Qhw=ho9 z={Y4qBx$5)v)bMH#s@^7PEvcJIzT1}8?DaKjc<~DNW8#XnQiaX6y>t4BE>Fq+igyf z@0TEmzXBdoTHoX-`TZp)nEHwQe#?t%g>j=!JC@`qu4nl!5saxUg4fd;J&GifuF6uVGKDaT5F|x8jbRmJa3r>T zz^8${vOAxR;&;55UIcm#lEI0gC&oF$4rt+Y<}2DiBWytJ9maiyEOv51&!_p z2+(_OUbk?LSy`==1weR-yRY45T{`1f^YvFt>+ z%ftc6GTNc2Vdn?Juyr3+ARLh%gCd{^C<4cT04p1@05-c**`;ial1KDd&Vo3+HRpUg zt)s(vV?drrus~d#$xw_nr3_KOq!^=t*ftim9v(?+%XSsY)&E!}&wsVgPLd21bieAU zm9l!TOKWvV&p6LKJ8~qi96g(q;o^r~_s$Zlo;Xx&ECq42%(Q2C0!uaah>J4B-tLDb zS7Vqsg=1n;6vyKunGj?Rq!O6ZGs-GTvk|MxhjCk@w-%c)FA{|yaOR;-0^7=5-d0Xx zSOhbp`G9lETD4ld3%_hcg4RR-*#`1VjG{~|FO$IN9ixmHiKy+|B__=DuEoMsCgAz! za=mf>@4sNYP!moC!d7T1SWHOz0!5ip2O=dIPN$A-)Rps$qyV*Ai4-M zIEa`Uo^Ka-$9cNEkx5Vv=xSs6R>qv#&+p#fh8Wp8)16C?GG8c2!s#t%7caf>#LOC% z45ENdUl3A~eG<}ZP8w`Ts!gb~%j(eo5J*Ru&BasuBlffj;VrH*ru3zna2Z2AUE@9R zlH&5FimXJu>N=FbWs)2}>nV@`LBrva3HV+0;A8a*ylM|TWgXoJf@j{ye&`my6c{Dp>l_N!?RKcN7&y- z_4Rw+*oeaQS)Q%*g*x|x7f+N)=(nRCoFV06Hq?3|yJf$(?N{gW;b?pUTr}Az`}8;h z9s44lpsg|MXs^5GC>bnM{@WT!^Kbv-3NB5B%t4MjCc!SpR*-4YEy$P%6F`SYXEfL~ zwQ(3Tkp#9yqx+MZU1iYE;WTFbFcZicQ}Am+VgK~$u2$|-o(B>+vN@BoZ-RDP*5iD( zlV)_f+^mmnsH$z2gt%~-0j;jCHfojq@5ehmyP?=D*{D4)Y(4i@z?Clx=FMH`X`qgDIs!MGIux$ddU<_K zwmT3x1aEJ+hZ;jA1zA+L&L1KDQ48*e&wg!O>b1CF_7+t@J1$Kwoi@M(sMf*V-V66O zyA#-@5sj{jM7%NX!L|2y2l^oOzLqc0*b&fBS04x)6}h5JtwBz_5op} z*~}h}ca|&FS@U#XQHuyB_d&r?>yqPl>#pQD&1MTR5S6?PT+Q_LnVI-U%_$y7If7|l zMM3Y>c(@X|kekqv>;3+N?RIe!Rrv@6lw!=7qtLL~vK^H1n2 z%l8x_+ox7~z;#JnnPC+UGEV%M6?Wc~zVhyd90ZA?gif0c($z}BOtRYNJ>>11=9pFKR$ch#rACT@ zRf1X@YtNEgyH5sYMu1?%8`0TjIcr=W`PDbawO&QP`85K(4{UV{y2&GPzH!ImgTPa` z&qEM+8iI@U&!6mq+(jJ%mfdeLxJ2^t{SQla$nxC<(2*;ZM6A#2o%a-9V0oQWvo0Fj zS+Y4#Zks>K6+__Hh`AcCstb0IsG6ns+-u60Md&{bM_fimxi8AdOok2GZccQ z17!9CA9{|1Ud$)2oPI>oR*dYi1^{8QJAemwMM~9ncs)EqSDZLl-iL11uixEbGCb%t zBmrYzH;sX;cb+}Ql!E#r(*B*~jPpr!BN31njj~U!>Ikm%e8w_ZB5Z%b>(veLhPFPZY?I62L3Oqt6Dc4|j2XW% zbBG3@Pdt4q0!M;R5WYu0eM-AAo#7(#yxn z1;ndA6y&Ke|IG5eX0;!~bHiGwOrOVWLy*5Sb0E5{V`UkyPb9O=ctA4VjSotC za;E=3d)EP2Re84G>`h1lN!Vd;6q({i95||0+}o>Oc~-1tdi>CyvIK5i%CO`Z6^pAT>`#}_0Aom*Y*>w(m;}rmCs^h4#(RXF z=D~^4YRDu223}s2u4-oLb!h=4$RNmL z3>ewbwJ^cwiEq=?$Dt=UVsiGvp^2{qKNLNqLjW?%*i?S-v@m%}$0)G=;(=r|c%PoWe1fn<)K}|sl+`o>R?|!2w!aGH zlIU_IhmIMNaG?F^Wxqm!@&B86eD7~)33J?UQ zf(m);;_>d7yVpP*Nmn}wY^9Z|D(GYC;R7hKr3q#^kHCR|mGOVYs5M&vrh+=m(?b2L zON(nu@U4Y&>Ed6vyI4qcsh3?csedN|ryoXiyHmu*0_dyW{BS5hXn%BA(&2p|SuvBT zYDt`ncNOn3-fuI}`8#uLvYhq!LiLW)dyNbak|!@6uiis?p7)T|KblJNEGsLQtg?OX z>K;STmX08vw?ao~jctR~fYpH2KpSbGJHV%zvA+paQ%hhe<;(b3lxZ@V_*v447Eh9C z5?k<(Aax%KkgZ%;2{BJ)48Y?H zS4~v2XlY|2*8QJuaIZ4BN3=R70rYspYiel>sCw5t)tTH|8RaQW}z3}p6askSig^_J;F5qjW&`JasMjqrkE(DBqsOjxK2RnQ4map(-NL%?>d zMVzoMu@wtz6t5!_NZLvW(9aw1Bw!Jsz6Z)6(vtN;+%NS;T^FL$!|DYTh9b~n%4K!1 zfb$yAl06iJyL8RcTJ|FD3n!Dmty*pO+mSWEj@NHa876nUyBeM1D%CO5?aje++!OaY z*)1OXI#s+AEAz4bIZrIaJ%w-=)DYGL$Y+h~B{N)|U3xim;wSgZcM8EOJDQQ8xhgBG zmv=H(sZRSxHy@)9eOQMUoE}#7+k;yTSPit72AD}sp}YDS(7F7$qey|9O`uZhi{vC) zFSM}ii^C32Sn9aboy>G+4FZlTfDgLRzeS=G6Nd;i(sIh>FCVX$uQ%q%-@$XL+3LBK zwm9yB<@EROuT_=`f=3FEVzLF4!$mBv3Q$aXGKDUFv*sz6{TrFd%r^vq5LGoCY1p|&l->7bs@g<)p(KVVU1d~X>(^aQH3 z$dkE1mj=_8QCjN>S_45huov}B|q zaj8tGGgRftlgo zZOxVWTO3L$wq$V6Xa(ZPGZ+IFBOzS)!m>ndZ%;OYf9jV6df9OZoM zkA<^;e*7S{zW$+m?j8WsEvQqN00sbBMbH2@MT}mLF=ed3N@>h;s@Ky)GlrEQm@*cY z+7pl#f)hGH5J+k*0#lwdBNw!uxScKx!0X(W!Wd*;$8y{s-NIR;v|c4? z5g4#|f(8X_1Mic1Slp9i!%(OIotPje8=J{oJcqNUFvOI=j)_Ct@Bz@$4=?vqM_t73hG6V3>6k0wN zP{m9$S&eb9aR{-Pw<%ZYg`En1Jjol%4gLu_LnIXzyfr&Z@KJD@ z%TGfw$`mrgoQXUHp%l7x={7Gyl1vh;Wu!i$B`*sxW2Tjlnck!xBCurAgBDua%w#4L zq+Lm3IXel}&UaidYPBK30Ixl- zF{K!mQI#Ww5Z)YEKY-Ig-sTo7&&KAH9toc0CRJ z>!KqYob)HSXUt?gdKjwFv%#&WgdQMT2cZHU;JJSM;-i!Wl>D6}V5eMt^Z?K@r67ZLfwGu! z9)T%^;7Jb|i%4$HFHv2I$DuH5o^h%KuDU1y=X1FUYV@(V%mkjCrs(52Qw`Ul<#rHDw54Jt zIt&fSK)h;yxLDIMy#LdUoULf4uJ1rrE7hnD`tzTsE`GQ*V*vvGQh#p;JyoY!^z}c2 z7f=v-H9F{&OM(Mou6M@_g!SOF&Gg-MHR%aAT znCoT8UNw8_wdM7a1hXohKSj3699pwZ{93enc=T?MYBgXr(9RkliX-tLedtkw zj3JlFi)IM=z5pVX(6{`Xw^xHd$P?74krP+=a(%X}%qT(-;yx)sd6z*6$gJWQ0iyl( zG-ZxNiaSk|uPn)MJ|tMqEFK1bkzYxpW7^pmOr}}txgV3UzcI9GB3rJ{AX5szJw!<) z34P=GscK$xKVd-Uhf}C;UKcY%gl1G`RWX=W3w11F=}F)NX(iB^rv zr&nv0A;~Hku)GF-Q*x1Mf?DpJnNtchV4S_4`G~Jz52i27zLP*&=}<}Z+^-!bPuYG8 z?Tjv^fq9^X*o$`rx$W}Gt2{kmxCgY*^+VulmPgh-Wd;FoJh@wh%E(-fcNNKz`HI3K zHPoC_T(8=-U;XaV$2i`C`Q%ErA!lrb`Dez0b{Y@thR_nita!nM1 z%ui63>Rwng7yzp!7$(lXFs&*67#c#blZHX4E^~?A07EkyhqJ30Aqxp5b0~x#u8!ib zQw?h7CnX_DR+W1K@sf~Y%#u{v?A^@f?(}6c{%Zj@X`9K#ll#fK_lwlW-aH6_slFE& z9DA87z?7L^2dXb??!%6EZ4EH?MqGS$W)(G8-&rY#onh8AR%O;anAK4nmIdQI!0ak^ zxaWfcfU!S}i}^9VKm|*?fh@KtLJ&1;(RBwH?kEJPJQ2^6?3|YUwHmM*uo~#J8elfc z;%x=89#${YQjo_oQm&UgtytcEZ<3B`aM}yNX%-mqDMt_PA%6sErwJw#z#;TIlwg{K zz)vmo>;D$k*i}1=Jr+~fLWYhN;rO(6R_rWPL1hhtP|eShvyryW>HTZ#Dhu5Ei%_SU z4~mvkQxu03X6TQlcly7%t~t(ZD=q0@0AQWdurMb+L(kVkmg?dM+goU%bz8Mv3u&n~ zv(UyOd+Vaf1LWE7oDPfLhX$scre&UdV7)+$Yo&cPFd*r+V_Gv7pnY5KIhyhW(A)_L z2EeRh2igmaGHcNB`Z$?VT$h`;?_nTZa-h)@#G1|It0EOiF*?YF2%a~ku&f3_sR42! zVrWQ7(ov-2w4|eG#J0m~z-pk=YJir-CoUN;_k%$64Y-iC5S;F;a#HylbGMo-|FoJi z^;Q#co`{;JTR^I+|3=^ozk&Keags%NkhP=G9_QWQRS zVFtB`<67@}U30jzphABA#!7kiiiw~k@BnCY9XoC{;H`magA?RA)b6Dlo?wwxA9$~k zHiYj2a>pg2T1a*YK_)Dz+yVQth)>l*x{>HGxcQN}$x0AR`~M6XSdW73zpRSH6CNQC z-ISz2(2k^dW_Exv3r;)0MX*h<8n7B@dkwtx<4)O@Tc!>~tEDNqbgQ|9);(__$kZ{C zS;dv|^5PxB8m5$7l2sY>9D56v~s>n8-G0*S*E`G<-$`6>o$Za!*f`iEchp#TpkOw|@GKbjy)5i3YyI_fK zsnCM0<+x;#)29xUrxxrG>YX3`D~=w3GGLBXGsFO@dK(UJMw>_*Kp`jB|IjvCwZ{*L zmDAC#FV|-&LR(%d9d}Idz%**Ne;?b@;>COEjb#~fBQnygSPl510eTKDo05X;!0if@ z@`>it&HgB@^qPt!9)!xI!C0PND0Ntgap#IQ73gsAqOiYx%ef!F=VZ)$Us@00s zfYpH2K*!Pmlaox*V1)A`4C|Y@4qw_#;GyO8jT=0PN3`(0x@3oY)%(vIBU1+@xTnb% zL$X4;l*xm%HMwk0)pG7WQ2=U^^0|Tv9XbH9f&eo#Pl(V|82(n~MN+_`gQ`}T$}7~PnjJ$s^t)(LXyrI*Ut zXP=EYii0|o-klv<4Ok6W4IGjNNDQjwA8_%1575$>oYO>cc_#0WGbkG$0#kN_h20dh zb}8pGo0}gMT1%Cs!}W!rWtJ{yd^g~Z3se@@KHZ(zNCF{R=~%MLn_r6i^tPhv8k>hX zA)ifakr2c`G-u0xMvbV@5cwQHk)ahW0ePAbm5s>3W$6co1%^ZH13PN>h*0RdR_^r4<% zw`yH|jp7$;>=<^Q)quAKcz?}>$AvCIIYBV zyYB&!DDyxlq>*BB@jE~6k}IYT0C@N9Q)N<2kxdq-AX7$&@a=IhGT!pP@swm#0>K-TZ1kmJw?-u3K z$3sQW54|{?Php9va4?N%aLPohrRn*qhVR{Hj6l++r#zdpMf?By^u(v8rph(fTq6q? zE|jRKD7pOd%VpNASu$@+rkCYZCr#;X2^h(Ue?KgCB*oSe0FaXd%SR|W^#nohfJHw82{l^YUy_NWYrty2IcZn%qHAQm&*N}YJ z4L4+tE739>w{taaUWNta8QJ^v=IkymcRw#Y204*80UY1<<|=ss$yI$^)>L1Q-`eZ> z1XSnGpD$atZk0zKc|@+e?mCH%j&8OcT<4^dPEvmlKm4$~|Ni^(*T4Q%-hA^-3sjrc z-ho%3CB}Sl^P9zzE9L01Up}Ruy9dv`@B?V2Z8g9WK(8;`>C-_-m~7Gz`LvN;sZ}*V zOW4LNC&{T>M6d!f+Pc64G3M*t70#>i2o!w3z(p-Z7rz!-;%I50JKx)-&*U0!A$=9C zaI?SI=w8H(++j(vcwU3$hwTMwezuWx^~Jg@lqvGm1EpJzZKrr!QQfhqL{WYu{7Aya zsanip2-FclD(BY%TvE)Ax0=z+PbBt3eb6FHizY!TWiMhW>R_#2ECuQ z9Qp)KPdVijx%lFXB{Fh<-Z)KmUB8Mb^r}X<#WnVCv8`*UX+%v$k?Z$a69ABw6MC6mBXt6 zyo23Y+DS2twW@3oBRBvxI749xqi9Ph#~WBtUDw;&g39$w(8Foo3w$<{x`m-*k|3Y1 z+0EHX*wdxJb1yj(2O$UR(=S->H*} zpI@RE3F)TT;h>)Vyv;KlUT|4w64KVSO-Wz&k=t)rgsOTuM6B~)ZXeMLo4i!6oRfxl8|B5x4wbzH8 zE51GGiTyq`|FECE=c{>uru6IA?*L1EUqtlI?0zdA4ZH!1Te=seP1?47vb;7;LP9*8 zc}%2OiV6@U{MM3LSP7zQWc8tkzYkR>c(dh~)+m5ljW_9i=Zsbs2)(`?wv7e|I_^7n zjM_Qfk^eD z#nHWE9!b!zg_*Jxb_%`4n<3!AFE+UooXpUI)SNPT_{+^|bGCkm6`MlCfdHfqs zC=c7?0jC}-2v0?KTK$RtlqY^U$nF>8L&)h%dw`2ik+q6oc>>$w3*B#igfLXe+dTC3L8^EhPNc&(I`m1q@`2>@JG z3m}a|HkPzyH86$;Fv&$Hx*UZp=qryNpw_oPomvAGX=mRo(w?yZUh0en;A9*$-zO31 zMlm_F6z??Nt_i3VRa;yymG#P|s(8F6p&AfKq6r~{EbsvDI<@fKcRR}c-ktVfu4zXei zy72tkeuFpVLhz~_KdkY9VXNuZ*8q!z-}UZVRVs(RL|ANO16arJd4H{3H7!L2CAR)i z?H*gI0W(u-A=3Got)zt}vbT^)!<79eEn(D`H58d#i96Hw$zwD zv2yQOqhq*d!e2BzDMl66<_C|& ziv&GsFu`ovQ>-HR5niJF$?V?qR-*rpA5_*eTLi8Y3r>}K+x&;=-S>U6LH2?XnWojM z0%Wh@qZJ>3->VC@7Qj@RuK+T z^eq-R$GYdtk*JZ|yF=B8Y}jf$)fF^w{qcijc}Ah?Xz_equMx>dC6YkA*R-(Q8s8b@ z%Ohx|dx6-M=Kl(G)M-jR|ENBC^}{x7b8yC?LHt<~tU zvq-peOTptMsd;-<(9{{@l4S(AYA8z(h{_bZFa-7n1`+@{94cJKS7{z@#C{-1W2YzA zC`ZHPR@zPj%TUfE3;Jd~Lt3)<(d9>vzJ3M;(f9lQ%3ab6+ zECL?Ql0tl0GyasR*mKrr%M8>XWwwA8UQ1rvH7NV^z~`F~K$)W#sojBg<3Yrn&2!vYiC! zOy{Rn`XA7(7W&jp>$9HU*pT~J=E^qX@@;ue?ONxp>TJOd{O+PM=n(HrEC|B|Cj&YT zjcd!dC^`k|J?yM+{=SIvJ})g!$F|r2!fVA+Q~cBNz)!~tEy7l;2Aa@7-Mq2~McObH zfbk~(y6{N31K!m&@DjLSg~A2wWHYOmu>jW{I|!E2I&=!NRS-2h>C7r*$1wu&0i*)z z2pN8D4pa*Z3uW(KCvCQgi`M05^Cd@C-2w5@sxH}{B1AGnD=@;Tl+$lE+{jNpi!K2zw)(4dw9lo`0Tnoy7+k}i0@ z^jTbi965=)#G8 z+3W)f-*vtU`J`qdLrItU(itMu3#kD#i7Aq@APB>OeygBu8VG90+r<~JO5yh$k=Ipc zIrc+X!0|(SsG7H2&z9v_mP(ql*KXawkf_`XpcUfk0J zrW6KLvl0b`mQuc>p8YOWl};PiyPI5#K>QOCD$tT=PVKA>emrlxHrTn{rEePe6t(ym zH%99X-GeF|ZXaSsc!%IWKY0IHxLv;Anh&m&T7Xl0^zbqbs3%7Y-=-d6>}d)1!H*~{ zKvvpE1Km(&hSwqnF+7ahAt869Tvd#O&348;D#ne%X(O5>E?IhzTkGoy`$5`xs8siW7oorL}Mdmvf*VdFogct0{M-5u2nVpx( zDBJV$;vH(0V}|sUV?oJa#cJSiYJi<({eC%maDvK=$^daXPP;_}a=k7mKv7>vy&F$g9G#G;}lZ zgI84Uij~6qk=8Z}W2*LIy=6NJR3YAiGUxs24NSQ%H>t)=cI|P4;byWWOsJFgkclaA z2ucqD$54$*YSG8!_1H&uAijC)cNfX?3%1E0!2uR+Si$tVcG&tFSb#L~XTM8x@0ib$ z2B)|rk_neP`f)05n+XPdajRq_JbbjkX<;y53Q63yeO|3SG|Nfpee>Fo z$BNg0*9@w~_2bitQd2JTG}-p~PIa`ex5T{x@E-#Ho+_k27sK_LiCUCeYzI?o!VcR) z12KO+60t5Viv_p=aj2%y!}IIcSE{}j?|UtL*9=VxkkiK`>seA+S*Zx`$Bymqp2rbg z`?SH#cAW4Ts6O}Hb24w@Xya9vf`gNFs* zEq+M#$BbHHe4P2Qx*I^qmNIGI`-ibV*B>`f9{VO0mhOgDJGO)t@r0;w`DSyDvc&xT zys_=uz%F{Ulfm^h37rTEtptoOo$ZQqB$(v0QwYl_ES*6;-#twTEUXgx+@Ht(O;YIT3QP1Jf zlyYpVFDLI6e)zQ9npf$kG`29%Q-3f0FEBlzNB7BFeL^9Q-ll#drF6%!- zFc+1k$!V^tBzixGd+0MbCF4Hjyz|a$vd(E^C-dTqFE%#Y&U0yiV06aFWRRHdP=FH# z9Tj`l0HtCbTIYDvIfB=#Ym_fdT7MY)ua}d(UzesKTQonkR@z-=t*AC@Ob9CnQFlFd$yWBJS#2 zbQ+k*-woIAvZ(_=Z8X3Q^jIZ>@mtLNCOFaJ;cKUhKfp~^%&b@fVUwPU+b}*%i|O8; z4_BK*X(aNqW6;6*daFv1L8W zqym)1R6|WKg_xEB_bRxS%8H5_rZ5aJL2WsxqvIeYM{+Dz;_)JIpb0N zqa?7#4NjT-$x0ZEkNvxXevj(rBF%*3BNcdjZ4b;%`{_W*C2DdLeT)_Ast2u zo|6>-8oj$mB33j?Egy~SYsQYUpf!cle6Ac8d1iS#R#Pt1?tPB)I)}jvqL(vCZa;A9 zV)*C!CYS0v%Xg{|2Io`E*wQUJRu=P9VV%`tX7QWgl;H|pQFv`>;iT|+G==Q_e>-Q4 z-1OQC70XW*>36DUJzPP)V3q7sz!TMWAp4|0?vJU1=qtMC1&?VFJLVi>VLkqWpbG@UB&p>s;gbC_gmM>o}Edi<>T>%}T zeWV?F@{;lL=+~+8=CTY$w9ii;dp-Jtq+X#2mSVQ4ex0h9B;KzCqraIkO#b-MI(0e; z;NN6yo0ay_09`0wpjM@pLL+`K2rmMiam_emlCRTPLwB*2_R+xSxHfdJGa<<=`4kMx zHo``cU{E2aj81AY;f2-+1xf*AOb}_do{5R3d!2PpX}L2+Co+&BpeI5?w2;lgl%>eB zz390mY8F2*8dqNmE4}uPnEa>acvp=@eGKKd)`7p4!cYxUNS$-_OUqO}Rf5q#Jd=Ef zvH&?AMJE}JGw@&r;vE|lh_EZXQ;X}s+7J{h(?=$%EN8t>?X9wLs9Ga-*pW2g08+=R zNe;$Bu2@xLwm}1o3jzVPXb-f zOO`KDMJL5*ewT0_0V=CRys&t?+hjL?OT)}Py}li`jRqKl_QF*Y-8;YJxy7=0TY;KR ziV0OU5jnvR9xZEh0k^csxO?Yzq{SLo3wIZV&x-CSa5^6K|H!_O2lxGfYEtGSp@8cZ z0_4!Up`JzTi$e`m$SZDLNcrOj>k;Etft%1b{j}$KcBNx6W)MYc z)hU~9UUDcB+)PY~2YrVn@;)5*gh^_z-CWJJ!|_B6mIvo-#>?IDTo9V_HAL$hEv7N} z0BDLS?$5N)>iz5ZQyqSz=X(zcUKeDuE^Y?$bxfEpM!lf-)qca%J@cQhr#MxGt?pyl!L-V+E_}NUXc#wBcM6+f>UJ zU~w?TCPtasu;U#?1AI^103OqaP&T-bp=PcJp$f^IC6#$QM!X>6!~#@-e>+-cP3YHA zn%-nPdoY1A0UaElclqpl>oO8~RrAwE}Ew|jFhOfNxicg1I zwrrWa{r20El9I9?xE(fZm{0p?>y>Ugaj-fBEo;xXXmJ8y^ob3KMx2~u{b3}kX*JPp zq6Mc4A4=BL9ff3%!LY&-sAeD`0E?6>S+8q{?Wch{gI&DL7ipvzw%E(?Z2W$}QtP{vQ4p;KZoi(_g%-Nn?{MUQzFqgxM_JNui5ha}@GE@J?hT=+$La_4XW^R#YZNYB7&XUc;^` zOW?V95FckEP{t1HcP7F6i{O&(0Wf<9tgZit4o4AyYb8jh3HbRQjR930I|yV);eEoc z{`@Vu>W})o;rPMI1HuG2EqyJQu~vm0b}$Y66eRq76+H9ZHpxdUJ>M6V;Cd!+FS(uh zj_L4HH9Pc<4%y82-+y1B(~B;;NM3vGHH9wg;b$R2e)ZK?h4ark>nxuYylT}diH+^x zrYpQ9wI8s!8f8s_Nx~PPzEt!vF{q8-9)~5x37(>dit`yj$8J4Ys=o(2FUv{gAWb=` zo5x@1v$E`phok|%35rosfkL0_S-ws_Xn<9B)Ys^7*HINo?Ei< zOc4IP_Ctp3M&>}3X4i+}*(;=3Z3@`e$@A2hFzxlNA9uPJoirjzb|avbu>nDNKC4k9 zvkYl9dysBF7-hj$p}rtx0np6{D>L2Ok$Z~Bhq4`YI9V^1LO=5dH%(LH<`2=5uK1wm zo2FDVUctB_X-1F5$5FkyDRfF*)4xw)@an@~ZdT(@e!JBj)1hYI=gDtVg~z0@EYCHdVX$H~a5y!< z@6XpDZ>F%9`->9?sr#PT;@t6l5zo|JR%JNvsQxH5Ny}D8l@19`pMCaOrLTMLxyNnx zq;H?o@4fe)JpcUjEdr+sEr+%am_XP;$A?@`0$rZ?6ql17?MXpZ3riD0IV zoqL$*ShsIrk4Obf`I0p2GCmDcY)V4Ik}5xi3CJu*V; z>%aV@X>&H8Io)YwkSR@-OHKduLT&@F&Xyx z)mpK`!>IvwzF%6L-k=F|2_%AcV&%?4@J~6wlq;M%Dya!@`tI>H7m4i03(#P%?K|NU zP-XglXlSU69eZdD;|wgNMf3F2PjB)p8Hl=I!2;>uzyE&68$W)$^zPkTRSjS)fXP_j zD@Dt|b0B<2UA z_M+2kDkfu}OFlszRqtASXBuU4^m=yKx*A}yZZff_fyaY>WJ&@e*GpkpWxNLK(K0=o zJ=}-pYywyTi$6fg%hyb5-GXHn3W`3#^AZT36ob_3% zJltQ;8SS>%>1BGT0g#^NePa6g%}h$uiVDu%E-8vb3Y`xfKSOE-zBg$AAX@DXOKM#cI}#TPfSX2+Vv zVz{&ndL~>LeH?y)Cay)jeezGUdhZx2qwo zH<2Lbq#TK&yWSvHEwDcar2&RjFyN5V7cP`nel}T+Uj*yT4x|{<`ZWZZ7(Yi{I%Z^0 z^v6(5*q>I_z}K5{+%-?#R!Q9dPGDj>f)mr-$9oW#)&se`elo7N%Aj#?;59-(dft=Y ztGNutS%hctsd-x!K%&J?2f(s6?mRrx3=q5Rl%ZaGaVEXNDLXU_Ue%)c=Q!N}oFeR? zD+)V%q(WH-+6G#i>8Yqfs8cN1$tlbfp=J71ghREI?%%QVW5?0uF>hK|+MyqVjjtZ^b z%y#n^DctK6><+H0l%1Q`%GcW~BnO#m6k2$GNXwHAyYl6fL6LIqun1-CNmomo|KYQh znwl!?#C3J4h1<5Kre?o;qok@{-rP{8)I-wJSQyV>M3YL^ih_g#zztFfQB^V`AwRU_GWPDViM1|~s z%<2-S{dyZP#K@|ZEA*sf=jOT3dwYh5ZM!JnGvB>(7a5t7P%W!>)Jkq~sXIfo4);g_ zrYrCQJTnFH!7G-xNE^JDo}Gu%Q6QfVG0%m)9D35)#NKP4eU~6oP4%!AQ z^BqcB9e;X{wt*VlyJT(8ty{Os&Yhmva`(b6Hp2I4b#?W@+#8wslWsfcSb(f7c#vF% zoaR*S6Hx8my}La8@WTqc&7M75UVr^{`TO7h?%sDuNQlJ8$0KndND2xH)B~Xxa^AV; zo-4om-S1@DwEYtT^m=+o7e7VUSk?3Okf8ZzypbL$guA9vhw^1uTRD8M>q zj1&K9?khYzT*ygf3X``8k~L5^m$mkIMQXNWlm)utaN*X6#q+4X&UpuUB|=PaxhVg; zZm0Bs#UK}n9|+5W4F@hkd;m-Ff~H1kfgxBWgZOE~W97neJq{QP*V^ZFu=``jh1a@g z&z=Xni+10xsxD6*Gd40@(kmn6-7N(u-w}jlDOUoO%f&xudzB3687rp`@9uLO2~@jv z>(=oT|9n29gI}?#s!F|Yyzh+&5Cr$@(a_gDXLGTlz2MRM#)ioVB*sw2Bt^=ReHzAW zs|5$~6OZRBARt$Pq}sqx*@JQQlXOF<7;bEZ?h8J{EMV*uf$4E-g{XtH@ zJieMeV3qqxPV9+iHdncvgF-^x&qPQVpKzy0d<+tQy!CMqr{b5JoQhXOML?mvu{sxZ z9_yXX2|6ml2#h*&RIFS)DXGQpywn|~QdM6h36UU`W$2LJKE_R?+69G4fW{ZoTu) zJJsLEAAc;3fzzm{sQt)NZ@&3v>DRBHBqk;{ci2N-J_rE%+H)rE2dtag@!vk#h`_60 zNdWNNTV19K@G{o16yc~~QGt8QRU!qJ?7gt`yuZF!s)Hg`{Hl)EYHHtydwynSW|JWw zhr4Z!9iNF8m9^{Dq8{C2gjk-!!wznU>zC9zQr@XRX~4PRBVwj>LNl06a{7AAoq$aw%;0}Vgn=y1?b5{lcZ z4jqw{loVfgd05t?k%*SEj^57pQVzXPe2*q4C%4xjJM3|2QQom*N4wn{zUB?J2%Ku| zvbS;Fb=Rps&Li^9%ge*dp-$+UC%8LUVNw(yBopkK`(lC>UkZKjQa@PHfOPk6&&&T{ z_tg=$OV2M;2^wsRx3iEIR6UPw&QQ4bBan6c*;*%gij34&T2BLHxiIXpp2*ub@O~|Pe=9sWW>LbQXHYVORQki*92|+SsH9DMT{HY~D z75NLUv#xVWJuw5Hp}4z>D&@XUH*l7cI3&rv^0O&=ve{vOux#*SO0rbOt+E^sV_BJO zN8$XTt~l~$cV<_x8t9@Lxa!*TE)h9+UVwuFs@}S$OS~nPQt$1}Y3^&>xN*YdELvwx zYZ2o{HgDc6j4y2}1i;)^Q{%L}P};*?-&7k~bAEhOh?}RcVZm^6rV^$)+^MvzGMku* zIxL?uXMOeo6F*wBja_A5CQ#K`*R;CiBXNz{-u1Bb(9%i@BYgi9ebIX>G8HQWbu>IF zMvflr#9Mb2owdbz@;dzAS6dYSD3gw;e+ofIA`)iEmOmE^{>;*DO<@)-{Ym8Byz=M) zvIxh?HnP9jQRFg);gA=XuKUKsJ5fpl)nHd5l$phE0@bd{g?inwgH-3h)aCcze=id! zj*>7~K((03W_sR%hGYA5m(i%1x@K1)60O2j(pw$!64>d~j2vel(fdHVu*<9ltOgE3 z0}X?;eB1%ujI1Tfho5mRtt~7g#l)nSUV5p)uK)9&|ER#)OD+K*H-rgUT&6)M7wN!o zV>LBOYwtr3J*37@J@r)Y#eO+`><|QALHLm(@F5;0Bz45GAQ2!@Xu%`DBP{|uVZF1` zx*9NRU3N!2@X(}aDMvhHIbu)gUeroqrGfu=M+A7aUukdueN~a&P!%apK^DhNaVB-)zVa^+avSKX} z5-0reEOHh{t7}U(6?P2BJ59^@jVBCI`)|oU?YdS2Rs+pzphcEb#%x@D_0=lwRPUPZ z_$#lxQl5C?37_L7^|nn7nL+=;3oppM_ui{w52sF@s(>&Pvqp~|t>~`IZCP3B$tRyw z6{iLc9HjZpq!k)Uo-Ljer$=1wb;7Pod>}Ac|*@sQ+Yy)@;iyQ`dYjieh^U z!vaVpSA-a43a2S_@lzVBB1NMrk71>CHNb)=FI+i6-i3$bg$3JSG2JJ}4^NbffsLKiz@Px(g+W?5O8qM~XIbuyOq ztFOLN$x5VzqD7S?^)qMAREb&<5e>6mnGi*$0iEH`Hqd=fTC9Kn``-;R{u#qcz-g{mN5~^^X}+=;#Qyj$Dx6v$fX^ioGO(PkW%aqlN7JzWAXqP8O=Y~~8gMxN?$qHHthVqzAZ^rf zBU02>#Di;h7t34Ab|PjFG1rJ2?1pPljlvd{E=#6%fisq~Il}f9Hbs>i9+DU%Te3@4 zGSl-5x4Gkvxi7|{vI^WH(2enr(p0%qE+coR2CDnH_#3gJdKVesKCB$P2J*qeaOb;g z8|;9wsC<6&(8+mfUYdIuLq%pyXxb|7UeS(N4Ok8MqyeA7={xVdqkt*F9Rc5oC!VNy z2+57g40mSr|K%@#kr5++fm=bCG5`7JpQ{8cCPy(2wW)Z~2OoS;_-6{OsdW7lR5R&{ ztN_f~JCp<~bN?M>d_6wK8?&73@R6X+Ibm3$%tckHVEB_m!I4^4%^Kja>F|xJ&?#T6 zRWfZz4>=RW<5pT%1B@+n3v^CP-SNYE%0Sd zj7?l~YtN^VKG6XxhLn2#`==Xa@wNh0Lwzp|r0Ag)54$`Q{s? z8~W3A@FvZb*Is+AY}>X?G2hRdH?P$Ys=g4U255@8U$1kZAv3N93>cvP4z!-XE=$cR zlP}ioaW}sXsE4%~^YDr0mzc&nWjHk;>R1F~Jv3_^zShJ>R$5a7OySn@`Px15Kh#I0 z(D$4Vv=W4#Lp_+jiCTzTcEj(_z(|(%`gHAXi%^xeTK2=hPL}l9n!Q))ac?EKoDj50 z-*`b;6?nf~T{G;K&jbUtXTscdLL0ab%3Kuzcm^zEl;g^dVq1umKU%d*E;y=>GPO2$ zmNeCA(Xj%W7tR$^s}}1FDf>KYZMDvj`nM+0*3qTii`76&HP9qD-T2QZKNs0A7C`M^ z^E1(e>Mhf!Pgg*dSyWcqR|Dr9*<1K$3QaKooU>Wxqpsqvq6+C18v)*(I@yVFq9gj2 z-*VMU9o@T|OiGD2H@D-5paB!8UOc&<41?d7vIHi+|C@{K82~6|)Kt67YmC3B0dOq@ zpdz)x#YLXrO8WgL*nqcYuf~U~cS~wcsYBCw=Nx4xa{o z^~Oq-fx%1)76+%bh}k1q0Hw^Lpv{!_q>Wi6vS6zd1jjUB5;Ia5l*iO#D+jNEGNkQm zVP*_dj490AVWtboQIiA16$vWwDXnAVts$oB6Bi{{ADyDs=TE=*NcXrMX<7p;e9t5y z`tHjC2x%#$WpPg+-$$@myNsv=qeu3OlkItB^3a!?)efhE)9XaUVH~W`I{qetUbXO9 z(?TGsg&A^Afbu?M+juPPdbv6L4xP$-KV07cR9Dv2%RgPbSzK39cDOdusr;eVD`KkA zj#~{_4fwGJdDL@H(DecIobs~t0{L*2t11ENcGDe7z)FA`9~CMe!Ro|6 z%Khh!kt6#0UUSoP6g#FhP>R%XEjl2P)8w}ybpox{3DA2)S+M(BY3B8ul5*Js3*e0> z3~pc<)BCf-#x+oaqW3{WrjRmZp-~71saIwPx^e@B#G-M~eQOGjL4GhTH7+2`X>HMD zr;yJaxuLkY7wW7wC44>~1V2?ZI+$dHFLj*{$x@?11(8v}vQZ6F`6~g$DW)}5&9FaK z16Bi81AeN37J*Zy(ZBun+YL{(Y|b*|=eT15STBo0CjI@^#r*jAcscpxlhr!*r|mQ_ z9)y72!no73U0G3Ng@0;Zs#=AXrmLn605FYH7AVea?bK%PwseK*Qp1W$&M7U7YrA>+ z5cwNQ+%QGB#sx0)aB0V34Ok8MzXm*CPan>OoKhEF zcwv)kvcNt6yk*j)N%F%FKX}i!)9s)Ek}C3#Zp{T-J?!Mg6}5;_-6{mM_nkXh5-d4t zt6U8;QN{$P+&PnTh#$z}UX23n`;fdAfcQuX=f?uLQ~+pvhQzn?#`lHgb^k)`YGwP= zKm%uk`Th&k7G-H4ZIR{*R015g4&@{+!Bkur6VO`h^n zOOumy(NdVTtr^w{9^7FTST+<1Q*_l5v;+X4)~Og&Ch-8kAe)~V{?<><>!? z{o?q+^2~xX!;l)ydSA2Gm;{iXCpe05t0uOltujQNfuaVZj+OksR6lMsJ%%J62d zX#%*Zc>Nid9F9afmV9G)04=l>7tU^AIb}Ei1=!b$)qvH2)j+4vfY0Fc;fEhS#1mdK zHE*I33vh^gvzs5B2ACbbBfm_=;t{km%by=tCiT<qmvhOwws2-l%lLR3?ecHC;fYQUd0;4@J5 z=acN}ZC?jQe_CefI%JlOS^wM0y7}LkT*{VtfF&lgH9;ezuILu)2is8LN+6qYNHguNZ;ywH1 zotJ-9r$Z2@IYT*vv`jBAa$@5-ZFU$q-CD8>@uswH5*+H9q}+Efo;M~VGSi=o`lN@_ zqAl!tS`XPV{PoF0l~qx@JSQXIjEs-zg@Xc+Iv7(ISeX+_(u@L1M@-V3Fq5Lp3_uIe=8msTpv*$F84ZKZh)^ahRS*g?r^#oeHH zns3Bd6EW)R!SI!VJsi5P=J(pNHRyh)b(G*V867^l_(}7Yh;f3`TyUa}?j0*PpESg$ z9kYBUqQlOO2?PZhu%kqpu1x^W6`A0C$dxv7|kSC6U^Lo`fvaXCeT#4V}NM|WF@3Oo;gYZ*v7WC^BQVkKumxl-6!!rDZRXf zoiR!htdFE`Y6}?$iZ;?%6_aWV4_6GHG15cP<F_lPWK{R zk^BJageb=nBZ3;`>LW*RR0Mh>C-b>p-dnM&!<|b;W9;Wmjv8Y#}#bW+@i33z;FwOA{v)`T5e4Ok8My9N&Y1@-r{ zv+Y+Jpu2GP`Qzkg|6PuH@SrwopchQxKP@nv&$5g(|1b_NJ#T-yoCcfPi3S=VxL~Xy z#f(2SS7NEt7Zz+&v*>g$^r$|nD8SS6(iA{tLR@uitxO-DB)>a- zShJlK9)hu@_nkZDfJeLg{k4*oi;5c%9;2bzc^YxTf0Xd`*sUVrZr9=8#s?hBB>E9+{}ub zhe37G2!gZXy9Trwm0L8WJ0g-}hdRh}E()&iD6RsMLHQu`5*UY(2U8r&4>7Cl)g>8j z-l~spKGtThHFGHzZ_9VD!E7Yc%(w;g2@ia}Ni9zS57iTwj(4wnC?l+T@yVJj_b#rO zIzV1snjtx0?mzp8-ZC<|o6Otn5D~NfE%~tyMVQjBr50RP<~dqZs@9AhS`AnY_`3%D z6{z~V-rKGHUvJD&L2degb;yXSsDjy4qfxNM!vg3Q6l74#$X#>XFq^UCR*VlL4ZtY^ zc^i{KJ)>YAMT!rETtiwecxJI8K_x)#g_w+6XAG5-5evX{a4WuPU}#db$|NE%oi?}! zKw*x1!=H}tgECShTOHsNoWA1~4?~cc+&x^@>?)S^;Gohnb9C|{gH!W7K{1 zrKGf`?a*q#YQWz$V8N-sU%_VDKMACYrtk18SnFOd$&m2+KxIAk-n<6jH3zW(3|hT> zYJcy=cKQGs*y~E;=PFHEP8-{t7SheI>?uJhw?)`9`vdI18ys^T1omQIH0E09s z%raw!ktuI4-=!w+1VC*~yw8lnH?E&*9;!{pP0J~R$W6;>sO6bzQ{CI0TMbwZw4Vk% zKBD$JX1gBg+gqNVZDRo(m9v&9-HQ5eWkwOo)dVB88RU$O8R!6%MkIBU$1WbP0_58D z!uk2Z8dy=0&h3jVPRp04zDrg0F!lPx8?gXD3|=L1sukZfum^5+3WI#T1A?O!J;iQ$ z`^TNW?W}{Y6>NY}l`Z8WLuGR%{I$wz-pky8tC*uwZ$6g#2e6koK;c*D^q~V z>M>KuP0P{|{bLn_Jj)-g+F8_zHPX7LW`a|1aMaY1Xx$@uCOZ*Y=(;7jCM~gFtlxuL zs18?>UT%l`Yk)D-g=OG*f&_sYA=U`7AX7q2c#xX^`m&uW&UgPk_{W@JaKUNuPUSb< z|9Txev>LD)up0134ftl>@y9c>ZQf4jEkn@>yQI8Ki<*SX^ z@}ny)dMbEs4mF`;zlL#6u~>kUMkLAWOLxeC_(&;3ozrZ%MM>z$7&n&Xq5KJK z@zX5Fk-kfO9-3BEU*ktZL9xEK$^lqm7$;EWR24GTcufdkLy-7m<-i)qF7c3|2D;qf z1gs?Y<&Z)kODN~h`V3}|(j_&>;x{>`EKb#$u|ul?s{w!4fagu;?`LA$?~?|uoH_s$ zQjw^`SOowCYaO!qnQ&2xkGPfsSXiyJTmxpJRC8?odhe4{;sG!SM`1-ZY5`SL*Gma# zz`BE&c6d^ZUfm9x&;UDGT4;r)uyhoOP-9&sq{^ylWGHB!s8fFxGyarTR1=(9iYb4+ zsvRoPbT#J@bcCZ}C&ten- zk`G>4&c6XApZrfYpH2Kxfc^eVeuTD*j?==^g$b3$O?o}uM3y#=t z7wP$BijNi{rgG=$!{y`EyX7rVAQ6}nc#P~7qsZR5p-oovs4o+7((j`#Y;|?Dr0=Mg z)NfN|{uWP%YJFBQI$Yh6BvmLmr2%p@0O@52ZDEoiSt*#9#%tn_VsfE+CrTL+BK~-4 zw#`-pRs&W8ZVgy)>Q+VPAGu}vkS0%(3D86T@R=%iI-*y%CfBnIJQ}$7>`_WzoI@A$ zZ{AuZIt=ojGe)2qloK{VFw~g51-hg8TRotP)KYZk6P)hD1-bjZb>Q)IwzVZrJsMilod1Uv-z zFD*)!|9rn)(Rit|fG+>^;d=Si$wTE_l-X=bxcsIQozBB=-(Dk?h%1bQDQsoA!#`Vt zK;gKE2nj-r>h=HqQSBzSu2OzGsHr`6jrr6&tE82dmdeYY?vag1C?o3ug&F&QK6|uG zAJKHFUeZwctC(5*^n#dlQ`W`ruUFN!+iJjSU_T91%?`c8qSgJ>(sgH$G2TI_He1Tq zyWo?xS@P`sG{vs3h}4lqPPTZ`Bs@50lO%QvM{)RsMt7%7tgfu`nPZbx_8Jif3%S<; z`ZCCMG6-qS?elk)HM*~^IP;q=`SR~g^%4|Ppn3|_0rmFx&o;;&1O(r3T<3%GCJJS? zR5kvzn9KA$JG2_G8n7DhM-5nT>W^2gjoPd~Zdt_@$|QK8!(1}CKiK8FcCsC;S1_BV zgQzr6x`De$r;`T&oW{WvO)js0FWlY$TsEcCbB^q-7BPE`#HbY@sHG2g-sT*sM7GtP zr;m^U36b)B<&LIy(WT}yHUF`B+d*s^AW5L7$Z-W&M1}-Q7+CpVUbI7wMHb+w-kmFE z#Z2XATu1}+sUMq#yVT8VPu6O{YM@ySSa8~`irRFAnbE^g(k4Y_&NyjSn;xj8JJ&u$ z0;ClgMZy&5^(cE33lQf0!M^}0=3&zHfa?F2X2`dja+GQbCf8>ctMXjM72t?+S+p1k zxdWL~@c>X;viG7YLaq2hkh*kxf!y`p+6I@60f@_X6v)l5t&kV4n$%!fSDqj(MF~>% zqhW<+V5S!8i&;_h27Ha{p)jB=0Z>)TX)bc_E_*K@_s?t)|9+M*dQG%)~3&D=I9@CPyYFCl6a@GSB zk${szYjn4WU7A1aZ?`ix^G2xI2Y{4Kf7?WW5t zCwZ^N{0%Ip8k}Nb&nS2(I++9+ZhUVSziCzNWT$OU$7;Z8po?k1f>S@a;J-Ry_)F~> z3(zCR^I>QG&DiizVWpZ-_zs!yL#yFGKHH>fr22^p+oYMLRkd>KTdQS3YQAzOYrv!r zRt*>b-pX3oQ1{6dQwQkT2OO3loT47vk(`!*ga0VC>?J^0>#`gcR)il~fd-}yN&t8d zl(L$A?$8Pj~*S=6EurCj$1=b3y|dL28o8n7C$8t_LAc;0mW*m~RM zR@FefVMPrDIHC)&1|PpFSOW=_Sjc=!c8P4v>e_7nWQyMc3u;fW9#Chra+*?HUZr%$ z`kJSVN-`HUIo=ngqzFnY;Ibt!)m%s#doWq5S2u7@c^#|C9d_~seY!`;m6Q9bj?!Lq z7PM4=Bz13PohpqsIi+#-1ih|54ov{6tf;Q|27+;WZ}{UCwQaTX{GyS`ozMDGVizoJ$SwjLP+$cmJ z9TqA#oiJE_k7^0torb3N<4^N-0wdeZXSAxuv8?NuXn#5v+g7Uqs{yNlrZmueU|dsM z+W8$*19OKT@ruYH^8%Q$s060WQrE&*O9EAX{JSF#uq#UuGWMCUvJ0`g<;c+2LYgT8 z%-Z?@&~_g3sui<+u=s<+d_+h+`w?I@3wiZVznD@9>Pp#&XCav`|ep6h^9lJoZi!vSLg30QBRF-%f=M5&!d z8HrB^Its1W&s;G<-aswXN9S&lT6PYAQEx%XsRgNd@|(9;qkK}igl$Ndntd6n^ALd= zsjQQ#B?WCrN83!HHJo7@OfF&#P%S47?8B4{U*t`ObIO!js!C>z?IZ7PD3Mheg)*v7j9hojKpEIGT3wP5SacaKK%wRP zW3VqgJAWIlU$sn5ibv?dK$$QgPA&J>AG6Ns!iCxLkF`OPhs&eq`|El6(r(jo%Ghwm zpi->m)EDj9b*u)g2K+?>7M%Lam1^~NzWS@wpBy+AfF)1My{vF-Mnq_!Jn++Ta_o@K zml@rhnn||*r;pY*SgiuttOE?u zShNl6g1hw^0Nd*USUZcbEG(s`!9vkl&dvKg%ve;0;pomy80g5XSPfVWSPgU*4OpPs zS+3+kp2v?M=Kr$`c$ERf&~@6F6vJ)$*_u6)iug!MS1NaU1o%(ixPGcS?=o0azk&tz zUvpC(OEWB|j5&=1@8%_w`^gnk`b$!5xLUU*KbK)UQ^=U0!D(uCDav_Pf#RzQ*N4pl(nLE(hF`+%W9yjYM^R%=p7cJcGauzI`2UUl)P)^ z2sJf-Yp%Tc!%iu#aK3t`YWQU=0Dsb|olbyXSE_T$bB_)WR&4rL|7V$^G9WV4SxihD zivVftCU8&P^T9f|+WvLXk!m~lj24X0jIHGtFpG}v(@m~@`A1bffgDufI9?^-Su9Fc z#iBYuF(?I|_;#x#ldQHBH9j}ypiM!F7_$~_C1)5>Y79_nPF1ClE2}e@!uLAyr%F`` zosgYh-vb=dy#Co*$E42bEvTGPj93#2P2_XaN>$HgUD+-fi09hh<2lrc*&nL`tAQ@5 z0SivMg41Ri5953HDyPv2CN4BO#>FF z`r8$2gZ7ei^IrJ+zel<_Q@yp{6gUW0I9hKAf~rxXr%qYs+Mo#?ewPbiVP;Lz6U2ifilCSs27PROtlIiIsRiIPCJobr4bZlW0?7!a<*O!nehKfWYi{JEpXifc$p-NQDGp>1iz7%-9;5LB+=nB((_!y)1%*l%m0vT1bFSWgHE!hq!!jC z)e`0A-wIIWr1bnl9fq)z5FX;h8oJD&VaSlG63?*-I-OPlya0qI#)5_cfaRetH^~JP z`^kS7??8;Iha8lkCKp*(5pePMij9yj)_LL@8*6V|RESI&*s<$|npRZP^4!?@bcT7( zt-!19gxxUogu>^Tf1htIk{MfaRk(>-V1KLztOl$G{7?fHsQTeWYwadGq>2vAF=HlU z!h-;QJfLhBfNEEju}D;+^@?_^C*{HKx2@a}TrVo(^zSyK!mKwBkSW>jATl>K(PaI<&+{CQi%2ap)VlAV3 z!DZaR!D(rYM+aujUVRRoZJ3%vOTBZ=^_Ay2@;QwN36RRV0Qq?JZVOI3+l9C1V>Qsl zG+@DL7rWq%o$lbCD0zf$JV6kXdk9d8hoWIc)s`7<*OguTlLyAj!~yZL1Vo}0Mr~8# z1jYc4>=h#?k4$Pzy$3yyV2d%YNZtn6L}@RVm5d264n9Gu6h~bsrJ%G1M4$mGzLX6A zH=a0Hl91J}6KJ|29D!U|L9oD5nB=t$RR~^Kuw8Lg4Ty`9Yo?{hDWj4V*goinx8sJz zuJ}{xp)2rAIW=r?vbX`r&Y(rz6PO4^7)UBed#zXvSPfVW__GEqIQ8eN*Gg?aerQkW z7Z)iz^2-5kLIGY7r^xRfeu|Oxt%WZBLzUSTPu?fuAF%-cTbdyoK^vuo;GKk`ED^=z zq8L~iNlZ#1TwvVik^N%jujho8sc3+m3oa(QWSI<7BbXkEfGQ?KCsC_E?D3N1nVSwf6-RIEyH??)SCQCflAd)7#G zPq;SiumhdNPwOwkB09S`^>ZD7T4{uuINlG^ijh*x6F=VB^PX^n*{wsou z{mb<^vV2DY;&Ow8)>Khh^RQR}tdggcC7kG1U$wIcz|{ln54CwZo{~`H$iCg()>2vw z;mMb{$WR%a80Ai!VG<479fzc^jTD=<9JY=S+N?h z8n7DhXAM{@sy|=7R%kncC~Io=?H(adU3!!%7xnaasmiT<)zlO@`-o(@@zoWw0oFvv zz1Ru|?ZCS@b6hXgx@;s7J@VBS`C)qjtfSS41Fe?b#Z{nf3WjslK?UIetM35BnLzH= z8AD}8Yq|Oj@G-J?jQjw$1xj~V#*Q2irvNhnCf&QVuF|qv1gfiE-NWUaBYR6a!UJ^d zDz{^p0EOJCOpJ;Ipdp~;cC?)GUWf(iEe9LGyDKu~LL^unZYe6S2A`{A8TM{V87$Z( zF80_dq0mej!sVRFHQ)}c8w_ix9o2CN3G2HH~to%fC2 zo?7(dW6|2D>zwM-pnEhZXrFw#W;d$V?1qcjaqVZL1}ec10gV>Y9x)-O)#di8BR}c@ zu>jupN*OR?8pG9ElpE4zShpT9ae61XY5xe6cQDk0M?; zKvfVT&}1AeD@9Q_=oc3u51ckpVg=+OvchKf6`QigT*1swT@YM zz}#1RjWbSm9+~LQVL@)DI|D5@2N!+ z1vjvkk;wFZXx2DA*(rvvEZG5I%DCKcDI-7(KrJSqo+Oa;Z%1O0meIXq54xPG-p7H4 zr;SRIPmqyKp}U`91eB{MCCQ{=j)jw*h`)ZkUUCtdK{@+~-m1;)fUMrtkQcMc1*{2b zc}aOaYcY?&l%nHXnST?C&WFk9pjJ2xKUW-`BIk~Gtb&EG6u$N2PWgIME|Q|cU91)lDZ3 zfpx~AB65;7)FQwAA&@h~9FKCtd!1krbDLTJ33|E7IBDT#YV%R-`IJ zveaoqk~&0k6gvd``*w4#{Qc7nl3Q8@%W0i#!F74;JCECYaL;IcX^b=OJfrL={`$3* zvQqIDo9BhfVt7oqW|zvAnhJG2qx1I&V^Aj`48)4nfYpH2fM05$YIf)y7NPp(rEB{} zkLuk`9fU0J-=GM*mbIBB@;4xb+YoGd*0|n!_Td?x_uspI*@m$I3^t`doaybAAluYF zbY_nc>1lH#GmpaCBG$psYJyQFiAa(X1=7oBW(}3( z@al5+IvgR|W()((kT;(+SnhatjpVpYuzJO;;tFt4h04#48>D9)_93Yi{`}E8rF;6Y zD{Cr+*+FInh0e}5_0pnr*@gh)A;^wP1v0Isy&DoA#Z0$AWI9I3&ql`}Rwu~TJU#4p ztZjhRfYrdEYQO^3L)Bm>-u}}yS@P7kTh+%;pJoWmcFeB&$LE`rAAAZ^+x=W`Sq)eXbbbwV-Zy&ZzjkeUhWsGDxNy7j8`7u0r^LyaV&EdViE>d1oy$7|IMs@L zWbS5p3-Ju9?kiKJ4G_R}^5J84W$`OogInKRr8YeVpy2m2N9f(O+K_rC7%|iF6B_{^ z1g8DryWbAN(I1WWi-A^41+l57!0V^?C$+8?*MxRse5W zhfnALoJEYo2t&h|*+p>5ss$|RWd_NK=n!g>R~;12 zKw`$(FdU}>u@6NRwE(A{Qed6s{Ox&I4Ok6yJq>hdaJq2eLV5l5*CjhUTe&eWyX-PK z`skw%`YL5+X3DFtzN)sr^wLYEUq2_y~+u&!5;=m0;rXwsQ3| zLn{_mGG+;Bp%t|;8G*7^1gdn||Lv2F$gFa*r@Ys_@0?Nc+qc)suA*{S!#z|1dOakD z3G1QKB0C@(*F z!)5N=x$@LgPsu|MJtV*R&2J7$_rLt*FXh7zKUCWi@LHhSv>ItGB>+%R2q-lenKw0v zd1EnpT3CUbR4O?6ZKM+zZsu>vQ`th1uu3V=qY;rCqBqpl2P-p$GzjM()wkh-R-R%C zzc1i)Fxo<)RkRqHHRNH^LbtuHm-^tGEwURHaRTZxSl)=hnK8wV2(7jK07zLUmEjAU z&=L8=2kTVE7_FC#`82cRdIJPLcf|yKn1dRA4q{eKQP9*5$S0LiR3igmDbAxM+aQz$*Z84fMHwlYte$tYe*gJg@yHmp*9)j_DR8%xI~?wW%_!ip=PDafQl{s6G&n`AcolkpoSZ`zf?zXF zmdRP^sJ+X0-hr+dSk{)^hqkdymsx~fADqxSHFvN48X*@iTsct)nme0Zb@TvKj>t#Y z2FPATxom2GsRC#rA*jhr zPXLh*fVvy;fY-hJqx}77cUs0M0xwNCm4zZ`7FVYxYN$;GW@@j2hsai zj-ltyA5Jmh;dcGf*XCjhl*Hs4qOk8R%h0O>Y6|s&S1uVv=a1>m)gw45j_)J9-!q4H zq1P`RMLqZ>LQD`41O$OVL7?ToF0FtBJg1#Hb)rd=CfS{R%9JV8p+g7dS+(2H$ij8k zT}N%(wxx$2dWi12>#j!jmdzT03%KTG2AAmhX59hhDJ_L^JsZ4h7+ySye;7|5maM#| zSZ@^Y#IiRGGCW!qs-f#6rqAeYLqZU;#W9=;$sP`1P=~AGZ(a;%p-`Q{kKt8h*cY+4 zb<_T$hAz|OZ9SP}+-pg?n#aWdOIBXjhPL#G4Wp|jcBLNOY*Js?`e8BMc{Nv zOoV21;22izjb)vFwO=`hYG{L(5`BZ(6z(lNshe2|;42PefLA<(z32!Lh~+8`ZQ0}c zxtcdi47( z>RRXpH&q$#Sp|Dpqr<~!>8@PW2VNC0_STX#+QXh@Z3r!4M>+LOh*B@~ul=@LZK^nO zn64V#N$)08K|l}?1pF2OpChLS4;~~8=O1&-F-ptNh-^4}PWt!nPk;XTC*|k!P-Vx) z<&9>~o=tP+%u$3E1Xav61mJ~&cjoVtd(&u^k$PkCZbAZ)E<1!cR1l}%cjnTQT#9O> z8;7}FYoYVK9%N4+hUm3{l!1t${FO0@X?XVz^!Qn}5?Vt!u?A&}K3Ja49%C~JS1v>_ zASR}rxvbQo;xaYF922Jh*)WmC_<1YX$1C^JD=hDY;d*UImil&gIaNj+peN2ZlT(n% zf6v>-wP_D3+NlYfJ_d29I_n$j%u&VWSW~qRHy`K4M_W3RQ&ea`6#$TUcv(@c0N&Ur zcwO0*{5eZgT{NZ#$0ggmvikU@PCr<&hl%N7E|(Rhvh+*1VixWdLQHmL714E{t|5e+ zpx&-Fv`s&YHB4ktupXXp+&d)mxuaeBTHc-}aP86|L$}6u_v8%4$nd0JNm562B zA6l4cX=zkgSZG|`D)YEjMn(pKjC;%eZ+<4no?3_zGoH+%N<~(6dq7l>MO{)}Om}^; zhMqXF8+D9!f97yeQ4y7vmeS6hJN2a-oZdXPy&_Q{ZJV>o=&Q~7l*{c*;UDzV2gOqF zj%H0!42?RT^SETriyqmYo?K)ucpt)4E`o_zG!wGuF!mfGp@@BZ(P1X~IET2nt2q{+ zqJqmC@im6C4yofg8*Ho#8?%m7(QAwLlZhRjQQVed%Q4IuDKgYVKXc8`F-tel(9Y4c z?m#L1ur7;-ze8A#h%>s8xMnbv#41(U-{zH7HN0F_&V<|SMMc(rWn~3>S#2b6*`^HI zpI5AKsHor=P>#<<3}}8y1;sIxNYBYBDy5Yhwo~VJE&DpbGxaq~e1-C~rmB)kSv>(# zQhX`Ko;Wj|<&+rzTgG*u)tROA`X6R*BAz?wqn7d~K01uzI7#S8c?I?0xXko4vscjo zgI~`MQN8ndkMYV`u|?&q-y?;e5agsBbJR49|a+El%5C4|^veb9h7ObzVG|TD4#5j+qYS+%3tqedL zvs-Stg=+Z67%{C)G2AGSQb)rB4bs%whMR|LRaMytLu+5t@%(=u5zF2v^V+?IRrJQH zDkW)in8|Ae$6dlYMu#c_gLuLD+w$o20p@#SKFU3Rai4JyUBw*{rKq^{*6PFb*8$Wl z<%AN(bHkxB+Lc>Qrw)jvvj%cIxo`SbZpo>j-}jicUQt0x(>r(!dvyq>w8NE@Sy*eX z3CiaOp~`2EUNjTO<9kKA+%{Jc`D+hif0=ZFaBDc~MKN_=Sht7sjL;=RQ(lR*6tCw5XaVtzaJVGU!UT83t=Fx{#)_&{9}7t#OUU&NE%AjOGNun;Ch?RogIvbLOZYh!5maN|*i6SQC!SnS=qw@K%Ib4fa_$0CTYeUs=^8cMOh$gW}zq{?vqBM#KFQe$_2r{){VtK@zq2D%&h5I=lAHw`0G3^>cTQ<3X;m!+#_GbGf7 z#rr4pO`xyWWHRxsp`4N$j?IlyB$nV>1(S#IY;;sK6|*O`UvfK|Hm>DKhZv@Bzhnt} zKx-(9<6x@`%rB?7YE(22*)uth4y~mLM-QVds?Fx_g=+TSRx!aw%gXqEL~#PxX-9Xb zn@$~MSNasq4i?+Id9#!_HQem|`}fn}!GrnVQQ)aM7`o}w?7n^bl&7#YzN1}gfRF72 zk7@7Tz3oax`&bh_)*N7g5ob?48;}ge3$!cdbL6yZS94sQ9_n}O1lFUZUvhG?<0iZ1 zmtTHa5!i(b7ZN_&ApQ?~_St6@d42G~2dR7a?s~IKogiR-mdp=SEGOD9+f#elGW$6W z(~uqRBvM@Sqxc3={nF`_FW;~g=%5YX3hNa{f2pYt;BjMuRM#`P}FCvn^> z-tv+0195s(5`yOz2~Cx7q4Le?+TQ%*iyG`8b%-540&P<^q#D5!AHw$sahcn*Y{OWe za9=MT+mk?KAs>dMuM>xKp+!5g2_9q^;!Y17X74r5%;(XDxovzd=(GOQO%TLsllO--eZ8#fYy5#!?GtT4m9x@F52rQYAz>oMGr5fKrDY$>B@ zU-oam{YGJ&^@4y^VXH=j>dp^lCI`j}Hyg58Fb@WSOdd>YI9hUecQf0od$ecQ?BjA+ zU}|8HFeu%F>0lbq^&q2}VM2ftVv@$1nYxw6l4_7YV2wPCy*40R)?oI}Z8o4aeC zPGYwv+Mma<1dt3sJHfcY<`8rl&d=?!0~6Kdrw>R}by_2N9khtRskj&%9E5@`XG1EhtAvJMjnDcq7- zoZb#s-f6?lF7U?B*1CJBqUB7UYmr{WW5GI*wvvf83~ot~HxL|Wmcg|`Vc6%ht0y=m zu#U$;%gLRFn1nl+0RDU47RuoWOe8%)J1dXVhN@Xs;NszlR@W~i2_Rhi(egdkw12o- z3z>|=MS4Gvd-r9HCT&v(bfP=?+@G_%8P=1l4WQ;}Qfvg*J!SQGu37*?5qM2t)e6g> zi^ilVfz+S41Qdi)r?`QncH;iN$^xHPy#FT^9k2zO4or??anxaynrJ4}a3#YM*_C<) z%Py3cm($j*Tj}Vd`&wFOo+P5)l9(VM2nYg#fZrnEljIc2`tQ8+j;d#hbp3JT#?i86 z%anjAQu&ejWVFX0f1F-<jT6~3Y1CCRLZm&X&@Af6B*-K7>CPvY zHV_K{74}X{xZYWs#@@pmHJ0BuF_y-4ETi*B+6P5nKeaFQ>C}!8WV*KGuNyG*9J=e!#I)BSS`Xj^ap@t4uONlB( zXjKp}pS#GQDTQlAvB2pUtIY8i?Kq_$!l|%SCf>m32nXs$<(d`i)du+^2nYg#fFKYM z2>2vXMX>4j-+xc%pMO4`e){QFpy4IOkbPpJd3e*hL1tPF0R(vJAV#ROk4ovlwLW($ z?IB$BI1V6$SR(>6li8cvnMtBI4c^adn3Vl{?q)h{5Jm;r%3=>81U?b4=*-|X+-p%q zM=eut7{U);UbxG8=}0c|l)xkv`cN2N|MOrGZTUHaGeELv02iOv_XY>R8yVj&oFbT@ zNAPhUashXIjAEJy(?H0Bt~`Kn4QOk@^g7I&*IxoyUOvDI_(n6T-ha-SMZfLHQJeoZA%)K8 z@>AZnQ;$kkMCJ`cc_p+7?SAC6A#_wdv9dXwqF!PlL@5c*bc9$_O zr_~m)G(}q#myOuWXegr^Y%HhkI~u&-KU(V1f}H9;Vn4*J9^#_zP}jnKFasA=%OJ?` zu)?+nm?4QQ zo5`vQjbN6_A^>KxY6(V7XdBL{{br(C1Sx?Q+ICJdL$aCCkgVlstM?WtPdI9}8q@g2 zi|UA!EDHjHfFKZ92-N+x4y^u3Z(4`|@`(`p=V<*od1^fqISes7+wQo(rr9HudQv}`0jUHzb@ZGf6MtOq1bXALQS|EK-Ab_^ zcQ!gAjQ6imN&w1@YUyWv>IY4M0f|lcz%e_2%ORGA+G{1uh=)ZSEks=td5Xk0*r7r* zImSE+(u)ZKf`A|(2m~$yLQVts-nMGbpqQ@h$Bl>R?2+A+ER$a63saq)uG<2Soi<2i z!Q%^W>8-2{x+j-wi*l(bWlWF|>xd*G9UrNW^ONcLc4*2qDn<2atx@-B4+{? z#v~SURImQFiw1X%qoW-Xm(;<2_6NfIFzDB!zNogNd&U#ws6blbVV%$?L4`=H=J19! z{E6aPs?)d_!0$V9m2v|JV>Oe>`sBLwc^jLq4Fw|D0%RV^`8 zfxvzD|)juiw1 z0YN|z@GS&{och-N@cGvK$_Yk@L&flK5qnIL*n)~pAb8NHe2<4|PvqD_WWIabpxx?d zX*o7>x;MX64OJs!ABE>F+?2w(cs(o}#Ei*UAefe-0_ih)lt=yV%!hO=0JLFYC}uyl zS6!(pjl~@ewvURS{e0hG@ZM^rh3e{R zI?5JxqPEK)K|l}?1Ox&9M?i?G|KCH8_F&oWJeA#ztZ-;#nu#jPaT3LGY$wWduHw4t zC$neHn+9*;o_t&Escs9%4m37l`78`y#W9x1`9j>OHeJ3H(x95W>23Zx)!FwL;x~Gi z+j2q_EP-D%@Tt$bc8FqfN-Bm`lgj$`b!9!S#ZFXZI_VAzNJ@(WVK#G78rrP`{h6Mt zo-b_2JH%;6b*1A5C8;V>OPA(iwU{6v2nYg#Kp-JdF)QM3VW@$0M?KJ&0%moZp&+o) zQomiLVk7$qPRJrlhKmQ9Q|~32{q*D}EFyI_2wF=uF{B{$^8uXK%rI>pf0obeS17G=qhf-tO61Xa6F8|q`}fEiqP)us#3A;z3xhGUEY zqz#>;p>5L!B+=CVNmR&{0zg(_Axu*9kiH*cb8Cl&gi@@bbQWHQ@FMZ%lHDxHVAG*$ z=vuN(5D)|e0YSjO5D=p3U-!%Ftv#N}UpN77` zA(;Jg<|~7I!a$~f-jYRk%wA7FZ_Z*e+l4L|(^Gj=&3h76a*V*po*n3g^GDcuSM_#( zoFXOzR>1+EMlG(GNb~x}`V?>RqGCU!*tgV}U(&zM97-3DOHtlxoDX*>iJza_kMD0i zJ&33=1_Qyu$Yett$MQ6rK9= zmV$Q@PSrBbR_PKEsl1cW4)O(qtbo;Q&N!7VRLBbbkAAm_mEoh5Ck{&X+NN-NJM@=+ z<#cn{;N&=;Al$lr&6#R1Du(Ed9W$8nZc z28+`wrpj%F_EmV8?cP9U0fsVHx%@JeEU(uJ5BVbq2m*pYAR!>+G?4D5hx&5WF}*38 zJ#ll^?x%u1@S;Iso#^<%ofXm3>D6dXI15+~;URTz*uDaDnhl($30#&ayU08Y912Fw zWs;B-#Zjg-uxmVR<|6P&{f9>nVG8Rlx<+U>>TLSEX`Lzb1LeQ8xiZ9$eLFE+TeXhj4((lVp8Efg)UVId+4=kc>>-=NlWOgD!pB% zf`A|(2nYiHkAM(W|G$SG>%q6{GU>Cw_NnVZ7*lyEK_#B zUQmQVXxW0sx*OltM%MN5jU7|p_VKA5x$1*C$?4^VyXY4Vxqu-qSc}!p+jx+W?1s9l z75stWB?6?ihzTwFGog1p?XeY&Q2WRqK|l}?1Ox&9M?lD_|KCH8_24L0xz}-wl{_R~ z3(ppR{_neu91qBO@9gDj-;PTU^-rYhr}a^V)j7AtYlgPLKx&ld)Y1~Qme+-Fl>ro_ zhqtw;%p4Ao6zlyYFH1AsI?<)1a8m6>ZR=v}DULD5pB^?Om0?>;EW=QTws%5X>#4eN zH2_6U`I+K(2xW*N&Sez|2vl7^y&v_6kD@)+BVKxRBX z+8`i$-<%D+$V`t790E%4i?(G`erW~0c)`)+>JYLzft&vz6I>aS8WeWNl7;8+jCrmt zdsJhTc&oY2W)jys4Mfu~73f<*iFqg;Wtm(xBqV9m$Lr;$Py39&bqUylGeX7iQ^UP0NXTDcgOz5p@>mE507*!RTj-?zS2nYg#Kwu-_`Y%;r;~>3j z1_Gb|wU>V1m21ta#wid#*oi%n>-QDXYm0Z&T_+4^#+lu72((Pi#+s*yb7bNQ^&*pn zq+(lJ*4YfjZkAm!#2<*J6Q$6%5*6ZrzPp~#2BM043l)UJZph--7-w(|T3FA77`pm{ zRcgMJV}kb|HpdaG71lo?l#x@)@=|HEct;MIOG)W_Y~4v_1OY)n5D*0X904JxetrYJ z)qL=~uUH5lbwS&*7c$F|gjC6%Rj4~5Bm1X~hvYGC9DVFau>e{+%h#&F6I@};N zu^nAMwU4^|p1e|e>Zh%=Gt1@~O=ufQ7mn?rwxiTkEPGZfDk>=Ea87+qmi2;wARq_` z0)C2skW)Xsao%orF?%9&xr9^{lTZ}22YEr3wJM;_#8p+8Vj^>}u#EaJ@$j_evdFv6 zHU|kC!nI8oSR|vMK%j{$$dQqp2CnC{u9p-q5KS?}pn{yXG&xFNQ+rLZoXWAMeUsWL z;`{VZ+bFwODGl8I^a)$*!~#@uvQ%nfTRLZCcYP{L&C_)2vdjnqf`A|(2m}TKP5Y$^ zj6O>*+<*Y2eo#EUT2+_gBI=w~jjVnQ0i!Aq`zfgn6WDeasfH1LwJpM+-5RJwHPWIXYP=Us@uK25rH7hli)A&QtFie*>}7xI$8C1IrRCbZ zq!_Z_m2-sF?knIBf@+F`$JP)^famLlUw5dN9_aCk2?BzEARq_?E&^W6u?}2NgP~`T zCmP2xL8Xf9naC*k4hf(_Uf-X+Vn}B3h9$;EGCekv(}{gL(SI)*N%L-=tg1$}i{^JS zOAZwzi$S*X%WPVzt~M9f2dUJb)K2Yak0FD9Bo{_V z;TTj5`y+YHnApW(uY~(qS#9F5l1x`GDf2@k6|*AlZrV?jzxGtx-6{xN zGbxo;rx(!vf-;poqf_Pcd5k?FHiD)MNTj8lL8UF3Ygl?}%pc18+Aya7{_+g92sRqa z**>#Z?KRiWGKxn%XZ9jB;xaK*%4?9#5~ z%(*n0vt~cbD_HIk--GnLBQ$eJ7Zo$okpoVvt=Vm)T=U4zisZK-eUHOpVWR!6h(x-pyRm4NL z^`xO)=!WTi^+a3jq}3Ejb)|u zbw54Un>lMTRVEaO#FdkJ)9Q?T zTEhgRyvnS4U(6+%Vk4?)XlepIdfE^I5z)oeJ<-Tg4v5t@v}+ufDe9*e)$}daDti0} z&ctCWwkY*?iHlaGRjmxLKSs5F-8EDyXvi?;{ed9 zsehk2RILI=ul=^07FhJex^Qsu-zKC`a{Fkt-4%VVb^8ky!E7HLsY;OH#RjIOAb12{ zy&N1Wtf1l$uCBmDRSU0X@w0?L)bmH1wWiERHtlb`>3PomcdQZ6v~$Sbf`A|(2nYh6 zML>wEXYmf0t6eiW)!G+?4PNu%Di%i#RYSKJ-UkUu=DL-koR;+Ux=b3=yCWStFv)sM zYtJB+Uz;;;VP7XYSb>_x4;-Au*v2%X){l%hWH&*V>#i@?)2clM8dBQGr1R;Yw$nZQ z0y2|BH(arDelAcg0fvk^ZSbPPIB|`V<6`)HUFkCp55aQ;sfZ%hx$46&7LS}Zn2B*< zR#VVe$W#yz1Ox#=fWy251VNys2y8!8L|YCNsiN)hdVy5H%9tR+^L2!W+ke=2z@y|8 z+EUt_)m=rwcgPRuwqoBd0Ze?H#fCb=MPp{1J~?kIZD2CmUTHs>NhRWRku3G(k6Wo1 z#~}~w5^FrfI{)na9oEuQ**t^~i3eZ~O)?nsQPw4^p&Sk$z&%5ZX(A8zUpl@geYtv{ z5=Ncew-Zg^&(?#*>Vvpvqx^z8k^B(^1OY)n5C|9qgq#MiygMy|8Em95uiWSVO;4)G&N3S=3U#KY7mTciz%Kw5PG#5IrzkW~3{b|!7bt)uZp@bCn|8+~2_4HF&9imn5%}@Ut%GG%#7Ql+Q%m@O4 zfFR&K1ca!15Bq?*GRP^sVIVIcL^^&DNiHyyo9Yl9q2@jH$LK{h64m#XWpLKMStZ`F zcIWclxwMr%!mw@~qUnvxMtQ2at$rO%7L6BjMX>-_RddyYD1Hg4WUr~VTEum*;U#2q zrsd5k)4X(%POYDcpVHKsx29WVOHi5d;JQK|l}) zUIc`k2JgLhThEW`-hs2QBe@o46?@jgnXrWLu&s$nX{{Q{g-j4UU!F9h7Zn-f+WvFl z2sH}{s_Q>lON+N>>rFJNUnhFtl)=gqY+V^3Gm%b`qFfo`M+_?7JU~Um_`a&Q5MHRV zkW&+{QtKPQ>5ue$dhol=DkK1PR#g(4ncI?y&vP^@^KZ7lKC)+fE}q`at=DYq zWIDbP`x!l_SVpbWTmH9>HBoKJK3G4WhmjAmGQmqsU_0=Tb|4u_X5ph13(&X5lB2NC z5SHh{{Y6+rER%esG2zZu@%;tAtmc=HvAr6WeR^flE-Gg)I?kbL&svBnJnSI1e`OS? zPys%jvP;JGq>IM()QdhmtsZ*}gCVg1K75O$X@YRB*)QQ^5W+Plj$IPvGO zigPSv&#$%wj+I0*&g)$9=-!i@$5!fn{c#FUYPBT}7wrcDZOou-aL%ai^zn*4D$8Dn zZy>uK$xVpoJ(suFXNXlzl6wh;bDY1Ox#= zKoAIC1nhscgBJ(sc{33}GSuUoM(Z=iXu9iWg!K|l}?1Ox#W5fF0f zB2coMa|A-!+o`3Qy0jyknft~654Vf(RUqL-O z#?hHacc(!Xd7}1Quiaf%Uvd-@qEIyoNn9cPpBnb&dqeVfdch-as

-r(w6hglC5Ho0#iCq@;j4+ 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 literal 0 HcmV?d00001 diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py index 609375fbba..cb9aa1e312 100644 --- a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py @@ -35,8 +35,10 @@ class StarkP1SpectAnalysis(BaseAnalysis): lossy TLS notches, and hence this analysis doesn't provide any generic mathematical model to fit the measurement data. A developer may subclass this to conduct own analysis. + The :meth:`StarkP1SpectAnalysis._run_spect_analysis` is a hook method where + you can define a custom analysis protocol. - This analysis just visualizes the measured P1 values against Stark tone amplitudes. + By default, this analysis just visualizes the measured P1 values against Stark tone amplitudes. The tone amplitudes can be converted into the amount of Stark shift when the calibrated coefficients are provided in the analysis option, or the calibration experiment results are available in the result database. From 6ba5a268f0095efe478b32dfd6e06ae0132185e2 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Jan 2024 17:05:52 +0900 Subject: [PATCH 12/15] Update file name --- qiskit_experiments/library/driven_freq_tuning/__init__.py | 2 +- .../driven_freq_tuning/{coefficient.py => coefficients.py} | 0 qiskit_experiments/library/driven_freq_tuning/p1_spect.py | 2 +- .../library/driven_freq_tuning/p1_spect_analysis.py | 2 +- .../library/driven_freq_tuning/ramsey_amp_scan_analysis.py | 2 +- test/library/driven_freq_tuning/test_coeffs.py | 2 +- test/library/driven_freq_tuning/test_stark_p1_spect.py | 2 +- test/library/driven_freq_tuning/test_stark_ramsey_xy.py | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename qiskit_experiments/library/driven_freq_tuning/{coefficient.py => coefficients.py} (100%) diff --git a/qiskit_experiments/library/driven_freq_tuning/__init__.py b/qiskit_experiments/library/driven_freq_tuning/__init__.py index f59a353f36..0bef002700 100644 --- a/qiskit_experiments/library/driven_freq_tuning/__init__.py +++ b/qiskit_experiments/library/driven_freq_tuning/__init__.py @@ -56,7 +56,7 @@ from .ramsey_amp_scan import StarkRamseyXYAmpScan from .p1_spect import StarkP1Spectroscopy -from .coefficient import ( +from .coefficients import ( StarkCoefficients, retrieve_coefficients_from_backend, retrieve_coefficients_from_service, diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficient.py b/qiskit_experiments/library/driven_freq_tuning/coefficients.py similarity index 100% rename from qiskit_experiments/library/driven_freq_tuning/coefficient.py rename to qiskit_experiments/library/driven_freq_tuning/coefficients.py diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py index 3404a10f20..5ee1bbc18e 100644 --- a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py @@ -24,7 +24,7 @@ from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options from .p1_spect_analysis import StarkP1SpectAnalysis -from .coefficient import ( +from .coefficients import ( StarkCoefficients, retrieve_coefficients_from_backend, ) diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py index cb9aa1e312..857f440bb8 100644 --- a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py @@ -20,7 +20,7 @@ import qiskit_experiments.visualization as vis from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options -from .coefficient import ( +from .coefficients import ( StarkCoefficients, retrieve_coefficients_from_service, ) diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py index 3b92c4bfbc..9ced48b07a 100644 --- a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py @@ -22,7 +22,7 @@ import qiskit_experiments.curve_analysis as curve import qiskit_experiments.visualization as vis from qiskit_experiments.framework import ExperimentData, AnalysisResultData -from .coefficient import StarkCoefficients +from .coefficients import StarkCoefficients class StarkRamseyXYAmpScanAnalysis(curve.CurveAnalysis): diff --git a/test/library/driven_freq_tuning/test_coeffs.py b/test/library/driven_freq_tuning/test_coeffs.py index 954a243357..ce1cd9ee85 100644 --- a/test/library/driven_freq_tuning/test_coeffs.py +++ b/test/library/driven_freq_tuning/test_coeffs.py @@ -17,7 +17,7 @@ from ddt import ddt, named_data, data, unpack import numpy as np -from qiskit_experiments.library.driven_freq_tuning import coefficient as util +from qiskit_experiments.library.driven_freq_tuning import coefficients as util from qiskit_experiments.test import FakeService diff --git a/test/library/driven_freq_tuning/test_stark_p1_spect.py b/test/library/driven_freq_tuning/test_stark_p1_spect.py index 02751f1ba5..00ddbd5c6f 100644 --- a/test/library/driven_freq_tuning/test_stark_p1_spect.py +++ b/test/library/driven_freq_tuning/test_stark_p1_spect.py @@ -24,7 +24,7 @@ from qiskit_experiments.framework import ExperimentData, AnalysisResultData from qiskit_experiments.library import StarkP1Spectroscopy from qiskit_experiments.library.driven_freq_tuning.p1_spect_analysis import StarkP1SpectAnalysis -from qiskit_experiments.library.driven_freq_tuning.coefficient import StarkCoefficients +from qiskit_experiments.library.driven_freq_tuning.coefficients import StarkCoefficients from qiskit_experiments.test import FakeService diff --git a/test/library/driven_freq_tuning/test_stark_ramsey_xy.py b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py index 62bcb736cf..dd0fd59e4a 100644 --- a/test/library/driven_freq_tuning/test_stark_ramsey_xy.py +++ b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py @@ -24,7 +24,7 @@ from qiskit_experiments.library.driven_freq_tuning.ramsey_amp_scan_analysis import ( StarkRamseyXYAmpScanAnalysis, ) -from qiskit_experiments.library.driven_freq_tuning.coefficient import StarkCoefficients +from qiskit_experiments.library.driven_freq_tuning.coefficients import StarkCoefficients from qiskit_experiments.framework import ExperimentData From 5cf46a0c7e83158f9c9f83296e428f1317fbc9d3 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Jan 2024 17:10:43 +0900 Subject: [PATCH 13/15] Wording Co-authored-by: Will Shanks --- .../characterization/stark_experiment.rst | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/manuals/characterization/stark_experiment.rst b/docs/manuals/characterization/stark_experiment.rst index 1c8c723088..fa9e378a22 100644 --- a/docs/manuals/characterization/stark_experiment.rst +++ b/docs/manuals/characterization/stark_experiment.rst @@ -214,8 +214,8 @@ the height of this GaussianSquare flat-top. Workflow -------- -In this example, you'll learn how to measure a spectrum of qubit relaxation property -with fixed frequency transmons. +In this example, you'll learn how to measure a spectrum of qubit relaxation versus +frequency with fixed frequency transmons. As you already know, we give an offset to the qubit frequency with a Stark tone, and the workflow starts from characterizing the amount of the Stark shift against the Stark amplitude :math:`\bar{\Omega}` that you can experimentally control. @@ -232,8 +232,7 @@ You first need to run the :class:`.StarkRamseyXYAmpScan` experiment that scans : and estimates the amount of the resultant frequency shift. This experiment fits the frequency shift to a polynomial model which is a function of :math:`\bar{\Omega}`. You can obtain the :class:`.StarkCoefficients` object that contains -all polynomial coefficients to map and reverse-map the :math:`\bar{\Omega}` to corresponding frequency value. - +all polynomial coefficients to map and reverse-map the :math:`\bar{\Omega}` to a corresponding frequency value. This object may be necessary for the following spectroscopy experiment. Since Stark coefficients are stable for a relatively long time, @@ -274,12 +273,12 @@ The saved object can be retrieved either from the service or file, as follows. with open("coefficients.json", "r") as fp: coefficients = json.load(fp, cls=ExperimentDecoder) -Now you can measure the spectrum of qubit relaxation property. +Now you can measure the qubit relaxation spectrum. The :class:`.StarkP1Spectroscopy` experiment also scans :math:`\bar{\Omega}`, but instead of measuring the frequency shift, it measures the excited state population P1 after certain delay, :code:`t1_delay` in the experiment options, following the state population. You can scan the :math:`\bar{\Omega}` values either in the "frequency" or "amplitude" domain, -but the :code:`stark_coefficients` options must be set when you prefer the frequency sweep. +but the :code:`stark_coefficients` option must be set to perform the frequency sweep. .. jupyter-input:: @@ -298,7 +297,7 @@ but the :code:`stark_coefficients` options must be set when you prefer the frequ exp_data = exp.run().block_for_results() -You may find notches in the P1 spectrum, which may indicate the existence of TLS +You may find notches in the P1 spectrum, which may indicate the existence of TLS's in the vicinity of your qubit drive frequency. .. jupyter-input:: @@ -307,10 +306,10 @@ in the vicinity of your qubit drive frequency. .. image:: ./stark_experiment_example.png -Note that this experiment doesn't yield any analysis result because a landscape of P1 spectrum -is hardly predicted due to random occurrence of the TLS or frequency collision. -If you have own protocol to extract meaningful quantities from the data, -you can write a custom analysis class and give it to the experiment instance before execution. +Note that this experiment doesn't yield any analysis result because the landscape of a P1 spectrum +can not be predicted due to the random occurrences of the TLS and frequency collisions. +If you have your own protocol to extract meaningful quantities from the data, +you can write a custom analysis subclass and give it to the experiment instance before execution. See :class:`.StarkP1SpectAnalysis` for more details. This protocol can be parallelized among many qubits unless crosstalk matters. From aaee397ce70b12f72d8e6a27879bf21c7e801d7e Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Jan 2024 17:17:58 +0900 Subject: [PATCH 14/15] Set limit to number of retrieved entry --- qiskit_experiments/library/driven_freq_tuning/coefficients.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficients.py b/qiskit_experiments/library/driven_freq_tuning/coefficients.py index 27dd7af7c7..df529ec8c6 100644 --- a/qiskit_experiments/library/driven_freq_tuning/coefficients.py +++ b/qiskit_experiments/library/driven_freq_tuning/coefficients.py @@ -232,6 +232,9 @@ def retrieve_coefficients_from_service( backend_name=backend_name, sort_by=["creation_datetime:desc"], json_decoder=ExperimentDecoder, + # Returns the latest value only. IBM service returns 10 entries by default. + # This could contain old data from previous version, which might not be deserialized. + limit=1, ) except (IBMApiError, ValueError) as ex: raise RuntimeError( From 8ba6fe4b21ff72ca56797d2b59e7a4dedea58b83 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Jan 2024 17:18:44 +0900 Subject: [PATCH 15/15] Remove type cast --- docs/manuals/characterization/stark_experiment.rst | 2 +- qiskit_experiments/library/driven_freq_tuning/coefficients.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/manuals/characterization/stark_experiment.rst b/docs/manuals/characterization/stark_experiment.rst index fa9e378a22..23b6c56d4e 100644 --- a/docs/manuals/characterization/stark_experiment.rst +++ b/docs/manuals/characterization/stark_experiment.rst @@ -265,7 +265,7 @@ The saved object can be retrieved either from the service or file, as follows. # When you have access to Experiment service from qiskit_experiments.library.driven_freq_tuning import retrieve_coefficients_from_backend - coefficients = retrieve_coefficients_from_backend(backend, (0,)) + coefficients = retrieve_coefficients_from_backend(backend, 0) # Alternatively you can load from file from qiskit_experiments.framework import ExperimentDecoder diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficients.py b/qiskit_experiments/library/driven_freq_tuning/coefficients.py index df529ec8c6..89dd840ed2 100644 --- a/qiskit_experiments/library/driven_freq_tuning/coefficients.py +++ b/qiskit_experiments/library/driven_freq_tuning/coefficients.py @@ -224,8 +224,6 @@ def retrieve_coefficients_from_service( RuntimeError: When stark_coefficients entry doesn't exist in the service. """ try: - if isinstance(qubit, (list, tuple)) and len(qubit) == 1: - qubit = qubit[0] retrieved = service.analysis_results( device_components=[f"Q{qubit}"], result_type="stark_coefficients",