diff --git a/doc/source/protocols/cross_resonance/amplitude.png b/doc/source/protocols/cross_resonance/amplitude.png new file mode 100644 index 0000000000..bb6694786e Binary files /dev/null and b/doc/source/protocols/cross_resonance/amplitude.png differ diff --git a/doc/source/protocols/cross_resonance/cross_resonance.rst b/doc/source/protocols/cross_resonance/cross_resonance.rst new file mode 100644 index 0000000000..10638025e1 --- /dev/null +++ b/doc/source/protocols/cross_resonance/cross_resonance.rst @@ -0,0 +1,188 @@ +Calibration of CNOT gate using Cross-Resonance +=============================================== + +It is possible to generate an interaction between two superconducting qubits without requiring +flux tunability, through a mechanism known as Cross Resonance (CR). This mechanism relies only +on microwave drive pulses. Moreover, not using flux lines, results in a reduction of the number +of fridge lines and allows to ignore all problems related to flux noise. + +The cross resonance effect was first proposed :cite:p:`CR_First` in and later +independently discovered in :cite:p:`CR_Righetti, CR_Second`. + +The CR effect can be showed by starting with the Hamiltonian of a two-qubit system with +a drive term on the first qubit :cite:p:`Manenti:2023zzn` + +.. math:: + + H = b_1^\dagger b_1 \omega_1 + \frac{\alpha_1}{2} b_1^\dagger b_1^\dagger b_1 b_1 + + b_2^\dagger b_2 \omega_2 + \frac{\alpha_2}{2} b_2^\dagger b_2^\dagger b_2 b_2 + + g (b_1 b_2^\dagger + b_1^\dagger b_2) + \Omega(t) (b_1 + b_1^\dagger) + +If we are in a dispersive regime (i.e. :math:`|\omega_1 - \omega_2| \gg g`), through a +Schrieffer-Wolff transformation we can obtain the effective Hamiltonian: + +.. math:: + + H_\text{eff} = - \frac{\tilde{\omega_1}}{2} \sigma_1^z - \frac{\tilde{\omega_2}}{2} \sigma_2^z + + \frac{\zeta}{4} \sigma_1^z \sigma_2^z + + \Omega(t) \Big[ \sigma_1^x + \nu \sigma_2^x + \mu \sigma_1^z \sigma_2^x\Big] + +where :math:`\zeta` is the ZZ coupling, :math:`\nu` is quantum crosstalk factor and :math:`\mu` is the +cross-resonance factor. From the equation above we can see that by driving the first qubit +at the frequency of the second qubit . + +By tuning the amplitude and the duration of this drive pulse it is possible to calibrate a +:math:`RZX` rotation to rotate exactly by :math:`- \pi/2`. This is done because starting +from a :math:`ZX_{frac{\pi}{2}}` we can obtain a CNOT gate using single qubit rotations. + +.. math:: + + \text{CNOT} = \text{R}_\text{ZX}(-\pi/2) \text{R}_\text{IX}(\pi/2) \text{R}_\text{ZI}(\pi/2) + +In Qibocal we provide protocols to calibrate CR pulses. + +Sweeping the duration of the CR pulse +------------------------------------- + +In a first experiment we can sweep the duration of the CR pulse and measure both the +target and control qubit. The measurement is performed while preparing the control +qubit in state :math:`\ket{0}` and :math:`\ket{1}`. + +Parameters +^^^^^^^^^^ + +.. autoclass:: qibocal.protocols.two_qubit_interaction.cross_resonance.length.CrossResonanceLengthParameters + :noindex: + +Example +^^^^^^^ + +A possible runcard to launch the experiment could be the following: + +.. code-block:: yaml + + - id: CR length + operation: cross_resonance_length + parameters: + targets: [[0,1]] + pulse_duration_start: 10 + pulse_duration_end: 200 + pulse_duration_step: 10 + flux_pulse_amplitude: 0.1 + nshots: 2000 + relaxation_time: 50000 + + +The expected output is the following: + +.. image:: length.png + +Post-processing +^^^^^^^^^^^^^^^ + +The probability of the target qubit is fitted in both cases to a dumped cosine functions. +It is possible to extract the effective coupling as + +.. math:: + + \text{J}_\text{eff}/ 2 \pi = \frac{f^{\pi}_\text{Rabi} - f_\text{Rabi}}{2} + + +where :math:`f^{\pi}_\text{Rabi}` and :math:`f_\text{Rabi}` are the frequencies of the +fitted Rabi oscillations on the target qubit. + +Sweeping amplitude of the CR pulse +---------------------------------- + +Similarly it is possible to sweep the amplitude of the CR pulse and measure both the +target and control qubit. + + +Parameters +^^^^^^^^^^ + +.. autoclass:: qibocal.protocols.two_qubit_interaction.cross_resonance.length.CrossResonanceLengthParameters + :noindex: + +Example +^^^^^^^ + +A possible runcard to launch the experiment could be the following: + +.. code-block:: yaml + + - id: CR amplitude + operation: cross_resonance_amplitude + parameters: + targets: [[0,1]] + max_amp: 0.05 + min_amp: 0.01 + step_amp: 0.005 + pulse_duration: 100 + nshots: 2000 + relaxation_time: 50000 + + +The expected output is the following: + +.. image:: amplitude.png + +Post-processing +^^^^^^^^^^^^^^^ + +The probability of the target qubit is fitted in both cases to a cosine function. + +Hamiltonian Tomography measurement +---------------------------------- + +Although from the two previous experiments it is possible to perform an initial +calibration of the CR gate, by performing a state tomography on the target qubit it is +possible to reconstruct the effective Hamiltonian of the system :cite:p:`CRDrag`: + +.. math:: + + H_\text{eff} = \frac{\nu_\text{ZX}}{2} \text{ZX} + \frac{\nu_\text{ZY}}{2} \text{ZY} + + \frac{\nu_\text{ZZ}}{2} \text{ZZ} + \frac{\nu_\text{IX}}{2} \text{IX} + + \frac{\nu_\text{IY}}{2} \text{IY} + \frac{\nu_\text{IZ}}{2} \text{IZ} + +In particular, by sweeping the duration of the CR pulse and measuring the expectation +values of the target qubit :math:`\langle X \rangle`, :math:`\langle Y \rangle` and :math:`\langle Z \rangle` +when the control qubit is prepared in :math:`\ket{0}` and :math:`\ket{1}` we can compute all terms in +the effective Hamiltonian following the procedure in :cite:p:`CRDrag`. + +Parameters +^^^^^^^^^^ + + +.. autoclass:: qibocal.protocols.two_qubit_interaction.cross_resonance.hamiltonian_tomography.length.HamiltonianTomographyCRLengthParameters + :noindex: + +Example +^^^^^^^ + +A possible runcard to launch the experiment could be the following: + +.. code-block:: yaml + + - id: Hamiltonian tomography CR + operation: cross_resonance_amplitude + parameters: + targets: [[0,1]] + nshots: 2000 + pulse_amplitude: 0.1 + pulse_duration_end: 400 + pulse_duration_start: 10 + pulse_duration_step: 20 + + +The expected output is the following: + +.. image:: tomography_length.png + + + +Requirements +^^^^^^^^^^^^ + +To run these experiments single qubit gates for both target and control qubit needs +to be calibrated. diff --git a/doc/source/protocols/cross_resonance/length.png b/doc/source/protocols/cross_resonance/length.png new file mode 100644 index 0000000000..a04dfc7595 Binary files /dev/null and b/doc/source/protocols/cross_resonance/length.png differ diff --git a/doc/source/protocols/cross_resonance/tomography_length.png b/doc/source/protocols/cross_resonance/tomography_length.png new file mode 100644 index 0000000000..582646f995 Binary files /dev/null and b/doc/source/protocols/cross_resonance/tomography_length.png differ diff --git a/doc/source/protocols/index.rst b/doc/source/protocols/index.rst index 34409aa87e..a989a4a6ce 100644 --- a/doc/source/protocols/index.rst +++ b/doc/source/protocols/index.rst @@ -44,4 +44,5 @@ In this section we introduce the basics of all protocols supported by ``qibocal` coherence/index chsh twpa_calibration/twpa + cross_resonance/cross_resonance references diff --git a/doc/source/refs.bib b/doc/source/refs.bib index d8dfdd2e63..6f697db531 100644 --- a/doc/source/refs.bib +++ b/doc/source/refs.bib @@ -293,3 +293,65 @@ @misc{shin2024inoperandomicrowavescatteringparametercalibrated primaryClass={quant-ph}, url={https://arxiv.org/abs/2406.03063}, } + +@article{CR_First, + title = {Microwave-induced coupling of superconducting qubits}, + author = {Paraoanu, G. S.}, + journal = {Phys. Rev. B}, + volume = {74}, + issue = {14}, + pages = {140504}, + numpages = {4}, + year = {2006}, + month = {Oct}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.74.140504}, + url = {https://link.aps.org/doi/10.1103/PhysRevB.74.140504} +} + +@article{CR_Righetti, + title = {Fully microwave-tunable universal gates in superconducting qubits with linear couplings and fixed transition frequencies}, + author = {Rigetti, Chad and Devoret, Michel}, + journal = {Phys. Rev. B}, + volume = {81}, + issue = {13}, + pages = {134507}, + numpages = {7}, + year = {2010}, + month = {Apr}, + publisher = {American Physical Society}, + doi = {10.1103/PhysRevB.81.134507}, + url = {https://link.aps.org/doi/10.1103/PhysRevB.81.134507} +} + +@article{CR_Second, +author = {Groot, P. and Lisenfeld, Jürgen and Schouten, R. and Ashhab, S. and Lupascu, Adrian and Harmans, Kees and Mooij, Hans}, +year = {2010}, +month = {08}, +pages = {}, +title = {Selective darkening of degenerate transitions demonstrated with two +superconducting quantum bits}, +volume = {6}, +journal = {Nature Physics}, +doi = {10.1038/nphys1733} +} + +@book{Manenti:2023zzn, + author = "Manenti, Riccardo and Motta, Mario", + title = "{Quantum Information Science}", + isbn = "978-0-19-878748-8", + publisher = "Oxford University Press", + month = "8", + year = "2023" +} + +@article{CRDrag, +author = {Li, Boxi and Calarco, Tommaso and Motzoi, Felix}, +year = {2024}, +month = {07}, +pages = {}, +title = {Experimental error suppression in Cross-Resonance gates via multi-derivative pulse shaping}, +volume = {10}, +journal = {npj Quantum Information}, +doi = {10.1038/s41534-024-00863-4} +} diff --git a/platforms/qutrits/calibration.json b/platforms/qutrits/calibration.json new file mode 100644 index 0000000000..def074c85e --- /dev/null +++ b/platforms/qutrits/calibration.json @@ -0,0 +1,75 @@ +{ + "single_qubits": { + "0": { + "resonator": { + "bare_frequency": 0.0, + "dressed_frequency": 0.0, + "depletion_time": 0, + "bare_frequency_amplitude": null + }, + "qubit": { + "frequency_01": 5114000000.0, + "frequency_12": 4784000000.0, + "maximum_frequency": 5114000000.0, + "asymmetry": 0.0, + "sweetspot": 0.0, + "flux_coefficients": null + }, + "readout": { + "fidelity": 0.0, + "coupling": null, + "effective_temperature": null, + "ground_state": [ + 0.0, + 1.0 + ], + "excited_state": [ + 1.0, + 0.0 + ], + "qudits_frequency": {} + }, + "t1": null, + "t2": null, + "t2_spin_echo": null, + "rb_fidelity": null + }, + "1": { + "resonator": { + "bare_frequency": 0.0, + "dressed_frequency": 0.0, + "depletion_time": 0, + "bare_frequency_amplitude": null + }, + "qubit": { + "frequency_01": 4914000000.0, + "frequency_12": 4584000000.0, + "maximum_frequency": 4914000000.0, + "asymmetry": 0.0, + "sweetspot": 0.0, + "flux_coefficients": null + }, + "readout": { + "fidelity": 0.0, + "coupling": null, + "effective_temperature": null, + "ground_state": [ + 0.0, + 1.0 + ], + "excited_state": [ + 1.0, + 0.0 + ], + "qudits_frequency": {} + }, + "t1": null, + "t2": null, + "t2_spin_echo": null, + "rb_fidelity": null + } + }, + "two_qubits": {}, + "readout_mitigation_matrix": null, + "flux_crosstalk_matrix": null +} diff --git a/platforms/qutrits/parameters.json b/platforms/qutrits/parameters.json new file mode 100644 index 0000000000..fdd91e5840 --- /dev/null +++ b/platforms/qutrits/parameters.json @@ -0,0 +1,357 @@ +{ + "settings": { + "nshots": 1024, + "relaxation_time": 0 + }, + "configs": { + "emulator/bounds": { + "kind": "bounds", + "waveforms": 1000000.0, + "readout": 50, + "instructions": 200 + }, + "hamiltonian": { + "kind": "hamiltonian", + "transmon_levels": 3, + "single_qubit": { + "0": { + "frequency": 5114000000.0, + "anharmonicity": -330000000.0, + "t1": { + "0-1": 38000.0, + "1-2": 19000.0 + }, + "t2": { + "0-1": 50000.0 + } + }, + "1": { + "frequency": 4914000000.0, + "anharmonicity": -330000000.0, + "t1": { + "0-1": 42000.0, + "1-2": 21000.0 + }, + "t2": { + "0-1": 61000.0 + } + } + }, + "two_qubit": { + "0-1": { + "coupling": 3800000.0 + } + } + }, + "0/drive": { + "kind": "drive-emulator", + "frequency": 5114000000.0, + "rabi_frequency": 50000000.0, + "scale_factor": 10.0 + }, + "0/drive1": { + "kind": "drive-emulator", + "frequency": 4914000000.0, + "rabi_frequency": 50000000.0, + "scale_factor": 10.0 + }, + "0/drive12": { + "kind": "drive-emulator", + "frequency": 4784000000.0, + "rabi_frequency": 50000000.0, + "scale_factor": 10.0 + }, + "1/drive": { + "kind": "drive-emulator", + "frequency": 4914000000.0, + "rabi_frequency": 50000000.0, + "scale_factor": 10.0 + }, + "1/drive12": { + "kind": "drive-emulator", + "frequency": 4584000000.0, + "rabi_frequency": 50000000.0, + "scale_factor": 10.0 + }, + "0/probe": { + "kind": "iq", + "frequency": 5500000000.0 + }, + "0/acquisition": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": null + }, + "1/probe": { + "kind": "iq", + "frequency": 5500000000.0 + }, + "1/acquisition": { + "kind": "acquisition", + "delay": 0.0, + "smearing": 0.0, + "threshold": 0.0, + "iq_angle": 0.0, + "kernel": null + } + }, + "native_gates": { + "single_qubit": { + "0": { + "RX": [ + [ + "0/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.200449, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ] + ], + "RX90": [ + [ + "0/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.1002245, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "0/drive12", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.1426586, + "envelope": { + "kind": "gaussian", + "rel_sigma": 0.1 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "0/acquisition", + { + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 10.0 + }, + "probe": { + "kind": "pulse", + "duration": 10.0, + "amplitude": 0.1, + "envelope": { + "kind": "rectangular" + }, + "relative_phase": 0.0 + } + } + ] + ], + "CP": null + }, + "1": { + "RX": [ + [ + "1/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.200792, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ] + ], + "RX90": [ + [ + "1/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.100396, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ] + ], + "RX12": [ + [ + "1/drive12", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.138545, + "envelope": { + "kind": "gaussian", + "rel_sigma": 0.1 + }, + "relative_phase": 0.0 + } + ] + ], + "MZ": [ + [ + "1/acquisition", + { + "kind": "readout", + "acquisition": { + "kind": "acquisition", + "duration": 10.0 + }, + "probe": { + "kind": "pulse", + "duration": 10.0, + "amplitude": 0.1, + "envelope": { + "kind": "rectangular" + }, + "relative_phase": 0.0 + } + } + ] + ], + "CP": null + } + }, + "coupler": {}, + "two_qubit": { + "0-1": { + "CZ": null, + "CNOT": [ + [ + "0/drive1", + { + "kind": "pulse", + "duration": 50.0, + "amplitude": 0.1, + "envelope": { + "kind": "rectangular" + }, + "relative_phase": 0.0 + } + ], + [ + "0/drive", + { + "kind": "delay", + "duration": 50.0 + } + ], + [ + "0/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.200449, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ], + [ + "0/drive1", + { + "kind": "delay", + "duration": 20.0 + } + ], + [ + "01/drive", + { + "kind": "pulse", + "duration": 50.0, + "amplitude": -0.1, + "envelope": { + "kind": "rectangular" + }, + "relative_phase": 0.0 + } + ], + [ + "0/drive", + { + "kind": "delay", + "duration": 50.0 + } + ], + [ + "0/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.200449, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ], + [ + "1/drive", + { + "kind": "delay", + "duration": 140.0 + } + ], + [ + "1/drive", + { + "kind": "pulse", + "duration": 20.0, + "amplitude": 0.100396, + "envelope": { + "kind": "drag", + "rel_sigma": 0.1, + "beta": 0.25 + }, + "relative_phase": 0.0 + } + ], + [ + "0/drive", + { + "kind": "virtualz", + "phase": -1.5707963267948966 + } + ] + ], + "iSWAP": null + } + } + } +} diff --git a/platforms/qutrits/platform.py b/platforms/qutrits/platform.py new file mode 100644 index 0000000000..00dc63d7b2 --- /dev/null +++ b/platforms/qutrits/platform.py @@ -0,0 +1,45 @@ +import pathlib + +from qibolab import ConfigKinds +from qibolab._core.components import IqChannel +from qibolab._core.instruments.emulator.emulator import EmulatorController +from qibolab._core.instruments.emulator.hamiltonians import ( + DriveEmulatorConfig, + HamiltonianConfig, +) +from qibolab._core.platform import Platform +from qibolab._core.qubits import Qubit + +FOLDER = pathlib.Path(__file__).parent + +ConfigKinds.extend([HamiltonianConfig, DriveEmulatorConfig]) + + +def create() -> Platform: + """Create a dummy platform using the dummy instrument.""" + qubits = {} + channels = {} + + qubits[0] = qubit = Qubit.default( + 0, drive_extra={(1, 2): "0/drive12", 1: "0/drive1"} + ) + channels |= { + qubit.drive: IqChannel(mixer=None, lo=None), + qubits[0].drive_extra[1, 2]: IqChannel(mixer=None, lo=None), + qubits[0].drive_extra[1]: IqChannel(mixer=None, lo=None), + } + qubits[1] = qubit = Qubit.default(1, drive_extra={(1, 2): "1/drive12"}) + channels |= { + qubit.drive: IqChannel(mixer=None, lo=None), + qubits[1].drive_extra[1, 2]: IqChannel(mixer=None, lo=None), + } + # register the instruments + instruments = { + "dummy": EmulatorController(address="0.0.0.0", channels=channels), + } + + return Platform.load( + path=FOLDER, + instruments=instruments, + qubits=qubits, + ) diff --git a/poetry.lock b/poetry.lock index 36805c4e47..6d7c96f037 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1748,6 +1748,22 @@ scipy = ">=1.13.0,<2.0.0" [package.extras] emulator = ["qutip (>=5.0.2,<6.0.0)"] qrng = ["pyserial (>=3.5,<4.0)"] +<<<<<<< HEAD +======= +rfsoc = ["qibosoq (>=0.1.2,<0.2) ; python_version < \"3.12\""] +twpa = ["pyvisa-py (==0.5.3)", "qcodes (>=0.37.0,<0.38.0)", "qcodes_contrib_drivers (==0.18.0)"] +zh = ["laboneq (==2.25.0)"] + +[package.source] +type = "git" +url = "https://github.com/qiboteam/qibolab.git" +reference = "emulator-multi-transmon" +<<<<<<< HEAD +resolved_reference = "3961167a4e00dcaa488ec9ee92eecb0d8bf9d897" +>>>>>>> 92fc0ac3 (feat: Adding echoCR and use parameters from IBM chip) +======= +resolved_reference = "92e6f980e162c4e79b1403bb0e19d0fd2f4b16a1" +>>>>>>> 3caefecd (refactor: Document code and add tests) [[package]] name = "recommonmark" @@ -2488,6 +2504,7 @@ files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] +<<<<<<< HEAD markers = {dev = "python_version < \"3.12\"", tests = "python_version == \"3.10\""} [[package]] @@ -2504,6 +2521,9 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +======= +markers = {dev = "python_version == \"3.9\""} +>>>>>>> 92fc0ac3 (feat: Adding echoCR and use parameters from IBM chip) [[package]] name = "tzdata" @@ -2559,7 +2579,53 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +<<<<<<< HEAD [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" content-hash = "281cd81988c166721554a5d7219cb27ece781228386385c465beffd3abd16e0c" +======= +[[package]] +name = "wmctrl" +version = "0.5" +description = "A tool to programmatically control windows inside X" +optional = false +python-versions = ">=2.7" +groups = ["dev"] +files = [ + {file = "wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7"}, + {file = "wmctrl-0.5.tar.gz", hash = "sha256:7839a36b6fe9e2d6fd22304e5dc372dbced2116ba41283ea938b2da57f53e962"}, +] + +[package.dependencies] +attrs = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main", "docs"] +markers = "python_version == \"3.9\"" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9,<3.13" +content-hash = "383831e92b04ba178ff69237545286fbe62746c4a24293e6b829bd7c33b66a75" +>>>>>>> 92fc0ac3 (feat: Adding echoCR and use parameters from IBM chip) diff --git a/src/qibocal/auto/transpile.py b/src/qibocal/auto/transpile.py index a38872bc1d..0dd180d272 100644 --- a/src/qibocal/auto/transpile.py +++ b/src/qibocal/auto/transpile.py @@ -124,7 +124,11 @@ def natives(platform) -> dict[str, NativeContainer]: # add two qubit natives only if there are pairs pair = next(iter(platform.pairs)) two_qubit_natives_container = platform.natives.two_qubit[pair] - two_qubit_natives = list(two_qubit_natives_container.model_fields) + two_qubit_natives = [ + i + for i in list(two_qubit_natives_container.model_fields) + if getattr(two_qubit_natives_container, i) is not None + ] else: two_qubit_natives = [] # Solve Qibo-Qibolab mismatch diff --git a/src/qibocal/protocols/rabi/utils.py b/src/qibocal/protocols/rabi/utils.py index 98e258d87b..47872a6d3b 100644 --- a/src/qibocal/protocols/rabi/utils.py +++ b/src/qibocal/protocols/rabi/utils.py @@ -405,4 +405,6 @@ def fit_amplitude_function( ), ] pi_pulse_parameter = popt[2] / 2 * period_correction_factor(phase=popt[3]) + if not isinstance(popt, list): + popt = popt.tolist() return popt, perr, pi_pulse_parameter diff --git a/src/qibocal/protocols/two_qubit_interaction/__init__.py b/src/qibocal/protocols/two_qubit_interaction/__init__.py index 9b3f9ac141..5df3b5810c 100644 --- a/src/qibocal/protocols/two_qubit_interaction/__init__.py +++ b/src/qibocal/protocols/two_qubit_interaction/__init__.py @@ -1,5 +1,10 @@ from .chevron import chevron, chevron_signal from .chsh import chsh +from .cross_resonance import ( + cross_resonance_amplitude, + cross_resonance_length, + hamiltonian_tomography_cr_length, +) from .optimize import optimize_two_qubit_gate from .snz_optimize import snz_optimize from .snz_optimize_t_idle import snz_optimize_t_idle @@ -11,3 +16,9 @@ __all__ += ["optimize_two_qubit_gate", "correct_virtual_z_phases"] __all__ += ["snz_optimize", "snz_optimize_t_idle", "snz_optimize_t_idle_vs_t_tot"] __all__ += ["chsh"] +__all__ += [ + "cross_resonance_length", + "cross_resonance_amplitude", + "hamiltonian_tomography_cr", +] +__all__ += ["hamiltonian_tomography_cr_length"] diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/__init__.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/__init__.py new file mode 100644 index 0000000000..36a09e9006 --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/__init__.py @@ -0,0 +1,5 @@ +from .amplitude import cross_resonance_amplitude as cross_resonance_amplitude +from .hamiltonian_tomography import ( + hamiltonian_tomography_cr_length as hamiltonian_tomography_cr_length, +) +from .length import cross_resonance_length as cross_resonance_length diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/amplitude.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/amplitude.py new file mode 100644 index 0000000000..6d9626849e --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/amplitude.py @@ -0,0 +1,195 @@ +"""Protocol for cross resonance with sweep on amplitude of the pulse.""" + +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +from qibolab import ( + AcquisitionType, + AveragingMode, + Parameter, + Sweeper, +) + +from ....auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) +from ....calibration import CalibrationPlatform +from ....result import probability +from ...rabi.utils import fit_amplitude_function, rabi_amplitude_function +from .utils import SetControl, cr_fit, cr_plot, cr_sequence + +CrossResonanceAmplitudeType = np.dtype( + [ + ("prob_target", np.float64), + ("error_target", np.float64), + ("prob_control", np.float64), + ("error_control", np.float64), + ("x", np.float64), + ] +) +"""Custom dtype for cross resonance amplitude.""" + + +@dataclass +class CrossResonanceAmplitudeParameters(Parameters): + """CrossResonanceAmplitude runcard inputs.""" + + min_amp: float + """Minimum amplitude.""" + max_amp: float + """Maximum amplitude.""" + step_amp: float + """Step amplitude.""" + pulse_duration: int + """CR pulse duration in ns.""" + echo: bool = False + """Apply echo sequence or not.""" + + @property + def amplitude_range(self): + return np.arange(self.min_amp, self.max_amp, self.step_amp) + + +@dataclass +class CrossResonanceAmplitudeResults(Results): + """CrossResonanceAmplitude outputs.""" + + fitted_parameters: dict[tuple[QubitPairId, str], list] = field(default_factory=dict) + + def __contains__(self, pair: QubitPairId): + return all(key[:2] == pair for key in list(self.fitted_parameters)) + + +@dataclass +class CrossResonanceAmplitudeData(Data): + """Data structure for CR amplitude.""" + + data: dict[ + tuple[QubitId, QubitId, str], npt.NDArray[CrossResonanceAmplitudeType] + ] = field(default_factory=dict) + """Raw data acquired.""" + + @property + def pairs(self): + return {(i[0], i[1]) for i in self.data} + + +def _acquisition( + params: CrossResonanceAmplitudeParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> CrossResonanceAmplitudeData: + """Data acquisition for CR amplitude. + + We measure the probabilities of both the target and the control qubit after + applying the CR sequence specified by the input parameters. We repeat the + measurement twice for each target qubit, once with the control qubit in state 0 + and once with the control qubit in state 1. + """ + + data = CrossResonanceAmplitudeData() + + for pair in targets: + control, target = pair + pair = (control, target) + for setup in SetControl: + sequence, cr_pulses, _, _ = cr_sequence( + platform=platform, + control=control, + target=target, + setup=setup, + amplitude=params.min_amp, + duration=params.pulse_duration, + echo=params.echo, + ) + sweeper = Sweeper( + parameter=Parameter.amplitude, + values=params.amplitude_range, + pulses=cr_pulses, + ) + + updates = [] + updates.append( + { + platform.qubits[control].drive_extra[target]: { + "frequency": platform.config( + platform.qubits[target].drive + ).frequency + } + } + ) + results = platform.execute( + [sequence], + [[sweeper]], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.SINGLESHOT, + updates=updates, + ) + + target_acq_handle = list( + sequence.channel(platform.qubits[target].acquisition) + )[-1].id + control_acq_handle = list( + sequence.channel(platform.qubits[control].acquisition) + )[-1].id + prob_target = probability(results[target_acq_handle], state=1) + prob_control = probability(results[control_acq_handle], state=1) + data.register_qubit( + CrossResonanceAmplitudeType, + (control, target, setup), + dict( + x=sweeper.values, + prob_target=prob_target, + error_target=np.sqrt( + prob_target * (1 - prob_target) / params.nshots + ).tolist(), + prob_control=prob_control, + error_control=np.sqrt( + prob_control * (1 - prob_control) / params.nshots + ).tolist(), + ), + ) + return data + + +def _fit( + data: CrossResonanceAmplitudeData, +) -> CrossResonanceAmplitudeResults: + """Post-processing function for CrossResonanceAmplitude. + + The target qubit probabilities are fitted with cosine oscillations. + + """ + + fitted_parameters = cr_fit(data=data, fitting_function=fit_amplitude_function) + return CrossResonanceAmplitudeResults( + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: CrossResonanceAmplitudeData, + target: QubitPairId, + fit: CrossResonanceAmplitudeResults, +): + """Plotting function for CrossResonanceAmplitude.""" + figs, fitting_report = cr_plot( + data=data, target=target, fit=fit, fitting_function=rabi_amplitude_function + ) + figs[0].update_layout( + xaxis_title="Cross resonance pulse amplitude [a.u.]", + yaxis_title="Excited state population", + ) + return figs, fitting_report + + +cross_resonance_amplitude = Routine(_acquisition, _fit, _plot) +"""CrossResonance Routine object.""" diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/__init__.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/__init__.py new file mode 100644 index 0000000000..7197098af1 --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/__init__.py @@ -0,0 +1,3 @@ +from .length import ( + hamiltonian_tomography_cr_length as hamiltonian_tomography_cr_length, +) diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/fitting.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/fitting.py new file mode 100644 index 0000000000..4b0a07c70e --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/fitting.py @@ -0,0 +1,49 @@ +"""Fitting function for CR tomography.""" + +import numpy as np + + +def fit_Z_exp(t: np.ndarray, wx: float, wy: float, wz: float, w: float) -> np.ndarray: + """Fitting Z expectation value for CR tomography. + + See https://arxiv.org/pdf/2303.01427 Eq. S10. + """ + return ((wx**2 + wy**2) * np.cos(w * t) + wz**2) / w**2 + + +def fit_X_exp(t: np.ndarray, wx: float, wy: float, wz: float, w: float) -> np.ndarray: + """Fitting X expectation value for CR tomography. + + See https://arxiv.org/pdf/2303.01427 Eq. S10. + """ + return (-wx * wz * np.cos(w * t) + w * wy * np.sin(t * w) + wx * wz) / w**2 + + +def fit_Y_exp(t: np.ndarray, wx: float, wy: float, wz: float, w: float) -> np.ndarray: + """Fitting Y expectation value for CR tomography. + + See https://arxiv.org/pdf/2303.01427 Eq. S10. + """ + return (-w * wx * np.sin(w * t) - wy * wz * np.cos(t * w) + wy * wz) / w**2 + + +def combined_fit( + t: np.ndarray, + wx: float, + wy: float, + wz: float, +) -> np.ndarray: + """Simulateneous fit for X, Y and Z expectation values. + + We also constrain w to be the norm of the w vector with component + wx, wy, wz.""" + + w = np.sqrt(wx**2 + wy**2 + wz**2) + t1, t2, t3 = np.split(t, 3) + return np.concatenate( + [ + fit_X_exp(t1, wx, wy, wz, w), + fit_Y_exp(t2, wx, wy, wz, w), + fit_Z_exp(t3, wx, wy, wz, w), + ] + ) diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/length.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/length.py new file mode 100644 index 0000000000..1a2f891f74 --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/length.py @@ -0,0 +1,283 @@ +"""Hamiltonian tomography protocol for CR gate calibration. + +This protocol computes the expectation values for X, Y and Z for the target qubit +after the application of a cross resonance sequence. The CR pulses are played on the control drive +channel with frequency set to the frequency of the target drive channel. +""" + +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +from qibolab import ( + AcquisitionType, + AveragingMode, + Parameter, + Sweeper, +) +from scipy.constants import kilo + +from .....auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) +from .....calibration import CalibrationPlatform +from .....result import probability +from ....utils import table_dict, table_html +from ..utils import Basis, SetControl, cr_sequence +from .utils import ( + EPS, + HamiltonianTerm, + extract_hamiltonian_terms, + tomography_cr_fit, + tomography_cr_plot, +) + +# DEBUG + +HamiltonianTomographyCRLengthType = np.dtype( + [ + ("prob_target", np.float64), + ("error_target", np.float64), + ("prob_control", np.float64), + ("error_control", np.float64), + ("x", np.int64), + ] +) +"""Custom dtype for CR length.""" + + +@dataclass +class HamiltonianTomographyCRLengthParameters(Parameters): + """HamiltonianTomographyCRLength runcard inputs.""" + + pulse_duration_start: float + """Initial duration of CR pulse [ns].""" + pulse_duration_end: float + """Final duration of CR pulse [ns].""" + pulse_duration_step: float + """Step CR pulse duration [ns].""" + pulse_amplitude: float + """CR pulse amplitude""" + phase: float = 0 + """Phase of CR pulse.""" + target_amplitude: float = 0 + """Amplitude of cancellation pulse.""" + target_phase: float = 0 + """Phase of target pulse.""" + interpolated_sweeper: bool = False + """Use real-time interpolation if supported by instruments.""" + echo: bool = False + """Apply echo sequence or not. + + The ECR is described in https://arxiv.org/pdf/1210.7011 + """ + + @property + def duration_range(self) -> np.ndarray: + """Duration range for CR pulses.""" + return np.arange( + self.pulse_duration_start, self.pulse_duration_end, self.pulse_duration_step + ) + + +@dataclass +class HamiltonianTomographyCRLengthResults(Results): + """HamiltonianTomographyCRLength outputs.""" + + hamiltonian_terms: dict[QubitId, QubitId, HamiltonianTerm] = field( + default_factory=dict + ) + """Terms in effective Hamiltonian.""" + fitted_parameters: dict[tuple[QubitId, QubitId, Basis, SetControl], list] = field( + default_factory=dict + ) + """Fitted parameters from X,Y,Z expectation values.""" + + def __contains__(self, pair: QubitPairId) -> bool: + return all(key[:2] == pair for key in list(self.fitted_parameters)) + + +@dataclass +class HamiltonianTomographyCRLengthData(Data): + """Data structure for CR length.""" + + data: dict[ + tuple[QubitId, QubitId, Basis, SetControl], + npt.NDArray[HamiltonianTomographyCRLengthType], + ] = field(default_factory=dict) + """Raw data acquired.""" + + @property + def pairs(self): + return {(i[0], i[1]) for i in self.data} + + +def _acquisition( + params: HamiltonianTomographyCRLengthParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> HamiltonianTomographyCRLengthData: + """Data acquisition for Hamiltonian tomography CR protocol. + + We measure the expectation values X,Y and Z on the target qubit after + applying the CR sequence specified by the input parameters. We repeat the + measurement twice for each target qubit, once with the control qubit in state 0 + and once with the control qubit in state 1. + + We store the probability of the control qubit and the expectation value of the target qubit. + + """ + + data = HamiltonianTomographyCRLengthData() + + for pair in targets: + control, target = pair + pair = (control, target) + for basis in Basis: + for setup in SetControl: + sequence, cr_pulses, cr_target_pulses, delays = cr_sequence( + platform=platform, + control=control, + target=target, + setup=setup, + amplitude=params.pulse_amplitude, + phase=params.phase, + target_amplitude=params.target_amplitude, + target_phase=params.target_phase, + duration=params.pulse_duration_end, + interpolated_sweeper=params.interpolated_sweeper, + echo=params.echo, + basis=basis, + ) + + if params.interpolated_sweeper: + sweeper = Sweeper( + parameter=Parameter.duration_interpolated, + values=params.duration_range, + pulses=cr_pulses + cr_target_pulses, + ) + else: + sweeper = Sweeper( + parameter=Parameter.duration, + values=params.duration_range, + pulses=cr_pulses + cr_target_pulses + delays, + ) + + updates = [] + updates.append( + { + platform.qubits[control].drive_extra[(1, 2)]: { + "frequency": platform.config( + platform.qubits[target].drive + ).frequency + } + } + ) + acquisition_type = AcquisitionType.INTEGRATION + results = platform.execute( + [sequence], + [[sweeper]], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=acquisition_type, + averaging_mode=AveragingMode.SINGLESHOT, + updates=updates, + ) + target_acq_handle = list( + sequence.channel(platform.qubits[target].acquisition) + )[-1].id + control_acq_handle = list( + sequence.channel(platform.qubits[control].acquisition) + )[-1].id + + if acquisition_type is AcquisitionType.INTEGRATION: + prob_target = results[target_acq_handle] + prob_control = results[control_acq_handle] + else: + prob_target = probability(results[target_acq_handle], state=1) + prob_control = probability(results[control_acq_handle], state=1) + # HERE I COULD RECOVER THE CORRECT SOLUTION ONLY FOR 'fixed-frequency-qutrits' PLATFORM + # STILL NOT WORKING FOR 'qutrits' PLATFORM + # t = datetime.datetime.now().strftime("%H:%M:%S") + # np.savez(f'{t}_qibocal_lengthpy_qutip_evolution.npz', np.stack([prob_control, prob_target])) + + # TODO: possibly drop control probablity even if it might be useful later on + # to compute leakage + data.register_qubit( + HamiltonianTomographyCRLengthType, + (control, target, basis, setup), + dict( + x=sweeper.values, + prob_target=1 - 2 * prob_target, + error_target=( + 2 + * np.sqrt( + EPS + prob_target * (1 - prob_target) / params.nshots + ) + ).tolist(), + prob_control=prob_control, + error_control=np.sqrt( + EPS + prob_control * (1 - prob_control) / params.nshots + ).tolist(), + ), + ) + return data + + +def _fit( + data: HamiltonianTomographyCRLengthData, +) -> HamiltonianTomographyCRLengthResults: + """Post-processing function for HamiltonianTomographyCRLength. + + We fit the expectation values using the Eq. S10 from the paper https://arxiv.org/pdf/2303.01427. + Afterwards, we extract the Hamiltonian terms from the fitted parameters. + + """ + fitted_parameters = tomography_cr_fit( + data=data, + ) + hamiltonian_terms = {} + for pair in data.pairs: + hamiltonian_terms |= extract_hamiltonian_terms( + pair=pair, fitted_parameters=fitted_parameters + ) + + return HamiltonianTomographyCRLengthResults( + hamiltonian_terms=hamiltonian_terms, + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: HamiltonianTomographyCRLengthData, + target: QubitPairId, + fit: HamiltonianTomographyCRLengthResults, +): + """Plotting function for HamiltonianTomographyCRLength.""" + figs, fitting_report = tomography_cr_plot(data, target, fit) + figs[0].update_layout( + xaxis3_title="CR pulse length [ns]", + ) + if fit is not None: + fitting_report = table_html( + table_dict( + 6 * [target], + [f"{term.name} [MHz]" for term in HamiltonianTerm], + [ + fit.hamiltonian_terms[target[0], target[1], term] * kilo + for term in HamiltonianTerm + ], + ) + ) + else: + fitting_report = "" + return figs, fitting_report + + +hamiltonian_tomography_cr_length = Routine(_acquisition, _fit, _plot) +"""HamiltonianTomography Routine object.""" diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/utils.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/utils.py new file mode 100644 index 0000000000..65f95cf10f --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/hamiltonian_tomography/utils.py @@ -0,0 +1,422 @@ +from enum import Enum +from typing import Optional, Union + +import numpy as np +import plotly.graph_objects as go +from plotly.subplots import make_subplots +from scipy.optimize import curve_fit + +from .....auto.operation import QubitId, QubitPairId +from .....config import log +from ....utils import fallback_period, guess_period +from ..utils import Basis, SetControl +from . import fitting + +EPS = 1e-15 + + +class HamiltonianTerm(str, Enum): + """Hamiltonian terms for CR effective Hamiltonian.""" + + IX = "IX" + IY = "IY" + IZ = "IZ" + ZX = "ZX" + ZY = "ZY" + ZZ = "ZZ" + + +def tomography_cr_fit( + data: Union[ + "HamiltonianTomographyCRLengthData", # noqa: F821 + "HamiltonianTomographyCRAmplitudeData", # noqa: F821 + ], +) -> dict[tuple[QubitId, QubitId, Basis, SetControl], list]: + """Perform fitting on expectation values for CR tomography. + + We first fit the Z expectation value to get the frequency of the CR pulse. + We then fit both the X and Y component. + Finally we perform a simultaneous fit all three components taking into account + constraint on the parameters. + """ + fitted_parameters = {} + for pair in data.pairs: + for setup in SetControl: + pair_data = data.data[pair[0], pair[1], Basis.Z, setup] + period = fallback_period(guess_period(pair_data.x, pair_data.prob_target)) + omega = 2 * np.pi / period + pguess = [ + omega / np.sqrt(2), + omega / np.sqrt(2), + 0, + omega, + ] + try: + popt, _ = curve_fit( + fitting.fit_Z_exp, + pair_data.x, + pair_data.prob_target, + maxfev=int(1e6), + p0=pguess, + sigma=pair_data.error_target, + bounds=( + [-5 * omega, -5 * omega, -5 * omega, 0], + [ + 5 * omega, + 5 * omega, + 5 * omega, + 5 * omega, + ], + ), + ) + fitted_parameters[pair[0], pair[1], Basis.Z, setup] = popt.tolist() + except Exception as e: # pragma: no cover + log.warning(f"CR Z fit failed for pair {pair} due to {e}.") + + for pair in data.pairs: + for setup in SetControl: + pair_data = data.data[pair[0], pair[1], Basis.X, setup] + omega = fitted_parameters[pair[0], pair[1], Basis.Z, setup][3] + pguess = [0, 0, 0, omega] + + try: + popt, _ = curve_fit( + fitting.fit_X_exp, + pair_data.x, + pair_data.prob_target, + maxfev=int(1e6), + p0=pguess, + sigma=pair_data.error_target, + absolute_sigma=True, + bounds=( + [-omega, -omega, -omega, 0.99 * omega], + [ + omega, + omega, + omega, + 1.01 * omega, + ], + ), + ) + fitted_parameters[pair[0], pair[1], Basis.X, setup] = popt.tolist() + except Exception as e: # pragma: no cover + log.warning(f"CR fit failed X for pair {pair} due to {e}.") + + for pair in data.pairs: + for setup in SetControl: + pair_data = data.data[pair[0], pair[1], Basis.Y, setup] + omega = fitted_parameters[pair[0], pair[1], Basis.Z, setup][3] + pguess = [0, 0, 0, omega] + try: + popt, _ = curve_fit( + fitting.fit_Y_exp, + pair_data.x, + pair_data.prob_target, + maxfev=int(1e6), + sigma=pair_data.error_target, + absolute_sigma=True, + bounds=( + [-omega, -omega, -omega, 0.99 * omega], + [ + omega, + omega, + omega, + 1.01 * omega, + ], + ), + ) + fitted_parameters[pair[0], pair[1], Basis.Y, setup] = popt.tolist() + except Exception as e: # pragma: no cover + log.warning(f"CR Y fit failed for pair {pair} due to {e}.") + + for pair in data.pairs: + for setup in SetControl: + fitted_parameters[pair[0], pair[1], setup] = fitted_parameters[ + pair[0], pair[1], Basis.Y, setup + ][:3] + pguess = fitted_parameters[pair[0], pair[1], setup] + popt, _ = curve_fit( + fitting.combined_fit, + np.concatenate([pair_data.x, pair_data.x, pair_data.x]), + np.concatenate( + [ + data.data[pair[0], pair[1], Basis.X, setup].prob_target, + data.data[pair[0], pair[1], Basis.Y, setup].prob_target, + data.data[pair[0], pair[1], Basis.Z, setup].prob_target, + ] + ), + maxfev=int(1e6), + p0=pguess, + sigma=np.concatenate( + [ + data.data[pair[0], pair[1], Basis.X, setup].error_target, + data.data[pair[0], pair[1], Basis.Y, setup].error_target, + data.data[pair[0], pair[1], Basis.Z, setup].error_target, + ] + ), + ) + fitted_parameters[pair[0], pair[1], setup] = popt.tolist() + return fitted_parameters + + +def extract_hamiltonian_terms(pair: QubitPairId, fitted_parameters: dict) -> dict: + """Extract Hamiltonian terms from fitted parameters. + + We follow the procedure presented in the paper https://arxiv.org/pdf/2303.01427. + """ + hamiltonian_terms = {} + hamiltonian_terms[pair[0], pair[1], HamiltonianTerm.ZX] = 0.5 * ( + fitted_parameters[pair[0], pair[1], SetControl.Id][0] + - fitted_parameters[pair[0], pair[1], SetControl.X][0] + ) + hamiltonian_terms[pair[0], pair[1], HamiltonianTerm.IX] = 0.5 * ( + fitted_parameters[pair[0], pair[1], SetControl.Id][0] + + fitted_parameters[pair[0], pair[1], SetControl.X][0] + ) + hamiltonian_terms[pair[0], pair[1], HamiltonianTerm.ZY] = 0.5 * ( + fitted_parameters[pair[0], pair[1], SetControl.Id][1] + - fitted_parameters[pair[0], pair[1], SetControl.X][1] + ) + hamiltonian_terms[pair[0], pair[1], HamiltonianTerm.IY] = 0.5 * ( + fitted_parameters[pair[0], pair[1], SetControl.Id][1] + + fitted_parameters[pair[0], pair[1], SetControl.X][1] + ) + hamiltonian_terms[pair[0], pair[1], HamiltonianTerm.ZZ] = 0.5 * ( + fitted_parameters[pair[0], pair[1], SetControl.Id][2] + - fitted_parameters[pair[0], pair[1], SetControl.X][2] + ) + hamiltonian_terms[pair[0], pair[1], HamiltonianTerm.IZ] = 0.5 * ( + fitted_parameters[pair[0], pair[1], SetControl.Id][2] + + fitted_parameters[pair[0], pair[1], SetControl.X][2] + ) + return hamiltonian_terms + + +def compute_total_expectation_value( + data: Union[ + "HamiltonianTomographyCRLengthData", # noqa: F821 + "HamiltonianTomographyCRAmplitudeData", # noqa: F821 + ], + pair: QubitPairId, +): + tot_exp_vals = [] + for basis in Basis: + tot_exp_vals.append( + data.data[pair[0], pair[1], basis, SetControl.Id].prob_target + + data.data[pair[0], pair[1], basis, SetControl.X].prob_target + ) + return np.vstack(tot_exp_vals) + + +def bloch_func(x, pair: QubitPairId, fitted_parameters: dict): + x = np.vstack([x, x, x]) + id_blochfit = fitting.combined_fit( + x, *fitted_parameters[pair[0], pair[1], SetControl.Id] + ).reshape((3, -1)) + x_blochfit = fitting.combined_fit( + x, *fitted_parameters[pair[0], pair[1], SetControl.X] + ).reshape((3, -1)) + return np.sqrt(np.sum((id_blochfit + x_blochfit) ** 2, axis=0)) + + +def compute_bloch_vector( + data: Union[ + "HamiltonianTomographyCRLengthData", # noqa: F821 + "HamiltonianTomographyCRAmplitudeData", # noqa: F821 + ], + pair: QubitPairId, + fitted_parameters: dict = None, +): + bloch_exp = compute_total_expectation_value(data, pair) + bloch_exp = np.sqrt(np.sum((bloch_exp) ** 2, axis=0)) + + bloch_fit = None + if fitted_parameters is not None: + times = data.data[pair[0], pair[1], Basis.Z, SetControl.Id].x + times_range = np.linspace(min(times), max(times), 2 * len(times)) + + bloch_fit = bloch_func(times_range, pair, fitted_parameters) + + return bloch_exp, bloch_fit + + +def tomography_cr_plot( + data: Union[ + "HamiltonianTomographyCRLengthData", # noqa: F821 + "HamiltonianTomographyCRAmplitudeData", # noqa: F821 + ], + target: QubitPairId, + fit: Optional[ + Union[ + "HamiltonianTomographyCRLengthResults", # noqa: F821 + "HamiltonianTomographyCRAmplitudeResults", # noqa: F821 + ] + ] = None, +) -> tuple[list[go.Figure], str]: + """Plotting function for HamiltonianTomographyCRLength.""" + fig = make_subplots( + rows=4, + cols=1, + horizontal_spacing=0.1, + vertical_spacing=0.05, + shared_xaxes=True, + shared_yaxes=True, + ) + for i, basis in enumerate(Basis): + for setup in SetControl: + target = target if target in data.pairs else (target[1], target[0]) + pair_data = data.data[target[0], target[1], basis, setup] + fig.add_trace( + go.Scatter( + x=pair_data.x, + y=pair_data.prob_target, + name=f"Target when Control at {0 if setup is SetControl.Id else 1}", + showlegend=True if basis is Basis.Z else False, + legendgroup=f"Target when Control at {0 if setup is SetControl.Id else 1}", + mode="markers", + marker=dict(color="blue" if setup is SetControl.Id else "red"), + error_y=dict( + type="data", + array=pair_data.error_target, + visible=True, + ), + ), + row=i + 1, + col=1, + ) + if fit is not None: + x = np.linspace(pair_data.x.min(), pair_data.x.max(), 100) + if basis == Basis.Z: + fig.add_trace( + go.Scatter( + x=x, + y=fitting.fit_Z_exp( + x, + *fit.fitted_parameters[ + target[0], target[1], basis, setup + ], + ), + name=f"Single target when control at {0 if setup is SetControl.Id else 1}", + showlegend=True if basis is Basis.Z else False, + legendgroup=f"Single target when control at {0 if setup is SetControl.Id else 1}", + mode="lines", + line=dict( + color="blue" if setup is SetControl.Id else "red", + ), + ), + row=i + 1, + col=1, + ) + elif basis == Basis.X: + fig.add_trace( + go.Scatter( + x=x, + y=fitting.fit_X_exp( + x, + *fit.fitted_parameters[ + target[0], target[1], basis, setup + ], + ), + name=f"Single target when control at {0 if setup is SetControl.Id else 1}", + showlegend=True if basis is Basis.Z else False, + legendgroup=f"Single target when control at {0 if setup is SetControl.Id else 1}", + mode="lines", + line=dict( + color="blue" if setup is SetControl.Id else "red", + ), + ), + row=i + 1, + col=1, + ) + elif basis == Basis.Y: + fig.add_trace( + go.Scatter( + x=x, + y=fitting.fit_Y_exp( + x, + *fit.fitted_parameters[ + target[0], target[1], basis, setup + ], + ), + name=f"Single target when control at {0 if setup is SetControl.Id else 1}", + showlegend=True if basis is Basis.Z else False, + legendgroup=f"Single target when control at {0 if setup is SetControl.Id else 1}", + mode="lines", + line=dict( + color="blue" if setup is SetControl.Id else "red", + ), + ), + row=i + 1, + col=1, + ) + + fig.add_trace( + go.Scatter( + x=x, + y=getattr(fitting, f"fit_{basis.name}_exp")( + x, + wx=fit.fitted_parameters[target[0], target[1], setup][0], + wy=fit.fitted_parameters[target[0], target[1], setup][1], + wz=fit.fitted_parameters[target[0], target[1], setup][2], + w=np.sqrt( + np.sum( + i**2 + for i in fit.fitted_parameters[ + target[0], target[1], setup + ] + ) + ), + ), + name=f"Simultaneous Fit of target when control at {0 if setup is SetControl.Id else 1}", + showlegend=True if basis is Basis.Z else False, + legendgroup=f"Simultaneous Fit target when control at {0 if setup is SetControl.Id else 1}", + mode="lines", + line=dict( + color="green" if setup is SetControl.Id else "orange", + ), + ), + row=i + 1, + col=1, + ) + + bloch_vect, bloch_fit = compute_bloch_vector(data, target, fit.fitted_parameters) + fig.add_trace( + go.Scatter( + x=pair_data.x, + y=bloch_vect, + name="Bloch vector |R(t)|", + legendgroup="Bloch vector |R(t)|", + showlegend=True, + mode="markers", + ), + row=4, + col=1, + ) + if bloch_fit is not None: + x = np.linspace(pair_data.x.min(), pair_data.x.max(), len(bloch_fit)) + fig.add_trace( + go.Scatter( + x=x, + y=bloch_fit, + name="Fitted Bloch vector |R(t)|", + showlegend=True, + legendgroup="Fitted Bloch vector |R(t)|", + mode="lines", + ), + row=4, + col=1, + ) + + fig.update_layout( + yaxis1=dict(range=[-1.2, 1.2]), + yaxis2=dict(range=[-1.2, 1.2]), + yaxis3=dict(range=[-1.2, 1.2]), + yaxis4=dict(range=[-0.2, 2.2]), + height=800, + ) + fig.update_yaxes(title_text="", row=1, col=1) + fig.update_yaxes(title_text="", row=2, col=1) + fig.update_yaxes(title_text="", row=3, col=1) + fig.update_yaxes(title_text="|R(t)|", row=4, col=1) + + return [fig], "" diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/length.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/length.py new file mode 100644 index 0000000000..e1d3b7f9e6 --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/length.py @@ -0,0 +1,284 @@ +"""Protocol to measure CR interaction varying drive amplitude.""" + +from dataclasses import dataclass, field + +import numpy as np +import numpy.typing as npt +from qibolab import ( + AcquisitionType, + AveragingMode, + Parameter, + Sweeper, +) +from scipy.constants import kilo + +from ....auto.operation import ( + Data, + Parameters, + QubitId, + QubitPairId, + Results, + Routine, +) +from ....calibration import CalibrationPlatform +from ....result import probability +from ...rabi.utils import fit_length_function, rabi_length_function +from ...utils import table_dict, table_html +from .utils import SetControl, cr_fit, cr_plot, cr_sequence + +CrossResonanceLengthType = np.dtype( + [ + ("prob_target", np.float64), + ("error_target", np.float64), + ("prob_control", np.float64), + ("error_control", np.float64), + ("x", np.int64), + ] +) +"""Custom dtype for CR length.""" + + +@dataclass +class CrossResonanceLengthParameters(Parameters): + """CrossResonanceLength runcard inputs.""" + + pulse_duration_start: float + """Initial pi pulse duration [ns].""" + pulse_duration_end: float + """Final pi pulse duration [ns].""" + pulse_duration_step: float + """Step pi pulse duration [ns].""" + pulse_amplitude: float + """CR pulse amplitude""" + interpolated_sweeper: bool = False + """Use real-time interpolation if supported by instruments.""" + echo: bool = False + """Apply echo sequence or not.""" + + @property + def duration_range(self): + return np.arange( + self.pulse_duration_start, self.pulse_duration_end, self.pulse_duration_step + ) + + +@dataclass +class CrossResonanceLengthResults(Results): + """CrossResonanceLength outputs.""" + + effective_coupling: dict[tuple[QubitId, QubitId], float] = field( + default_factory=dict + ) + fitted_parameters: dict[tuple[QubitPairId, str], list] = field(default_factory=dict) + + def __contains__(self, pair: QubitPairId): + return all(key[:2] == pair for key in list(self.fitted_parameters)) + + +@dataclass +class CrossResonanceLengthData(Data): + """Data structure for CR length.""" + + anharmonicity: dict[QubitPairId, float] = field(default_factory=dict) + detuning: dict[QubitPairId, float] = field(default_factory=dict) + data: dict[tuple[QubitId, QubitId, str], npt.NDArray[CrossResonanceLengthType]] = ( + field(default_factory=dict) + ) + """Raw data acquired.""" + + @property + def pairs(self): + return {(i[0], i[1]) for i in self.data} + + +def _acquisition( + params: CrossResonanceLengthParameters, + platform: CalibrationPlatform, + targets: list[QubitPairId], +) -> CrossResonanceLengthData: + """Data acquisition for cross resonance protocol.""" + + data = CrossResonanceLengthData() + + for pair in targets: + control, target = pair + pair = (control, target) + data.detuning[pair] = ( + platform.config(platform.qubits[control].drive).frequency + - platform.config(platform.qubits[target].drive).frequency + ) + data.anharmonicity[pair] = platform.calibration.single_qubits[ + control + ].qubit.anharmonicity + for setup in SetControl: + sequence, cr_pulses, cr_target_pulses, delays = cr_sequence( + platform=platform, + control=control, + target=target, + setup=setup, + amplitude=params.pulse_amplitude, + duration=params.pulse_duration_end, + interpolated_sweeper=params.interpolated_sweeper, + echo=params.echo, + ) + + if params.interpolated_sweeper: + sweeper = Sweeper( + parameter=Parameter.duration_interpolated, + values=params.duration_range, + pulses=cr_pulses + cr_target_pulses, + ) + else: + sweeper = Sweeper( + parameter=Parameter.duration, + values=params.duration_range, + pulses=cr_pulses + cr_target_pulses + delays, + ) + + try: + cr_frequency = { + platform.qubits[control].drive_extra[target]: { + "frequency": platform.config( + platform.qubits[target].drive + ).frequency + } + } + target_offset_sweeper = control_offset_sweeper = None + except Exception: + cr_frequency = { + platform.qubits[control].drive_extra[(1, 2)]: { + "frequency": platform.config( + platform.qubits[target].drive + ).frequency + } + } + target_channel = platform.qubits[target].flux + target_offset = platform.config(target_channel).offset + target_offset_sweeper = Sweeper( + parameter=Parameter.offset, + values=np.array([target_offset]), + channels=[target_channel], + ) + control_channel = platform.qubits[control].flux + control_offset = platform.config(control_channel).offset + control_offset_sweeper = Sweeper( + parameter=Parameter.offset, + values=np.array([control_offset]), + channels=[control_channel], + ) + + updates = [] + updates.append(cr_frequency) + + if target_offset_sweeper is not None: + # execute the sweep + results = platform.execute( + [sequence], + [[target_offset_sweeper], [control_offset_sweeper], [sweeper]], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.SINGLESHOT, + updates=updates, + ) + else: + # execute the sweep + results = platform.execute( + [sequence], + [[sweeper]], + nshots=params.nshots, + relaxation_time=params.relaxation_time, + acquisition_type=AcquisitionType.DISCRIMINATION, + averaging_mode=AveragingMode.SINGLESHOT, + updates=updates, + ) + + target_acq_handle = list( + sequence.channel(platform.qubits[target].acquisition) + )[-1].id + control_acq_handle = list( + sequence.channel(platform.qubits[control].acquisition) + )[-1].id + prob_target = probability(results[target_acq_handle], state=1) + prob_control = probability(results[control_acq_handle], state=1) + + if target_offset_sweeper is not None: + prob_target = prob_target[0][0] + prob_control = prob_control[0][0] + import rich + + rich.print(prob_target) + rich.print(prob_control) + data.register_qubit( + CrossResonanceLengthType, + (control, target, setup), + dict( + x=sweeper.values, + prob_target=prob_target, + error_target=np.sqrt( + prob_target * (1 - prob_target) / params.nshots + ).tolist(), + prob_control=prob_control, + error_control=np.sqrt( + prob_control * (1 - prob_control) / params.nshots + ).tolist(), + ), + ) + # finally, save the remaining data + return data + + +def _fit( + data: CrossResonanceLengthData, +) -> CrossResonanceLengthResults: + """Post-processing function for CrossResonanceLength. + + After fitting the data with dumped cosine function, the effective coupling + is computed as specified in https://arxiv.org/pdf/1905.11480. + + """ + fitted_parameters = cr_fit(data=data, fitting_function=fit_length_function) + effective_coupling = {} + for pair in data.pairs: + try: + effective_coupling[pair] = ( + 1 / fitted_parameters[pair[0], pair[1], SetControl.X][2] + - 1 / fitted_parameters[pair[0], pair[1], SetControl.Id][2] + ) / 2 + except KeyError: # pragma: no cover + pass + return CrossResonanceLengthResults( + effective_coupling=effective_coupling, + fitted_parameters=fitted_parameters, + ) + + +def _plot( + data: CrossResonanceLengthData, + target: QubitPairId, + fit: CrossResonanceLengthResults, +): + """Plotting function for CrossResonanceLength.""" + figs, fitting_report = cr_plot( + data=data, target=target, fit=fit, fitting_function=rabi_length_function + ) + if fit is not None: + fitting_report = table_html( + table_dict( + [target], + [ + "Effective coupling [MHz]", + ], + [fit.effective_coupling[target] * kilo], + ) + ) + + figs[0].update_layout( + xaxis_title="Cross resonance pulse duration [ns]", + yaxis_title="Excited state population", + ) + return figs, fitting_report + + +cross_resonance_length = Routine(_acquisition, _fit, _plot) +"""CrossResonance Routine object.""" diff --git a/src/qibocal/protocols/two_qubit_interaction/cross_resonance/utils.py b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/utils.py new file mode 100644 index 0000000000..d866008ec4 --- /dev/null +++ b/src/qibocal/protocols/two_qubit_interaction/cross_resonance/utils.py @@ -0,0 +1,283 @@ +from enum import Enum +from typing import Callable, Optional, Union + +import numpy as np +import plotly.graph_objects as go +from qibolab import Delay, Platform, Pulse, PulseSequence, Rectangular + +from ....auto.operation import QubitId, QubitPairId +from ....config import log +from ....update import replace +from ...utils import fallback_period, guess_period + + +class SetControl(str, Enum): + """Helper to create sequence with control set to X or I.""" + + Id = "Id" + X = "X" + + +class Basis(str, Enum): + """Measurement basis.""" + + X = "X" + Y = "Y" + Z = "Z" + + +def cr_sequence( + platform: Platform, + control: QubitId, + target: QubitId, + setup: SetControl, + amplitude: float, + duration: int, + target_amplitude: float = 0, + target_phase: float = 0, + interpolated_sweeper: bool = False, + echo: bool = False, + basis: Basis = Basis.Z, + phase: float = 0, +) -> tuple[PulseSequence, list[Pulse], list[Pulse], list[Delay]]: + """Creates sequence for CR experiment on ``control`` and ``target`` qubits. + + With ``setup`` it is possible to set the control qubit to 1 or keep it at 0. + If ``echo`` is set to ``True`` a ECR gate will be played. + With ``basis`` it is possible to set the measurement basis. If it is not provided + the default is Z.""" + + cr_pulses = [] + cr_target_pulses = [] + sequence = PulseSequence() + natives_control = platform.natives.single_qubit[control] + natives_target = platform.natives.single_qubit[target] + try: + cr_channel = platform.qubits[control].drive_extra[target] + except Exception: + cr_channel = platform.qubits[control].drive_extra[(1, 2)] + + cr_drive_pulse = Pulse( + duration=duration, + amplitude=amplitude, + relative_phase=phase, + # envelope=GaussianSquare(rel_sigma=0.2, risefall=15), + envelope=Rectangular(), + ) + target_drive_pulse = Pulse( + duration=duration, + amplitude=target_amplitude, + relative_phase=target_phase, + # envelope=GaussianSquare(rel_sigma=0.2, risefall=15), + envelope=Rectangular(), + ) + cr_pulses.append(cr_drive_pulse) + cr_target_pulses.append(target_drive_pulse) + control_drive_channel, control_drive_pulse = natives_control.RX()[0] + target_drive_channel, _ = natives_target.RX()[0] + ro_channel, ro_pulse = natives_target.MZ()[0] + ro_channel_control, ro_pulse_control = natives_control.MZ()[0] + if setup == SetControl.X: + control_delay = Delay(duration=control_drive_pulse.duration) + sequence.append((control_drive_channel, control_drive_pulse)) + sequence.append((target_drive_channel, control_delay)) + sequence.append((ro_channel, control_delay)) + sequence.append((ro_channel_control, control_delay)) + sequence.append((cr_channel, control_delay)) + + if echo: + delays = 6 * [Delay(duration=cr_drive_pulse.duration)] + control_delay = Delay(duration=control_drive_pulse.duration) + cr_pulse_minus = replace(cr_drive_pulse, relative_phase=np.pi) + target_pulse_minus = replace(target_drive_pulse, relative_phase=np.pi) + cr_pulses.append(cr_pulse_minus) + cr_target_pulses.append(target_pulse_minus) + sequence.append((cr_channel, cr_drive_pulse)) + sequence.append((control_drive_channel, delays[-1])) + sequence.append((target_drive_channel, target_drive_pulse)) + sequence.append((control_drive_channel, control_drive_pulse)) + sequence.append((cr_channel, control_delay)) + sequence.append((target_drive_channel, control_delay)) + sequence.append((cr_channel, cr_pulse_minus)) + sequence.append((control_drive_channel, delays[-2])) + sequence.append((target_drive_channel, target_pulse_minus)) + sequence.append((control_drive_channel, control_drive_pulse)) + sequence.append((target_drive_channel, control_delay)) + + else: + delays = 2 * [Delay(duration=cr_drive_pulse.duration)] + sequence.append((cr_channel, cr_drive_pulse)) + sequence.append((target_drive_channel, target_drive_pulse)) + + if interpolated_sweeper: + sequence.align( + [ + cr_channel, + target_drive_channel, + control_drive_channel, + ro_channel, + ro_channel_control, + ] + ) + else: + sequence.append((ro_channel, delays[0])) + sequence.append((ro_channel_control, delays[1])) + if echo: + sequence.append((ro_channel, delays[2])) + sequence.append((ro_channel_control, delays[3])) + sequence.append( + (ro_channel, Delay(duration=2 * control_drive_pulse.duration)) + ) + sequence.append( + (ro_channel_control, Delay(duration=2 * control_drive_pulse.duration)) + ) + + if basis == Basis.X: + sequence.append( + ( + target_drive_channel, + natives_target.R(theta=np.pi / 2, phi=np.pi / 2)[0][1], + ) + ) + elif basis == Basis.Y: + sequence.append( + ( + target_drive_channel, + natives_target.R(theta=np.pi / 2, phi=0)[0][1], + ) + ) + + target_delay = Delay( + duration=natives_target.R(theta=np.pi / 2, phi=np.pi / 2)[0][1].duration + ) + sequence.append((ro_channel, target_delay)) + sequence.append((ro_channel_control, target_delay)) + sequence.append((ro_channel, ro_pulse)) + sequence.append((ro_channel_control, ro_pulse_control)) + return sequence, cr_pulses, cr_target_pulses, delays + + +def cr_fit( + data: Union[ + "CrossResonanceLengthData", # noqa: F821 + "CrossResonanceAmplitudeData", # noqa: F821 + ], + fitting_function: Callable, +) -> dict[tuple[QubitId, QubitId, SetControl], list]: + """Perform fitting on CR data for probabilities. + + We fit oscillations observed in the target qubit. Using a cosine function. + When on the x axis we change the duration of the CR pulse we include an exponential + term to address the relaxation time of the qubit. + """ + fitted_parameters = {} + for pair in data.pairs: + for setup in SetControl: + pair_data = data[pair[0], pair[1], setup] + pair = (pair[0], pair[1]) + raw_x = pair_data.x + min_x = np.min(raw_x) + max_x = np.max(raw_x) + y = pair_data.prob_target + x = (raw_x - min_x) / (max_x - min_x) + + period = fallback_period(guess_period(x, y)) + pguess = ( + [0, 0.5, period, 0, 0] + if fitting_function.__name__ == "fit_length_function" + else [0, 0.5, period, 0] + ) + + try: + popt, _, _ = fitting_function( + x, + y, + pguess, + sigma=pair_data.error_target, + signal=False, + x_limits=(min_x, max_x), + ) + fitted_parameters[pair[0], pair[1], setup] = popt + except Exception as e: # pragma: no cover + log.warning(f"CR fit failed for pair {pair} due to {e}.") + return fitted_parameters + + +def cr_plot( + data: Union[ + "CrossResonanceLengthData", # noqa: F821 + "CrossResonanceAmplitudeData", # noqa: F821 + ], + target: QubitPairId, + fit: Optional[ + Union[ + "CrossResonanceLengthResults", # noqa: F821 + "CrossResonanceAmplitudeResults", # noqa: F821 + ] + ] = None, + fitting_function: Optional[Callable] = None, +) -> tuple[list[go.Figure], str]: + """Plotting function for CR protocols.""" + fig = go.Figure() + for setup in SetControl: + target = target if target in data.pairs else (target[1], target[0]) + pair_data = data.data[target[0], target[1], setup] + fig.add_trace( + go.Scatter( + x=pair_data.x, + y=pair_data.prob_target, + name=f"Target when Control at {0 if setup is SetControl.Id else 1}", + showlegend=True, + legendgroup=f"Target when Control at {0 if setup is SetControl.Id else 1}", + mode="markers", + marker=dict(color="blue" if setup is SetControl.Id else "red"), + error_y=dict( + type="data", + array=pair_data.error_target, + visible=True, + ), + ) + ) + fig.add_trace( + go.Scatter( + x=pair_data.x, + y=pair_data.prob_control, + name=f"Control at {0 if setup is SetControl.Id else 1}", + showlegend=True, + legendgroup=f"Control at {0 if setup is SetControl.Id else 1}", + mode="markers", + marker=dict(color="green" if setup is SetControl.Id else "orange"), + error_y=dict( + type="data", + array=pair_data.error_control, + visible=True, + ), + ) + ) + if fit is not None: + if (target[0], target[1], setup) in fit.fitted_parameters: + x = np.linspace(pair_data.x.min(), pair_data.x.max(), 100) + fig.add_trace( + go.Scatter( + x=x, + y=fitting_function( + x, + *fit.fitted_parameters[target[0], target[1], setup], + ), + name=f"Fit target when control at {0 if setup is SetControl.Id else 1}", + showlegend=True, + legendgroup=f"Fit target when control at {0 if setup is SetControl.Id else 1}", + mode="lines", + line=dict( + color="blue" if setup is SetControl.Id else "red", + ), + ) + ) + + fig.update_layout( + yaxis1=dict(range=[-0.1, 1.1]), + yaxis2=dict(range=[-0.1, 1.1]), + yaxis3=dict(range=[-0.1, 1.1]), + height=600, + ) + return [fig], "" diff --git a/tests/platforms/mock/parameters.json b/tests/platforms/mock/parameters.json index adb4254e6f..ce760c03d1 100644 --- a/tests/platforms/mock/parameters.json +++ b/tests/platforms/mock/parameters.json @@ -14,6 +14,10 @@ "kind": "iq", "frequency": 4000000000.0 }, + "01/drive": { + "kind": "iq", + "frequency": 4200000000.0 + }, "1/drive": { "kind": "iq", "frequency": 4200000000.0 diff --git a/tests/platforms/mock/platform.py b/tests/platforms/mock/platform.py index c324b924d5..5457c97136 100644 --- a/tests/platforms/mock/platform.py +++ b/tests/platforms/mock/platform.py @@ -21,19 +21,28 @@ def create_mock_hardware() -> Hardware: channels = {} # attach the channels pump_name = "twpa_pump" - for q in range(2): - drive12 = f"{q}/drive12" - qubits[q] = qubit = Qubit.default(q, drive_extra={(1, 2): drive12}) - channels |= { - qubit.probe: IqChannel(mixer=None, lo="01/probe_lo"), - qubit.acquisition: AcquisitionChannel( - twpa_pump=pump_name, probe=qubit.probe - ), - qubit.drive: IqChannel(mixer=None, lo=f"{q}/drive_lo"), - drive12: IqChannel(mixer=None, lo=f"{q}/drive_lo"), - qubit.flux: DcChannel(), - } - + qubits[0] = Qubit.default(0, drive_extra={(1, 2): "0/drive12", 1: "01/drive"}) + + channels |= { + qubits[0].probe: IqChannel(mixer=None, lo="01/probe_lo"), + qubits[0].acquisition: AcquisitionChannel( + twpa_pump=pump_name, probe=qubits[0].probe + ), + qubits[0].drive: IqChannel(mixer=None, lo="0/drive_lo"), + qubits[0].drive_extra[1, 2]: IqChannel(mixer=None, lo="0/drive_lo"), + qubits[0].drive_extra[1]: IqChannel(mixer=None, lo="0/drive_lo"), + qubits[0].flux: DcChannel(), + } + qubits[1] = Qubit.default(1, drive_extra={(1, 2): "0/drive12"}) + channels |= { + qubits[1].probe: IqChannel(mixer=None, lo="01/probe_lo"), + qubits[1].acquisition: AcquisitionChannel( + twpa_pump=pump_name, probe=qubits[1].probe + ), + qubits[1].drive: IqChannel(mixer=None, lo="1/drive_lo"), + qubits[1].drive_extra[1, 2]: IqChannel(mixer=None, lo="1/drive_lo"), + qubits[1].flux: DcChannel(), + } couplers = {} couplers["01"] = coupler = Qubit(flux="coupler_01/flux") channels |= {coupler.flux: DcChannel()} diff --git a/tests/runcards/protocols.yml b/tests/runcards/protocols.yml index 8eb44c7842..a0cae77b32 100644 --- a/tests/runcards/protocols.yml +++ b/tests/runcards/protocols.yml @@ -1052,3 +1052,57 @@ actions: twpa_pow_width: 10 twpa_pow_step: 3 nshots: 20000 + + - id: cross_resonance amp + operation: cross_resonance_amplitude + targets: [[0, 1]] + parameters: + min_amp: 0.01 + max_amp: 0.1 + step_amp: 0.01 + echo: true + nshots: 2000 + pulse_duration: 10 + + - id: cross_resonance length + operation: cross_resonance_length + targets: [[0, 1]] + parameters: + pulse_duration_start: 10 + pulse_duration_end: 20 + pulse_duration_step: 1 + pulse_amplitude: 0.1 + nshots: 2000 + + - id: cross_resonance length interpolated + operation: cross_resonance_length + targets: [[0, 1]] + parameters: + pulse_duration_start: 10 + pulse_duration_end: 20 + pulse_duration_step: 1 + nshots: 2000 + pulse_amplitude: 0.1 + interpolated_sweeper: true + + - id: CR tomography + operation: hamiltonian_tomography_cr_length + targets: [[0, 1]] + parameters: + pulse_duration_start: 10 + pulse_duration_end: 20 + pulse_duration_step: 1 + nshots: 2000 + pulse_amplitude: 0.1 + + + - id: CR tomography interpolated + operation: hamiltonian_tomography_cr_length + targets: [[0, 1]] + parameters: + pulse_duration_start: 10 + pulse_duration_end: 20 + pulse_duration_step: 1 + nshots: 2000 + pulse_amplitude: 0.1 + interpolated_sweeper: true