diff --git a/cirq-core/cirq/experiments/__init__.py b/cirq-core/cirq/experiments/__init__.py index 71f577d4898..96310ca63f7 100644 --- a/cirq-core/cirq/experiments/__init__.py +++ b/cirq-core/cirq/experiments/__init__.py @@ -82,3 +82,9 @@ parallel_two_qubit_xeb as parallel_two_qubit_xeb, run_rb_and_xeb as run_rb_and_xeb, ) + + +from cirq.experiments.z_phase_calibration import ( + z_phase_calibration_workflow as z_phase_calibration_workflow, + calibrate_z_phases as calibrate_z_phases, +) diff --git a/cirq-core/cirq/experiments/two_qubit_xeb.py b/cirq-core/cirq/experiments/two_qubit_xeb.py index 5cb78c5b859..13bd9c78d72 100644 --- a/cirq-core/cirq/experiments/two_qubit_xeb.py +++ b/cirq-core/cirq/experiments/two_qubit_xeb.py @@ -38,6 +38,7 @@ from cirq._compat import cached_method if TYPE_CHECKING: + import multiprocessing import cirq @@ -358,6 +359,7 @@ def parallel_xeb_workflow( cycle_depths: Sequence[int] = (5, 25, 50, 100, 200, 300), random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None, ax: Optional[plt.Axes] = None, + pool: Optional['multiprocessing.pool.Pool'] = None, **plot_kwargs, ) -> Tuple[pd.DataFrame, Sequence['cirq.Circuit'], pd.DataFrame]: """A utility method that runs the full XEB workflow. @@ -373,6 +375,7 @@ def parallel_xeb_workflow( random_state: The random state to use. ax: the plt.Axes to plot the device layout on. If not given, no plot is created. + pool: An optional multiprocessing pool. **plot_kwargs: Arguments to be passed to 'plt.Axes.plot'. Returns: @@ -426,7 +429,7 @@ def parallel_xeb_workflow( ) fids = benchmark_2q_xeb_fidelities( - sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths + sampled_df=sampled_df, circuits=circuit_library, cycle_depths=cycle_depths, pool=pool ) return fids, circuit_library, sampled_df diff --git a/cirq-core/cirq/experiments/xeb_fitting.py b/cirq-core/cirq/experiments/xeb_fitting.py index 7f46d2d7f92..54e9dfcd14c 100644 --- a/cirq-core/cirq/experiments/xeb_fitting.py +++ b/cirq-core/cirq/experiments/xeb_fitting.py @@ -95,6 +95,11 @@ def benchmark_2q_xeb_fidelities( df['e_u'] = np.sum(pure_probs**2, axis=1) df['u_u'] = np.sum(pure_probs, axis=1) / D df['m_u'] = np.sum(pure_probs * sampled_probs, axis=1) + # Var[m_u] = Var[sum p(x) * p_sampled(x)] + # = sum p(x)^2 Var[p_sampled(x)] + # = sum p(x)^2 p(x) (1 - p(x)) + # = sum p(x)^3 (1 - p(x)) + df['var_m_u'] = np.sum(pure_probs**3 * (1 - pure_probs), axis=1) df['y'] = df['m_u'] - df['u_u'] df['x'] = df['e_u'] - df['u_u'] df['numerator'] = df['x'] * df['y'] @@ -103,7 +108,11 @@ def benchmark_2q_xeb_fidelities( def per_cycle_depth(df): """This function is applied per cycle_depth in the following groupby aggregation.""" fid_lsq = df['numerator'].sum() / df['denominator'].sum() - ret = {'fidelity': fid_lsq} + # Note: both df['denominator'] and df['x'] are constants. + # Var[f] = Var[df['numerator']] / (sum df['denominator'])^2 + # = sum (df['x']^2 * df['var_m_u']) / (sum df['denominator'])^2 + var_fid = (df['var_m_u'] * df['x'] ** 2).sum() / df['denominator'].sum() ** 2 + ret = {'fidelity': fid_lsq, 'fidelity_variance': var_fid} def _try_keep(k): """If all the values for a key `k` are the same in this group, we can keep it.""" @@ -385,16 +394,21 @@ def SqrtISwapXEBOptions(*args, **kwargs): def parameterize_circuit( - circuit: 'cirq.Circuit', options: XEBCharacterizationOptions + circuit: 'cirq.Circuit', + options: XEBCharacterizationOptions, + target_gatefamily: Optional[ops.GateFamily] = None, ) -> 'cirq.Circuit': """Parameterize PhasedFSim-like gates in a given circuit according to `phased_fsim_options`. """ + if isinstance(target_gatefamily, ops.GateFamily): + should_parameterize = lambda op: op in target_gatefamily or options.should_parameterize(op) + else: + should_parameterize = options.should_parameterize gate = options.get_parameterized_gate() return circuits.Circuit( circuits.Moment( - gate.on(*op.qubits) if options.should_parameterize(op) else op - for op in moment.operations + gate.on(*op.qubits) if should_parameterize(op) else op for op in moment.operations ) for moment in circuit.moments ) @@ -667,13 +681,16 @@ def _per_pair(f1): a, layer_fid, a_std, layer_fid_std = _fit_exponential_decay( f1['cycle_depth'], f1['fidelity'] ) + fidelity_variance = 0 + if 'fidelity_variance' in f1: + fidelity_variance = f1['fidelity_variance'].values record = { 'a': a, 'layer_fid': layer_fid, 'cycle_depths': f1['cycle_depth'].values, 'fidelities': f1['fidelity'].values, 'a_std': a_std, - 'layer_fid_std': layer_fid_std, + 'layer_fid_std': np.sqrt(layer_fid_std**2 + fidelity_variance), } return pd.Series(record) diff --git a/cirq-core/cirq/experiments/z_phase_calibration.py b/cirq-core/cirq/experiments/z_phase_calibration.py new file mode 100644 index 00000000000..363810fbd13 --- /dev/null +++ b/cirq-core/cirq/experiments/z_phase_calibration.py @@ -0,0 +1,273 @@ +# Copyright 2024 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Provides a method to do z-phase calibration for excitation-preserving gates.""" +from typing import Union, Optional, Sequence, Tuple, Dict, TYPE_CHECKING +import multiprocessing +import multiprocessing.pool + +import matplotlib.pyplot as plt +import numpy as np + +from cirq.experiments import xeb_fitting +from cirq.experiments.two_qubit_xeb import parallel_xeb_workflow +from cirq import ops + +if TYPE_CHECKING: + import cirq + import pandas as pd + + +def z_phase_calibration_workflow( + sampler: 'cirq.Sampler', + qubits: Optional[Sequence['cirq.GridQubit']] = None, + two_qubit_gate: 'cirq.Gate' = ops.CZ, + options: Optional[xeb_fitting.XEBPhasedFSimCharacterizationOptions] = None, + n_repetitions: int = 10**4, + n_combinations: int = 10, + n_circuits: int = 20, + cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)), + random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None, + atol: float = 1e-3, + num_workers_or_pool: Union[int, 'multiprocessing.pool.Pool'] = -1, +) -> Tuple[xeb_fitting.XEBCharacterizationResult, 'pd.DataFrame']: + """Perform z-phase calibration for excitation-preserving gates. + + For a given excitation-preserving two-qubit gate we assume an error model that can be described + using Z-rotations: + 0: ───Rz(a)───two_qubit_gate───Rz(c)─── + │ + 1: ───Rz(b)───two_qubit_gate───Rz(d)─── + for some angles a, b, c, and d. + + Since the two-qubit gate is a excitation-preserving-gate, it can be represented by an FSimGate + and the effect of rotations turns it into a PhasedFSimGate. Using XEB-data we find the + PhasedFSimGate parameters that minimize the infidelity of the gate. + + References: + - https://arxiv.org/abs/2001.08343 + - https://arxiv.org/abs/2010.07965 + - https://arxiv.org/abs/1910.11333 + + Args: + sampler: The quantum engine or simulator to run the circuits. + qubits: Qubits to use. If none, use all qubits on the sampler's device. + two_qubit_gate: The entangling gate to use. + options: The XEB-fitting options. If None, calibrate only the three phase angles + (chi, gamma, zeta) using the representation of a two-qubit gate as an FSimGate + for the initial guess. + n_repetitions: The number of repetitions to use. + n_combinations: The number of combinations to generate. + n_circuits: The number of circuits to generate. + cycle_depths: The cycle depths to use. + random_state: The random state to use. + atol: Absolute tolerance to be used by the minimizer. + num_workers_or_pool: An optional multi-processing pool or number of workers. + A zero value means no multiprocessing. + A positive integer value will create a pool with the given number of workers. + A negative value will create pool with maximum number of workers. + Returns: + - An `XEBCharacterizationResult` object that contains the calibration result. + - A `pd.DataFrame` comparing the before and after fidelities. + """ + + pool: Optional['multiprocessing.pool.Pool'] = None + local_pool = False + if isinstance(num_workers_or_pool, multiprocessing.pool.Pool): + pool = num_workers_or_pool # pragma: no cover + elif num_workers_or_pool != 0: + pool = multiprocessing.Pool(num_workers_or_pool if num_workers_or_pool > 0 else None) + local_pool = True + + fids_df_0, circuits, sampled_df = parallel_xeb_workflow( + sampler=sampler, + qubits=qubits, + entangling_gate=two_qubit_gate, + n_repetitions=n_repetitions, + cycle_depths=cycle_depths, + n_circuits=n_circuits, + n_combinations=n_combinations, + random_state=random_state, + pool=pool, + ) + + if options is None: + options = xeb_fitting.XEBPhasedFSimCharacterizationOptions( + characterize_chi=True, + characterize_gamma=True, + characterize_zeta=True, + characterize_theta=False, + characterize_phi=False, + ).with_defaults_from_gate(two_qubit_gate) + + p_circuits = [ + xeb_fitting.parameterize_circuit(circuit, options, ops.GateFamily(two_qubit_gate)) + for circuit in circuits + ] + + result = xeb_fitting.characterize_phased_fsim_parameters_with_xeb_by_pair( + sampled_df=sampled_df, + parameterized_circuits=p_circuits, + cycle_depths=cycle_depths, + options=options, + fatol=atol, + xatol=atol, + pool=pool, + ) + + before_after = xeb_fitting.before_and_after_characterization( + fids_df_0, characterization_result=result + ) + + if local_pool: + assert isinstance(pool, multiprocessing.pool.Pool) + pool.close() + return result, before_after + + +def calibrate_z_phases( + sampler: 'cirq.Sampler', + qubits: Optional[Sequence['cirq.GridQubit']] = None, + two_qubit_gate: 'cirq.Gate' = ops.CZ, + options: Optional[xeb_fitting.XEBPhasedFSimCharacterizationOptions] = None, + n_repetitions: int = 10**4, + n_combinations: int = 10, + n_circuits: int = 20, + cycle_depths: Sequence[int] = tuple(np.arange(3, 100, 20)), + random_state: 'cirq.RANDOM_STATE_OR_SEED_LIKE' = None, + atol: float = 1e-3, + num_workers_or_pool: Union[int, 'multiprocessing.pool.Pool'] = -1, +) -> Dict[Tuple['cirq.Qid', 'cirq.Qid'], 'cirq.PhasedFSimGate']: + """Perform z-phase calibration for excitation-preserving gates. + + For a given excitation-preserving two-qubit gate we assume an error model that can be described + using Z-rotations: + 0: ───Rz(a)───two_qubit_gate───Rz(c)─── + │ + 1: ───Rz(b)───two_qubit_gate───Rz(d)─── + for some angles a, b, c, and d. + + Since the two-qubit gate is a excitation-preserving gate, it can be represented by an FSimGate + and the effect of rotations turns it into a PhasedFSimGate. Using XEB-data we find the + PhasedFSimGate parameters that minimize the infidelity of the gate. + + References: + - https://arxiv.org/abs/2001.08343 + - https://arxiv.org/abs/2010.07965 + - https://arxiv.org/abs/1910.11333 + + Args: + sampler: The quantum engine or simulator to run the circuits. + qubits: Qubits to use. If none, use all qubits on the sampler's device. + two_qubit_gate: The entangling gate to use. + options: The XEB-fitting options. If None, calibrate only the three phase angles + (chi, gamma, zeta) using the representation of a two-qubit gate as an FSimGate + for the initial guess. + n_repetitions: The number of repetitions to use. + n_combinations: The number of combinations to generate. + n_circuits: The number of circuits to generate. + cycle_depths: The cycle depths to use. + random_state: The random state to use. + atol: Absolute tolerance to be used by the minimizer. + num_workers_or_pool: An optional multi-processing pool or number of workers. + A zero value means no multiprocessing. + A positive integer value will create a pool with the given number of workers. + A negative value will create pool with maximum number of workers. + + Returns: + - A dictionary mapping qubit pairs to the calibrated PhasedFSimGates. + """ + + if options is None: + options = xeb_fitting.XEBPhasedFSimCharacterizationOptions( + characterize_chi=True, + characterize_gamma=True, + characterize_zeta=True, + characterize_theta=False, + characterize_phi=False, + ).with_defaults_from_gate(two_qubit_gate) + + result, _ = z_phase_calibration_workflow( + sampler=sampler, + qubits=qubits, + two_qubit_gate=two_qubit_gate, + options=options, + n_repetitions=n_repetitions, + n_combinations=n_combinations, + n_circuits=n_circuits, + cycle_depths=cycle_depths, + random_state=random_state, + atol=atol, + num_workers_or_pool=num_workers_or_pool, + ) + + gates = {} + for pair, params in result.final_params.items(): + params['theta'] = params.get('theta', options.theta_default or 0) + params['phi'] = params.get('phi', options.phi_default or 0) + params['zeta'] = params.get('zeta', options.zeta_default or 0) + params['chi'] = params.get('chi', options.chi_default or 0) + params['gamma'] = params.get('gamma', options.gamma_default or 0) + gates[pair] = ops.PhasedFSimGate(**params) + return gates + + +def plot_z_phase_calibration_result( + before_after_df: 'pd.DataFrame', + axes: Optional[np.ndarray[Sequence[Sequence['plt.Axes']], np.dtype[np.object_]]] = None, + pairs: Optional[Sequence[Tuple['cirq.Qid', 'cirq.Qid']]] = None, + *, + with_error_bars: bool = False, +) -> np.ndarray[Sequence[Sequence['plt.Axes']], np.dtype[np.object_]]: + """A helper method to plot the result of running z-phase calibration. + + Note that the plotted fidelity is a statistical estimate of the true fidelity and as a result + may be outside the [0, 1] range. + + Args: + before_after_df: The second return object of running `z_phase_calibration_workflow`. + axes: And ndarray of the axes to plot on. + The number of axes is expected to be >= number of qubit pairs. + pairs: If provided, only the given pairs are plotted. + with_error_bars: Whether to add error bars or not. + The width of the bar is an upper bound on standard variation of the estimated fidelity. + """ + if pairs is None: + pairs = before_after_df.index + if axes is None: + # Create a 16x9 rectangle. + ncols = int(np.ceil(np.sqrt(9 / 16 * len(pairs)))) + nrows = (len(pairs) + ncols - 1) // ncols + _, axes = plt.subplots(nrows=nrows, ncols=ncols) + axes = axes if isinstance(axes, np.ndarray) else np.array(axes) + for pair, ax in zip(pairs, axes.flatten()): + row = before_after_df.loc[[pair]].iloc[0] + ax.errorbar( + row.cycle_depths_0, + row.fidelities_0, + yerr=row.layer_fid_std_0 * with_error_bars, + label='original', + ) + ax.errorbar( + row.cycle_depths_0, + row.fidelities_c, + yerr=row.layer_fid_std_c * with_error_bars, + label='calibrated', + ) + ax.axhline(1, linestyle='--') + ax.set_xlabel('cycle depth') + ax.set_ylabel('fidelity estimate') + ax.set_title('-'.join(str(q) for q in pair)) + ax.legend() + return axes diff --git a/cirq-core/cirq/experiments/z_phase_calibration_test.py b/cirq-core/cirq/experiments/z_phase_calibration_test.py new file mode 100644 index 00000000000..c7c149b37c5 --- /dev/null +++ b/cirq-core/cirq/experiments/z_phase_calibration_test.py @@ -0,0 +1,207 @@ +# Copyright 2024 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import numpy as np +import pandas as pd + +import cirq + +from cirq.experiments.z_phase_calibration import ( + calibrate_z_phases, + z_phase_calibration_workflow, + plot_z_phase_calibration_result, +) +from cirq.experiments.xeb_fitting import XEBPhasedFSimCharacterizationOptions + +_ANGLES = ['theta', 'phi', 'chi', 'zeta', 'gamma'] + + +def _create_tests(n, seed, with_options: bool = False): + rng = np.random.default_rng(seed) + angles = (rng.random((n, 5)) * 2 - 1) * np.pi + # Add errors to the last 3 angles (chi, zeta, gamma). + # The errors are in the union (-2, -1) U (1, 2). + # This is because we run the tests with few repetitions so a small error might not get fixed. + error = np.concatenate( + [np.zeros((n, 2)), (rng.random((n, 3)) + 1) * rng.choice([-1, 1], (n, 3))], axis=-1 + ) + if with_options: + options = [] + for _ in range(n): + v = [False, False, False] + # Calibrate only one to keep the run time down. + v[rng.integers(0, 3)] = True + options.append( + { + 'characterize_chi': v[0], + 'characterize_gamma': v[1], + 'characterize_zeta': v[2], + 'characterize_phi': False, + 'characterize_theta': False, + } + ) + + return zip(angles, error, options) + return zip(angles, error) + + +def _trace_distance(A, B): + return 0.5 * np.abs(np.linalg.eigvals(A - B)).sum() + + +class _TestSimulator(cirq.Simulator): + """A simulator that replaces a specific gate by another.""" + + def __init__(self, gate: cirq.Gate, replacement: cirq.Gate, **kwargs): + super().__init__(**kwargs) + self.gate = gate + self.replacement = replacement + + def _core_iterator( + self, + circuit: 'cirq.AbstractCircuit', + sim_state, + all_measurements_are_terminal: bool = False, + ): + new_circuit = cirq.Circuit( + [ + [op if op.gate != self.gate else self.replacement(*op.qubits) for op in m] + for m in circuit + ] + ) + yield from super()._core_iterator(new_circuit, sim_state, all_measurements_are_terminal) + + +@pytest.mark.parametrize( + ['angles', 'error', 'characterization_flags'], + _create_tests(n=10, seed=32432432, with_options=True), +) +def test_calibrate_z_phases(angles, error, characterization_flags): + + original_gate = cirq.PhasedFSimGate(**{k: v for k, v in zip(_ANGLES, angles)}) + actual_gate = cirq.PhasedFSimGate(**{k: v + e for k, v, e in zip(_ANGLES, angles, error)}) + + options = XEBPhasedFSimCharacterizationOptions( + **{f'{n}_default': t for n, t in zip(_ANGLES, angles)}, **characterization_flags + ) + + sampler = _TestSimulator(original_gate, actual_gate, seed=0) + qubits = cirq.q(0, 0), cirq.q(0, 1) + calibrated_gate = calibrate_z_phases( + sampler, + qubits, + original_gate, + options, + n_repetitions=10, + n_combinations=10, + n_circuits=10, + cycle_depths=range(3, 10), + )[qubits] + + initial_unitary = cirq.unitary(original_gate) + final_unitary = cirq.unitary(calibrated_gate) + target_unitary = cirq.unitary(actual_gate) + maximally_mixed_state = np.eye(4) / 2 + dm_initial = initial_unitary @ maximally_mixed_state @ initial_unitary.T.conj() + dm_final = final_unitary @ maximally_mixed_state @ final_unitary.T.conj() + dm_target = target_unitary @ maximally_mixed_state @ target_unitary.T.conj() + + original_dist = _trace_distance(dm_initial, dm_target) + new_dist = _trace_distance(dm_final, dm_target) + + # Either we reduced the error or the error is small enough. + assert new_dist < original_dist or new_dist < 1e-6 + + +@pytest.mark.parametrize(['angles', 'error'], _create_tests(n=3, seed=32432432)) +def test_calibrate_z_phases_no_options(angles, error): + + original_gate = cirq.PhasedFSimGate(**{k: v for k, v in zip(_ANGLES, angles)}) + actual_gate = cirq.PhasedFSimGate(**{k: v + e for k, v, e in zip(_ANGLES, angles, error)}) + + sampler = _TestSimulator(original_gate, actual_gate, seed=0) + qubits = cirq.q(0, 0), cirq.q(0, 1) + calibrated_gate = calibrate_z_phases( + sampler, + qubits, + original_gate, + options=None, + n_repetitions=10, + n_combinations=10, + n_circuits=10, + cycle_depths=range(3, 10), + )[qubits] + + initial_unitary = cirq.unitary(original_gate) + final_unitary = cirq.unitary(calibrated_gate) + target_unitary = cirq.unitary(actual_gate) + maximally_mixed_state = np.eye(4) / 2 + dm_initial = initial_unitary @ maximally_mixed_state @ initial_unitary.T.conj() + dm_final = final_unitary @ maximally_mixed_state @ final_unitary.T.conj() + dm_target = target_unitary @ maximally_mixed_state @ target_unitary.T.conj() + + original_dist = _trace_distance(dm_initial, dm_target) + new_dist = _trace_distance(dm_final, dm_target) + + # Either we reduced the error or the error is small enough. + assert new_dist < original_dist or new_dist < 1e-6 + + +@pytest.mark.parametrize(['angles', 'error'], _create_tests(n=3, seed=32432432)) +def test_calibrate_z_phases_workflow_no_options(angles, error): + + original_gate = cirq.PhasedFSimGate(**{k: v for k, v in zip(_ANGLES, angles)}) + actual_gate = cirq.PhasedFSimGate(**{k: v + e for k, v, e in zip(_ANGLES, angles, error)}) + + sampler = _TestSimulator(original_gate, actual_gate, seed=0) + qubits = cirq.q(0, 0), cirq.q(0, 1) + result, _ = z_phase_calibration_workflow( + sampler, + qubits, + original_gate, + options=None, + n_repetitions=1, + n_combinations=1, + n_circuits=1, + cycle_depths=(1, 2), + ) + + for params in result.final_params.values(): + assert 'zeta' in params + assert 'chi' in params + assert 'gamma' in params + assert 'phi' not in params + assert 'theta' not in params + + +def test_plot_z_phase_calibration_result(): + df = pd.DataFrame() + qs = cirq.q(0, 0), cirq.q(0, 1), cirq.q(0, 2) + df.index = [qs[:2], qs[-2:]] + df['cycle_depths_0'] = [[1, 2, 3]] * 2 + df['fidelities_0'] = [[0.9, 0.8, 0.7], [0.6, 0.4, 0.1]] + df['layer_fid_std_0'] = [0.1, 0.2] + df['fidelities_c'] = [[0.9, 0.92, 0.93], [0.7, 0.77, 0.8]] + df['layer_fid_std_c'] = [0.2, 0.3] + + axes = plot_z_phase_calibration_result(before_after_df=df) + + np.testing.assert_allclose(axes[0].lines[0].get_xdata().astype(float), [1, 2, 3]) + np.testing.assert_allclose(axes[0].lines[0].get_ydata().astype(float), [0.9, 0.8, 0.7]) + np.testing.assert_allclose(axes[0].lines[1].get_ydata().astype(float), [0.9, 0.92, 0.93]) + + np.testing.assert_allclose(axes[1].lines[0].get_xdata().astype(float), [1, 2, 3]) + np.testing.assert_allclose(axes[1].lines[0].get_ydata().astype(float), [0.6, 0.4, 0.1]) + np.testing.assert_allclose(axes[1].lines[1].get_ydata().astype(float), [0.7, 0.77, 0.8])