diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8896d83bfa..2e3810a89b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: isort args: ["--profile", "black"] - repo: https://github.com/PyCQA/docformatter - rev: v1.7.5 + rev: master hooks: - id: docformatter additional_dependencies: [tomli] diff --git a/crate/src/lib.rs b/crate/src/lib.rs index 3238e6acb6..be73f64552 100644 --- a/crate/src/lib.rs +++ b/crate/src/lib.rs @@ -3,7 +3,7 @@ use numpy::PyArray2; use pyo3::prelude::*; use pyo3::types::PyDict; -pub fn execute_qasm(circuit: String, platform: String, nshots: u32) -> PyResult> { +pub fn execute_qasm(circuit: String, platform: String, nshots: u32) -> PyResult> { Python::with_gil(|py| { let kwargs = PyDict::new(py); kwargs.set_item("circuit", circuit)?; @@ -11,7 +11,7 @@ pub fn execute_qasm(circuit: String, platform: String, nshots: u32) -> PyResult< kwargs.set_item("nshots", nshots)?; let qibolab = PyModule::import(py, "qibolab")?; - let pyarray: &PyArray2 = qibolab + let pyarray: &PyArray2 = qibolab .getattr("execute_qasm")? .call((), Some(kwargs))? .call_method0("samples")? diff --git a/doc/source/tutorials/compiler.rst b/doc/source/tutorials/compiler.rst index a445e8e7f1..b6ae145d0a 100644 --- a/doc/source/tutorials/compiler.rst +++ b/doc/source/tutorials/compiler.rst @@ -80,11 +80,10 @@ The following example shows how to modify the compiler in order to execute a cir # define a compiler rule that translates X to the pi-pulse - def x_rule(gate, platform): + def x_rule(qubits_ids, platform, parameters=None): """X gate applied with a single pi-pulse.""" - qubit = gate.target_qubits[0] sequence = PulseSequence() - sequence.add(platform.create_RX_pulse(qubit, start=0)) + sequence.add(platform.create_RX_pulse(qubits_ids[1][0], start=0)) return sequence, {} diff --git a/poetry.lock b/poetry.lock index 44aadec062..33d033bb9e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -193,39 +193,23 @@ lxml = ["lxml"] [[package]] name = "betterproto" -version = "2.0.0b5" +version = "2.0.0b7" description = "A better Protobuf / gRPC generator & library" optional = false -python-versions = ">=3.6.2,<4.0" +python-versions = "<4.0,>=3.7" files = [ - {file = "betterproto-2.0.0b5-py3-none-any.whl", hash = "sha256:d3e6115c7d5136f1d5974e565b7560273f66b43065e74218e472321ee1258f4c"}, - {file = "betterproto-2.0.0b5.tar.gz", hash = "sha256:00a301c70a2db4d3cdd2b261522ae1d34972fb04b655a154d67daaaf4131102e"}, + {file = "betterproto-2.0.0b7-py3-none-any.whl", hash = "sha256:401ab8055e2f814e77b9c88a74d0e1ae3d1e8a969cced6aeb1b59f71ad63fbd2"}, + {file = "betterproto-2.0.0b7.tar.gz", hash = "sha256:1b1458ca5278d519bcd62556a4c236f998a91d503f0f71c67b0b954747052af2"}, ] [package.dependencies] grpclib = ">=0.4.1,<0.5.0" python-dateutil = ">=2.8,<3.0" +typing-extensions = ">=4.7.1,<5.0.0" [package.extras] -compiler = ["black (>=19.3b0)", "isort (>=5.10.1,<6.0.0)", "jinja2 (>=3.0.3)"] - -[[package]] -name = "betterproto" -version = "2.0.0b6" -description = "A better Protobuf / gRPC generator & library" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "betterproto-2.0.0b6-py3-none-any.whl", hash = "sha256:a0839ec165d110a69d0d116f4d0e2bec8d186af4db826257931f0831dab73fcf"}, - {file = "betterproto-2.0.0b6.tar.gz", hash = "sha256:720ae92697000f6fcf049c69267d957f0871654c8b0d7458906607685daee784"}, -] - -[package.dependencies] -grpclib = ">=0.4.1,<0.5.0" -python-dateutil = ">=2.8,<3.0" - -[package.extras] -compiler = ["black (>=19.3b0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"] +compiler = ["black (>=23.1.0)", "isort (>=5.11.5,<6.0.0)", "jinja2 (>=3.0.3)"] +rust-codec = ["betterproto-rust-codec (==0.1.1)"] [[package]] name = "bleach" @@ -2199,20 +2183,22 @@ files = [ [[package]] name = "marshmallow" -version = "3.0.0" +version = "3.23.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "marshmallow-3.0.0-py2.py3-none-any.whl", hash = "sha256:e5e9fd0c2e919b4ece915eb30808206349a49a45df72e99ed20e27a9053d574b"}, - {file = "marshmallow-3.0.0.tar.gz", hash = "sha256:fa2d8a4b61d09b0e161a14acc5ad8ab7aaaf1477f3dd52819ddd6c6c8275733a"}, + {file = "marshmallow-3.23.1-py3-none-any.whl", hash = "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491"}, + {file = "marshmallow-3.23.1.tar.gz", hash = "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468"}, ] +[package.dependencies] +packaging = ">=17.0" + [package.extras] -dev = ["flake8 (==3.7.8)", "flake8-bugbear (==19.8.0)", "pre-commit (>=1.17,<2.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.12)", "sphinx (==2.1.2)", "sphinx-issues (==1.2.0)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==3.7.8)", "flake8-bugbear (==19.8.0)", "pre-commit (>=1.17,<2.0)"] -tests = ["pytest", "pytz", "simplejson"] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] +docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.14)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"] +tests = ["pytest", "simplejson"] [[package]] name = "marshmallow-polyfield" @@ -2825,6 +2811,7 @@ optional = false python-versions = ">=3.9" files = [ {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, @@ -2838,12 +2825,14 @@ files = [ {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, @@ -4313,64 +4302,52 @@ full = ["pyro4"] [[package]] name = "qm-octave" -version = "2.0.1" +version = "2.1.3" description = "SDK to control an Octave with QUA" optional = false -python-versions = ">=3.7,<3.12" +python-versions = "<3.13,>=3.8" files = [ - {file = "qm_octave-2.0.1-py3-none-any.whl", hash = "sha256:ef2607101b978beb306f30bfc56656f087fe7764133ba22e3ce5739a409c4edb"}, - {file = "qm_octave-2.0.1.tar.gz", hash = "sha256:f20844d98e493a253bedb72f86fcb665bc7461a46d502de3cc7c91f3d54416d6"}, + {file = "qm_octave-2.1.3-py3-none-any.whl", hash = "sha256:1957eadaa6d8e3150c57bb8b378d3d9fbcd9dc66163ace3a317ecb94685e606a"}, + {file = "qm_octave-2.1.3.tar.gz", hash = "sha256:f7981b73dc51276f306e553f9906e40119024067ce7cab79c19f07b53778cc7b"}, ] [package.dependencies] -betterproto = [ - {version = "2.0.0b5", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, - {version = "2.0.0b6", markers = "python_version >= \"3.11\" and python_version < \"4.0\""}, -] +betterproto = "2.0.0b7" grpcio = ">=1.59.2,<2.0.0" grpclib = {version = ">=0.4.3rc3,<0.5.0", markers = "python_version >= \"3.10\" and python_version < \"4.0\""} protobuf = [ - {version = ">=3.17.3,<4.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = ">=3.17.3,<4.0.0", markers = "python_version >= \"3.8\" and python_version < \"3.11\""}, {version = ">=4.24,<5.0", markers = "python_version >= \"3.11\""}, ] [[package]] name = "qm-qua" -version = "1.1.6" +version = "1.2.1" description = "QUA language SDK to control a Quantum Computer" optional = false -python-versions = ">=3.7,<3.12" +python-versions = "<3.13,>=3.8" files = [ - {file = "qm_qua-1.1.6-py3-none-any.whl", hash = "sha256:69f8159805889fe9389b1acb4e94afacc37d68e1455017ec0e58a4fcc238bd8c"}, - {file = "qm_qua-1.1.6.tar.gz", hash = "sha256:9e09240bf1d9623c0f5b15fb2bc955f7387da15280d0b02c266e42a40818a2b1"}, + {file = "qm_qua-1.2.1-py3-none-any.whl", hash = "sha256:3315379d16929468b058adbe489f6ac3979d1a5fcae102d4fc498bb7ac2feded"}, + {file = "qm_qua-1.2.1.tar.gz", hash = "sha256:9a6af3cbab527bd95948c78a640d84d4ec8487ab1e146afd9e38f0ce9f939701"}, ] [package.dependencies] -betterproto = [ - {version = "2.0.0b5", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, - {version = "2.0.0b6", markers = "python_version == \"3.11\""}, -] +betterproto = "2.0.0b7" datadog-api-client = ">=2.6.0,<3.0.0" dependency_injector = ">=4.41.0,<5.0.0" deprecation = ">=2.1.0,<3.0.0" grpcio = [ - {version = ">=1.39.0,<2.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, + {version = ">=1.39.0,<2.0.0", markers = "python_version >= \"3.8\" and python_version < \"3.11\""}, {version = ">=1.57,<2.0", markers = "python_version >= \"3.11\""}, ] grpclib = {version = ">=0.4.5,<0.5.0", markers = "python_version >= \"3.10\""} -httpx = {version = ">=0.23.3,<0.24.0", extras = ["http2"]} -marshmallow = "3" +httpx = {version = ">=0.23.3,<1", extras = ["http2"]} +marshmallow = ">=3.20.1,<4.0.0" marshmallow-polyfield = ">=5.7,<6.0" -numpy = [ - {version = ">=1.17.0,<2.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, - {version = ">=1.24,<2.0", markers = "python_version >= \"3.11\""}, -] +numpy = {version = ">=1.17.0,<2", markers = "python_version >= \"3.8\" and python_version < \"3.12\""} plotly = ">=5.13.0,<6.0.0" -protobuf = [ - {version = ">=3.17.3,<4.0.0", markers = "python_version >= \"3.7\" and python_version < \"3.11\""}, - {version = ">=4.24,<5.0", markers = "python_version >= \"3.11\""}, -] -qm-octave = ">=2.0.1,<2.1.0" +protobuf = ">=3.17.3,<5" +qm-octave = "2.1.3" tinydb = ">=4.6.1,<5.0.0" typing-extensions = ">=4.5,<5.0" @@ -5798,4 +5775,4 @@ zh = ["laboneq"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "2e9555b32f971566f63c0505cb15a651f0dabd88ce630a265e96bc27cf250f6e" +content-hash = "593881ae2ecdab7ce87f556217d8be30ae8cbb0e5e7d61a43f0f839b2060b45e" diff --git a/pyproject.toml b/pyproject.toml index ff0656b367..7258638f67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "qibolab" -version = "0.1.10" +version = "0.1.11" description = "Quantum hardware module and drivers for Qibo" authors = ["The Qibo team"] license = "Apache License 2.0" @@ -30,7 +30,7 @@ qblox-instruments = { version = "0.12.0", optional = true } qcodes = { version = "^0.37.0", optional = true } qcodes_contrib_drivers = { version = "0.18.0", optional = true } pyvisa-py = { version = "0.5.3", optional = true } -qm-qua = { version = "==1.1.6", optional = true } +qm-qua = { version = "==1.2.1", optional = true } qualang-tools = { version = "^0.15.0", optional = true } setuptools = { version = ">67.0.0", optional = true } laboneq = { version = "==2.25.0", optional = true } diff --git a/src/qibolab/backends.py b/src/qibolab/backends.py index f17ce97f82..b2207505c3 100644 --- a/src/qibolab/backends.py +++ b/src/qibolab/backends.py @@ -11,6 +11,7 @@ from qibolab.execution_parameters import ExecutionParameters from qibolab.platform import Platform, create_platform from qibolab.platform.load import available_platforms +from qibolab.qubits import QubitId, QubitPairId from qibolab.version import __version__ as qibolab_version @@ -50,6 +51,33 @@ def __init__(self, platform): } self.compiler = Compiler.default() + @property + def qubits(self) -> list[QubitId]: + """Returns the qubits in the platform.""" + return list(self.platform.qubits) + + @property + def connectivity(self) -> list[QubitPairId]: + """Returns the list of connected qubits.""" + return list(self.platform.pairs) + + @property + def natives(self) -> list[str]: + """Returns the list of native gates supported by the platform.""" + compiler = Compiler.default() + natives = [g.__name__ for g in list(compiler.rules)] + calibrated = self.platform.pairs + + check_2q = ["CZ", "CNOT"] + for gate in check_2q: + if gate in natives and all( + getattr(calibrated[p].native_gates, gate) is None + for p in self.connectivity + ): + natives.remove(gate) + + return natives + def apply_gate(self, gate, state, nqubits): # pragma: no cover raise_error(NotImplementedError, "Qibolab cannot apply gates directly.") @@ -97,6 +125,11 @@ def execute_circuit(self, circuit, initial_state=None, nshots=1000): "Hardware backend only supports circuits as initial states.", ) + # This should be done in qibo side + # Temporary fix: overwrite the wire names + if not all(q in self.qubits for q in circuit.wire_names): + circuit._wire_names = self.qubits[: circuit.nqubits] + sequence, measurement_map = self.compiler.compile(circuit, self.platform) if not self.platform.is_connected: @@ -139,6 +172,12 @@ def execute_circuits(self, circuits, initial_states=None, nshots=1000): "Hardware backend only supports circuits as initial states.", ) + # This should be done in qibo side + # Temporary fix: overwrite the wire names + for circuit in circuits: + if not all(q in self.qubits for q in circuit.wire_names): + circuit._wire_names = self.qubits[: circuit.nqubits] + # TODO: Maybe these loops can be parallelized sequences, measurement_maps = zip( *(self.compiler.compile(circuit, self.platform) for circuit in circuits) diff --git a/src/qibolab/compilers/compiler.py b/src/qibolab/compilers/compiler.py index 316fde2586..dcc7c2a784 100644 --- a/src/qibolab/compilers/compiler.py +++ b/src/qibolab/compilers/compiler.py @@ -15,7 +15,7 @@ u3_rule, z_rule, ) -from qibolab.pulses import PulseSequence, ReadoutPulse +from qibolab.pulses import DrivePulse, PulseSequence @dataclass @@ -99,12 +99,24 @@ def inner(func): return inner def _compile_gate( - self, gate, platform, sequence, virtual_z_phases, moment_start, delays + self, + gate, + platform, + sequence, + virtual_z_phases, + moment_start, + delays, + wire_names, ): """Adds a single gate to the pulse sequence.""" rule = self[gate.__class__] + # get local sequence and phases for the current gate - gate_sequence, gate_phases = rule(gate, platform) + qubits_ids = ( + [wire_names[qubit] for qubit in gate.control_qubits], + [wire_names[qubit] for qubit in gate.target_qubits], + ) + gate_sequence, gate_phases = rule(qubits_ids, platform, gate.parameters) # update global pulse sequence # determine the right start time based on the availability of the qubits involved @@ -119,7 +131,7 @@ def _compile_gate( # shift start time and phase according to the global sequence for pulse in gate_sequence: pulse.start += start - if not isinstance(pulse, ReadoutPulse): + if isinstance(pulse, DrivePulse): pulse.relative_phase += virtual_z_phases[pulse.qubit] sequence.add(pulse) @@ -154,7 +166,13 @@ def compile(self, circuit, platform): delays[qubit] += gate.delay continue gate_sequence, gate_phases = self._compile_gate( - gate, platform, sequence, virtual_z_phases, moment_start, delays + gate, + platform, + sequence, + virtual_z_phases, + moment_start, + delays, + circuit.wire_names, ) for qubit in gate.qubits: delays[qubit] = 0 diff --git a/src/qibolab/compilers/default.py b/src/qibolab/compilers/default.py index a9bc0a8d3c..317a22ac32 100644 --- a/src/qibolab/compilers/default.py +++ b/src/qibolab/compilers/default.py @@ -8,52 +8,48 @@ from qibolab.pulses import PulseSequence -def identity_rule(gate, platform): +def identity_rule(qubits_ids, platform, parameters=None): """Identity gate skipped.""" return PulseSequence(), {} -def z_rule(gate, platform): +def z_rule(qubits_ids, platform, parameters=None): """Z gate applied virtually.""" - qubit = list(platform.qubits)[gate.target_qubits[0]] - return PulseSequence(), {qubit: math.pi} + return PulseSequence(), {qubits_ids[1][0]: math.pi} -def rz_rule(gate, platform): +def rz_rule(qubits_ids, platform, parameters=None): """RZ gate applied virtually.""" - qubit = list(platform.qubits)[gate.target_qubits[0]] - return PulseSequence(), {qubit: gate.parameters[0]} + return PulseSequence(), {qubits_ids[1][0]: -parameters[0]} -def gpi2_rule(gate, platform): +def gpi2_rule(qubits_ids, platform, parameters=None): """Rule for GPI2.""" - qubit = list(platform.qubits)[gate.target_qubits[0]] - theta = gate.parameters[0] + theta = parameters[0] sequence = PulseSequence() - pulse = platform.create_RX90_pulse(qubit, start=0, relative_phase=theta) + pulse = platform.create_RX90_pulse(qubits_ids[1][0], start=0, relative_phase=theta) sequence.add(pulse) return sequence, {} -def gpi_rule(gate, platform): +def gpi_rule(qubits_ids, platform, parameters=None): """Rule for GPI.""" - qubit = list(platform.qubits)[gate.target_qubits[0]] - theta = gate.parameters[0] + theta = parameters[0] sequence = PulseSequence() # the following definition has a global phase difference compare to # to the matrix representation. See # https://github.com/qiboteam/qibolab/pull/804#pullrequestreview-1890205509 # for more detail. - pulse = platform.create_RX_pulse(qubit, start=0, relative_phase=theta) + pulse = platform.create_RX_pulse(qubits_ids[1][0], start=0, relative_phase=theta) sequence.add(pulse) return sequence, {} -def u3_rule(gate, platform): +def u3_rule(qubits_ids, platform, parameters=None): """U3 applied as RZ-RX90-RZ-RX90-RZ.""" - qubit = list(platform.qubits)[gate.target_qubits[0]] + qubit = qubits_ids[1][0] # Transform gate to U3 and add pi/2-pulses - theta, phi, lam = gate.parameters + theta, phi, lam = parameters # apply RZ(lam) virtual_z_phases = {qubit: lam} sequence = PulseSequence() @@ -79,24 +75,26 @@ def u3_rule(gate, platform): return sequence, virtual_z_phases -def cz_rule(gate, platform): +def cz_rule(qubits_ids, platform, parameters=None): """CZ applied as defined in the platform runcard. Applying the CZ gate may involve sending pulses on qubits that the gate is not directly acting on. """ - return platform.create_CZ_pulse_sequence(gate.qubits) + qubits = qubits_ids[0] + qubits_ids[1] + return platform.create_CZ_pulse_sequence(qubits) -def cnot_rule(gate, platform): +def cnot_rule(qubits_ids, platform, parameters=None): """CNOT applied as defined in the platform runcard.""" - return platform.create_CNOT_pulse_sequence(gate.qubits) + qubits = qubits_ids[0] + qubits_ids[1] + return platform.create_CNOT_pulse_sequence(qubits) -def measurement_rule(gate, platform): +def measurement_rule(qubits_ids, platform, parameters=None): """Measurement gate applied using the platform readout pulse.""" sequence = PulseSequence() - for qubit in gate.target_qubits: + for qubit in qubits_ids[1]: MZ_pulse = platform.create_MZ_pulse(qubit, start=0) sequence.add(MZ_pulse) return sequence, {} diff --git a/src/qibolab/dummy/parameters.json b/src/qibolab/dummy/parameters.json index 498ab97d58..fb4632ff47 100644 --- a/src/qibolab/dummy/parameters.json +++ b/src/qibolab/dummy/parameters.json @@ -280,7 +280,7 @@ { "type": "virtual_z", "phase": 0.0, - "qubit": 1 + "qubit": 0 }, { "type": "virtual_z", @@ -309,12 +309,12 @@ }, { "type": "virtual_z", - "phase": 0.0, + "phase": 0.1, "qubit": 1 }, { "type": "virtual_z", - "phase": 0.0, + "phase": 0.2, "qubit": 2 }, { @@ -396,7 +396,7 @@ { "type": "virtual_z", "phase": 0.0, - "qubit": 1 + "qubit": 3 }, { "type": "virtual_z", @@ -426,7 +426,7 @@ { "type": "virtual_z", "phase": 0.0, - "qubit": 1 + "qubit": 3 }, { "type": "virtual_z", @@ -476,7 +476,7 @@ { "type": "virtual_z", "phase": 0.0, - "qubit": 1 + "qubit": 4 }, { "type": "virtual_z", diff --git a/src/qibolab/instruments/qblox/cluster_qcm_bb.py b/src/qibolab/instruments/qblox/cluster_qcm_bb.py index e95cf69ef9..4693777022 100644 --- a/src/qibolab/instruments/qblox/cluster_qcm_bb.py +++ b/src/qibolab/instruments/qblox/cluster_qcm_bb.py @@ -201,25 +201,54 @@ def setup(self, **settings): """ pass - def _get_next_sequencer(self, port, frequency, qubits: dict): - """Retrieves and configures the next avaliable sequencer. + def _get_next_sequencer( + self, port: str, frequency: float, qubits: dict, couplers: dict + ): + """Retrieves and configures the next available sequencer. The parameters of the new sequencer are copied from those of the default sequencer, except for the intermediate frequency and classification parameters. Args: - port (str): - frequency (): - qubit (): + port: name of the output port + frequency: NCO frequency + qubits: qubits associated with this sequencer + couplers: couplers associated with this sequencer Raises: Exception = If attempting to set a parameter without a connection to the instrument. """ - # select the qubit with flux line, if present, connected to the specific port + # check if this port is responsible for the flux of any qubit or coupler qubit = None for _qubit in qubits.values(): - name = _qubit.flux.port.name - module = _qubit.flux.port.module - if _qubit.flux is not None and (name, module) == (port, self): - qubit = _qubit + if _qubit.flux.port is not None: + if ( + _qubit.flux.port.name == port + and _qubit.flux.port.module.name == self.name + ): + qubit = _qubit + else: + log.warning(f"Qubit {_qubit.name} has no flux line connected") + + coupler = None + for _coupler in couplers.values(): + if _coupler.flux.port is not None: + if ( + _coupler.flux.port.name == port + and _coupler.flux.port.module.name == self.name + ): + coupler = _coupler + else: + log.warning(f"Coupler {_coupler.name} has no flux line connected") + + if qubit and coupler: + raise ValueError( + f"Port {port} of device {self.name} is configured for more than one line (flux lines of qubit {qubit.name} and coupler {coupler.name}" + ) + if qubit: + self._ports[port].offset = qubit.sweetspot + elif coupler: + self._ports[port].offset = coupler.sweetspot + else: + self._ports[port].offset = 0 # select a new sequencer and configure it as required next_sequencer_number = self._free_sequencers_numbers.pop(0) @@ -248,6 +277,7 @@ def _get_next_sequencer(self, port, frequency, qubits: dict): # create sequencer wrapper sequencer = Sequencer(next_sequencer_number) sequencer.qubit = qubit.name if qubit else None + sequencer.coupler = coupler.name if coupler else None return sequencer def get_if(self, pulse): @@ -267,6 +297,7 @@ def get_if(self, pulse): def process_pulse_sequence( self, qubits: dict, + couplers: dict, instrument_pulses: PulseSequence, navgs: int, nshots: int, @@ -349,6 +380,7 @@ def process_pulse_sequence( port=port, frequency=self.get_if(non_overlapping_pulses[0]), qubits=qubits, + couplers=couplers, ) # add the sequencer to the list of sequencers required by the port self._sequencers[port].append(sequencer) @@ -383,12 +415,13 @@ def process_pulse_sequence( port=port, frequency=self.get_if(non_overlapping_pulses[0]), qubits=qubits, + couplers=couplers, ) # add the sequencer to the list of sequencers required by the port self._sequencers[port].append(sequencer) else: sequencer = self._get_next_sequencer( - port=port, frequency=0, qubits=qubits + port=port, frequency=0, qubits=qubits, couplers=couplers ) # add the sequencer to the list of sequencers required by the port self._sequencers[port].append(sequencer) @@ -493,7 +526,13 @@ def process_pulse_sequence( ) else: # qubit_sweeper_parameters - if sequencer.qubit in [qubit.name for qubit in sweeper.qubits]: + if ( + sweeper.qubits + and sequencer.qubit in [q.name for q in sweeper.qubits] + ) or ( + sweeper.couplers + and sequencer.coupler in [c.name for c in sweeper.couplers] + ): # plays an active role if sweeper.parameter == Parameter.bias: reference_value = self._ports[port].offset @@ -616,7 +655,7 @@ def process_pulse_sequence( and pulses[n].sweeper.type == QbloxSweeperType.duration ): RI = pulses[n].sweeper.register - if pulses[n].type == PulseType.FLUX: + if pulses[n].type in (PulseType.FLUX, PulseType.COUPLERFLUX): RQ = pulses[n].sweeper.register else: RQ = pulses[n].sweeper.aux_register diff --git a/src/qibolab/instruments/qblox/cluster_qcm_rf.py b/src/qibolab/instruments/qblox/cluster_qcm_rf.py index cd91832577..a8f2c62bd1 100644 --- a/src/qibolab/instruments/qblox/cluster_qcm_rf.py +++ b/src/qibolab/instruments/qblox/cluster_qcm_rf.py @@ -197,6 +197,10 @@ def connect(self, cluster: Cluster = None): self._ports[port].lo_frequency = self.settings[port][ "lo_frequency" ] + if "mixer_calibration" in self.settings[port]: + self._ports[port].mixer_calibration = self.settings[port][ + "mixer_calibration" + ] self._ports[port].attenuation = self.settings[port]["attenuation"] self._ports[port].hardware_mod_en = True self._ports[port].nco_freq = 0 @@ -289,6 +293,7 @@ def get_if(self, pulse): def process_pulse_sequence( self, qubits: dict, + couplers: dict, instrument_pulses: PulseSequence, navgs: int, nshots: int, @@ -610,7 +615,7 @@ def process_pulse_sequence( and pulses[n].sweeper.type == QbloxSweeperType.duration ): RI = pulses[n].sweeper.register - if pulses[n].type == PulseType.FLUX: + if pulses[n].type in (PulseType.FLUX, PulseType.COUPLERFLUX): RQ = pulses[n].sweeper.register else: RQ = pulses[n].sweeper.aux_register diff --git a/src/qibolab/instruments/qblox/cluster_qrm_rf.py b/src/qibolab/instruments/qblox/cluster_qrm_rf.py index a70d26c428..7d930a4e92 100644 --- a/src/qibolab/instruments/qblox/cluster_qrm_rf.py +++ b/src/qibolab/instruments/qblox/cluster_qrm_rf.py @@ -213,6 +213,10 @@ def connect(self, cluster: Cluster = None): self._ports["o1"].lo_frequency = self.settings["o1"][ "lo_frequency" ] + if "mixer_calibration" in self.settings["o1"]: + self._ports["o1"].mixer_calibration = self.settings["o1"][ + "mixer_calibration" + ] self._ports["o1"].hardware_mod_en = True self._ports["o1"].nco_freq = 0 self._ports["o1"].nco_phase_offs = 0 @@ -337,6 +341,7 @@ def get_if(self, pulse: Pulse): def process_pulse_sequence( self, qubits: dict, + couplers: dict, instrument_pulses: PulseSequence, navgs: int, nshots: int, @@ -740,7 +745,10 @@ def process_pulse_sequence( and pulses[n].sweeper.type == QbloxSweeperType.duration ): RI = pulses[n].sweeper.register - if pulses[n].type == PulseType.FLUX: + if pulses[n].type in ( + PulseType.FLUX, + PulseType.COUPLERFLUX, + ): RQ = pulses[n].sweeper.register else: RQ = pulses[n].sweeper.aux_register @@ -787,7 +795,10 @@ def process_pulse_sequence( and pulses[n].sweeper.type == QbloxSweeperType.duration ): RI = pulses[n].sweeper.register - if pulses[n].type == PulseType.FLUX: + if pulses[n].type in ( + PulseType.FLUX, + PulseType.COUPLERFLUX, + ): RQ = pulses[n].sweeper.register else: RQ = pulses[n].sweeper.aux_register diff --git a/src/qibolab/instruments/qblox/controller.py b/src/qibolab/instruments/qblox/controller.py index f125677583..a82ca6635f 100644 --- a/src/qibolab/instruments/qblox/controller.py +++ b/src/qibolab/instruments/qblox/controller.py @@ -11,7 +11,7 @@ from qibolab.instruments.qblox.cluster_qcm_rf import QcmRf from qibolab.instruments.qblox.cluster_qrm_rf import QrmRf from qibolab.instruments.qblox.sequencer import SAMPLING_RATE -from qibolab.pulses import PulseSequence, PulseType +from qibolab.pulses import Custom, PulseSequence, PulseType, ReadoutPulse from qibolab.result import SampleResults from qibolab.sweeper import Parameter, Sweeper, SweeperType from qibolab.unrolling import Bounds @@ -102,7 +102,7 @@ def _termination_handler(self, signum, frame): log.warning("QbloxController: all modules are disconnected.") exit(0) - def _set_module_channel_map(self, module: QrmRf, qubits: dict): + def _set_module_channel_map(self, module: QrmRf, qubits: dict, couplers: dict): """Retrieve all the channels connected to a specific Qblox module. This method updates the `channel_port_map` attribute of the @@ -115,11 +115,16 @@ def _set_module_channel_map(self, module: QrmRf, qubits: dict): for channel in qubit.channels: if channel.port and channel.port.module.name == module.name: module.channel_map[channel.name] = channel + for coupler in couplers.values(): + for channel in coupler.channels: + if channel.port and channel.port.module.name == module.name: + module.channel_map[channel.name] = channel return list(module.channel_map) def _execute_pulse_sequence( self, qubits: dict, + couplers: dict, sequence: PulseSequence, options: ExecutionParameters, sweepers: list() = [], # list(Sweeper) = [] @@ -172,12 +177,13 @@ def _execute_pulse_sequence( data = {} for name, module in self.modules.items(): # from the pulse sequence, select those pulses to be synthesised by the module - module_channels = self._set_module_channel_map(module, qubits) + module_channels = self._set_module_channel_map(module, qubits, couplers) module_pulses[name] = sequence.get_channel_pulses(*module_channels) # ask each module to generate waveforms & program and upload them to the device module.process_pulse_sequence( qubits, + couplers, module_pulses[name], navgs, nshots, @@ -228,7 +234,7 @@ def _execute_pulse_sequence( return data def play(self, qubits, couplers, sequence, options): - return self._execute_pulse_sequence(qubits, sequence, options) + return self._execute_pulse_sequence(qubits, couplers, sequence, options) def sweep( self, @@ -269,10 +275,56 @@ def sweep( values=sweeper.values, pulses=ps, qubits=sweeper.qubits, + couplers=sweeper.couplers, type=sweeper.type, ) ) + serial_map = {p.serial: p.serial for p in sequence_copy.ro_pulses} + for sweeper in sweepers_copy: + if sweeper.parameter in (Parameter.duration, Parameter.start) and not any( + pulse.type is PulseType.READOUT for pulse in sweeper.pulses + ): + for pulse in sequence_copy.ro_pulses: + current_serial = pulse.serial + if sweeper.parameter is Parameter.duration: + sweep_values = sweeper.get_values(sweeper.pulses[0].duration) + if ( + max_finish := sweeper.pulses[0].start + np.max(sweep_values) + ) > pulse.start: + pulse.start = max_finish + serial_map[pulse.serial] = current_serial + if sweeper.parameter is Parameter.start: + idx = sequence_copy.index(pulse) + padded_pulse = ReadoutPulse( + start=0, + duration=pulse.start + pulse.duration, + amplitude=pulse.amplitude, + frequency=pulse.frequency, + relative_phase=pulse.relative_phase, + shape=Custom( + envelope_i=np.concatenate( + ( + np.zeros(pulse.start), + pulse.envelope_waveform_i().data + / pulse.amplitude, + ) + ), + envelope_q=np.concatenate( + ( + np.zeros(pulse.start), + pulse.envelope_waveform_q().data + / pulse.amplitude, + ) + ), + ), + channel=pulse.channel, + qubit=pulse.qubit, + ) + serial_map[padded_pulse.serial] = current_serial + sequence_copy[idx] = padded_pulse + sweeper.pulses.append(padded_pulse) + # reverse sweepers exept for res punchout att contains_attenuation_frequency = any( sweepers_copy[i].parameter == Parameter.attenuation @@ -292,6 +344,7 @@ def sweep( # execute the each sweeper recursively self._sweep_recursion( qubits, + couplers, sequence_copy, options, *tuple(sweepers_copy), @@ -301,13 +354,14 @@ def sweep( # return the results using the original serials serial_results = {} for pulse in sequence_copy.ro_pulses: - serial_results[map_id_serial[pulse.id]] = id_results[pulse.id] + serial_results[serial_map[map_id_serial[pulse.id]]] = id_results[pulse.id] serial_results[pulse.qubit] = id_results[pulse.id] return serial_results def _sweep_recursion( self, qubits, + couplers, sequence, options: ExecutionParameters, *sweepers, @@ -386,6 +440,7 @@ def _sweep_recursion( if len(sweepers) > 1: self._sweep_recursion( qubits, + couplers, sequence, options, *sweepers[1:], @@ -393,7 +448,10 @@ def _sweep_recursion( ) else: result = self._execute_pulse_sequence( - qubits=qubits, sequence=sequence, options=options + qubits=qubits, + couplers=couplers, + sequence=sequence, + options=options, ) for pulse in sequence.ro_pulses: if results[pulse.id]: @@ -437,9 +495,11 @@ def _sweep_recursion( values=_values, pulses=sweeper.pulses, qubits=sweeper.qubits, + couplers=sweeper.couplers, ) self._sweep_recursion( qubits, + couplers, sequence, options, *((split_sweeper,) + sweepers[1:]), @@ -486,7 +546,7 @@ def _sweep_recursion( # qubits[pulse.qubit].drive.gain = 1 result = self._execute_pulse_sequence( - qubits, sequence, options, sweepers + qubits, couplers, sequence, options, sweepers ) self._add_to_results(sequence, results, result) else: @@ -509,6 +569,7 @@ def _sweep_recursion( res = self._execute_pulse_sequence( qubits, + couplers, sequence, replace(options, nshots=_nshots), sweepers, diff --git a/src/qibolab/instruments/qblox/port.py b/src/qibolab/instruments/qblox/port.py index c83cf391e3..8827709d99 100644 --- a/src/qibolab/instruments/qblox/port.py +++ b/src/qibolab/instruments/qblox/port.py @@ -19,6 +19,8 @@ class QbloxOutputPort_Settings: nco_phase_offs: float = 0 lo_enabled: bool = True lo_frequency: int = 2_000_000_000 + i_offset: float = 0 + q_offset: float = 0 @dataclass @@ -36,7 +38,7 @@ class QbloxOutputPort(Port): def __init__(self, module, port_number: int, port_name: str = None): self.name = port_name self.module = module - self.sequencer_number: int = port_number + self.sequencer_number: int = module.DEFAULT_SEQUENCERS[port_name] self.port_number: int = port_number self._settings = QbloxOutputPort_Settings() @@ -221,6 +223,38 @@ def lo_frequency(self, value): elif self.module.device.is_qcm_type: self.module.device.set(f"out{self.port_number}_lo_freq", value=value) + @property + def mixer_calibration(self): + """Parameters for calibrating mixer output. + + i and q offsets are supported. + """ + if self.module.device: + self._settings.i_offset = self.module.device.get( + f"out{self.port_number}_offset_path0" + ) + self._settings.q_offset = self.module.device.get( + f"out{self.port_number}_offset_path1" + ) + return [self._settings.i_offset, self._settings.q_offset] + + @mixer_calibration.setter + def mixer_calibration(self, value): + if not isinstance(value, list) or len(value) != 2: + raise_error( + ValueError, + f"Invalid mixer calibration parameters {value}. A list [i_offset, q_offset] is required.", + ) + self._settings.i_offset, self._settings.q_offset = value + + if self.module.device: + self.module.device.set( + f"out{self.port_number}_offset_path0", self._settings.i_offset + ), + self.module.device.set( + f"out{self.port_number}_offset_path1", self._settings.q_offset + ), + class QbloxInputPort: def __init__(self, module, port_number: int, port_name: str = None): diff --git a/src/qibolab/instruments/qblox/sequencer.py b/src/qibolab/instruments/qblox/sequencer.py index 185375185d..60a03fd33c 100644 --- a/src/qibolab/instruments/qblox/sequencer.py +++ b/src/qibolab/instruments/qblox/sequencer.py @@ -1,3 +1,5 @@ +from typing import Optional, Union + import numpy as np from qblox_instruments.qcodes_drivers.sequencer import Sequencer as QbloxSequencer @@ -120,6 +122,7 @@ def bake_pulse_waveforms( Raises: NotEnoughMemory: If the memory needed to store the waveforms in more than the memory avalible. """ + values = np.round(values).astype(int) # In order to generate waveforms for each duration value, the pulse will need to be modified. # To avoid any conflicts, make a copy of the pulse first. pulse_copy = pulse.copy() @@ -127,7 +130,7 @@ def bake_pulse_waveforms( # there may be other waveforms stored already, set first index as the next available first_idx = len(self.unique_waveforms) - if pulse.type == PulseType.FLUX: + if pulse.type in (PulseType.FLUX, PulseType.COUPLERFLUX): # for flux pulses, store i waveforms idx_range = np.arange(first_idx, first_idx + len(values), 1) @@ -231,4 +234,5 @@ def __init__(self, number: int): self.acquisitions: dict = {} self.weights: dict = {} self.program: Program = Program() - self.qubit = None # self.qubit: int | str = None + self.qubit: Optional[Union[int, str]] = None + self.coupler: Optional[Union[int, str]] = None diff --git a/src/qibolab/instruments/qm/__init__.py b/src/qibolab/instruments/qm/__init__.py index e053aa970a..6689872786 100644 --- a/src/qibolab/instruments/qm/__init__.py +++ b/src/qibolab/instruments/qm/__init__.py @@ -1,2 +1,2 @@ from .controller import QMController -from .devices import Octave, OPXplus +from .devices import FEM, OPX1000, Octave, OPXplus diff --git a/src/qibolab/instruments/qm/config.py b/src/qibolab/instruments/qm/config.py index 41c1f3e4af..7915f1b917 100644 --- a/src/qibolab/instruments/qm/config.py +++ b/src/qibolab/instruments/qm/config.py @@ -6,12 +6,12 @@ from qibolab.pulses import PulseType, Rectangular -from .ports import OPXIQ, OctaveInput, OctaveOutput, OPXOutput +from .ports import OPXIQ, FEMInput, FEMOutput, OctaveInput, OctaveOutput, OPXOutput SAMPLING_RATE = 1 """Sampling rate of Quantum Machines OPX in GSps.""" -DEFAULT_INPUTS = {1: {}, 2: {}} +DEFAULT_INPUTS = {1: {"offset": 0}, 2: {"offset": 0}} """Default controller config section. Inputs are always registered to avoid issues with automatic mixer @@ -48,28 +48,48 @@ def register_port(self, port): self.register_port(port.q) else: is_octave = isinstance(port, (OctaveOutput, OctaveInput)) + is_fem = isinstance(port, (FEMOutput, FEMInput)) controllers = self.octaves if is_octave else self.controllers if port.device not in controllers: if is_octave: controllers[port.device] = {} + elif is_fem: + controllers[port.device] = {"type": "opx1000", "fems": {}} else: controllers[port.device] = { - "analog_inputs": DEFAULT_INPUTS, + "analog_inputs": DEFAULT_INPUTS.copy(), "digital_outputs": {}, } - device = controllers[port.device] + if is_fem: + fems = controllers[port.device]["fems"] + if port.fem_number not in fems: + fems[port.fem_number] = { + "type": port.fem_type, + "analog_inputs": DEFAULT_INPUTS.copy(), + "digital_outputs": {}, + } + device = fems[port.fem_number] + else: + device = controllers[port.device] + if port.key in device: device[port.key].update(port.config) else: device[port.key] = port.config if is_octave: - con = port.opx_port.i.device - number = port.opx_port.i.number - device["connectivity"] = con self.register_port(port.opx_port) - self.controllers[con]["digital_outputs"][number] = {} + subport = port.opx_port.i + con = subport.device + number = subport.number + if isinstance(subport, (FEMOutput, FEMInput)): + fem = subport.fem_number + device["connectivity"] = (con, fem) + self.controllers[con]["fems"][fem]["digital_outputs"][number] = {} + else: + device["connectivity"] = con + self.controllers[con]["digital_outputs"][number] = {} @staticmethod def iq_imbalance(g, phi): @@ -275,10 +295,6 @@ def register_pulse(self, qubit, qmpulse): "waveforms": {"I": serial_i, "Q": serial_q}, "digital_marker": "ON", } - # register drive pulse in elements - self.elements[qmpulse.element]["operations"][ - qmpulse.operation - ] = qmpulse.operation elif pulse.type is PulseType.FLUX: serial = self.register_waveform(pulse) @@ -289,10 +305,6 @@ def register_pulse(self, qubit, qmpulse): "single": serial, }, } - # register flux pulse in elements - self.elements[qmpulse.element]["operations"][ - qmpulse.operation - ] = qmpulse.operation elif pulse.type is PulseType.READOUT: serial_i = self.register_waveform(pulse, "i") @@ -312,14 +324,14 @@ def register_pulse(self, qubit, qmpulse): }, "digital_marker": "ON", } - # register readout pulse in elements - self.elements[qmpulse.element]["operations"][ - qmpulse.operation - ] = qmpulse.operation else: raise_error(TypeError, f"Unknown pulse type {pulse.type.name}.") + self.elements[qmpulse.element]["operations"][ + qmpulse.operation + ] = qmpulse.operation + def register_waveform(self, pulse, mode="i"): """Registers waveforms in QM config. diff --git a/src/qibolab/instruments/qm/controller.py b/src/qibolab/instruments/qm/controller.py index 08d3f23f6e..c7d7871dea 100644 --- a/src/qibolab/instruments/qm/controller.py +++ b/src/qibolab/instruments/qm/controller.py @@ -189,6 +189,9 @@ def __post_init__(self): # convert simulation duration from ns to clock cycles self.simulation_duration //= 4 + def __str__(self): + return self.name + def ports(self, name, output=True): """Provides instrument ports to the user. @@ -255,7 +258,7 @@ def disconnect(self): self._reset_temporary_calibration() if self.manager is not None: self.manager.close_all_quantum_machines() - self.manager.close() + self.manager = None self.is_connected = False def calibrate_mixers(self, qubits): @@ -285,6 +288,10 @@ def execute_program(self, program): Args: program: QUA program. """ + if self.manager is None: + raise RuntimeError( + "Quantum Machines are not connected. Please use ``platform.connect()``." + ) machine = self.manager.open_qm(self.config.__dict__) return machine.execute(program) @@ -373,6 +380,10 @@ def sweep(self, qubits, couplers, sequence, options, *sweepers): # play pulses using QUA with qua.program() as experiment: n = declare(int) + for qubit in qubits.values(): + if qubit.flux: + qua.set_dc_offset(qubit.flux.name, "single", qubit.sweetspot) + acquisitions = declare_acquisitions(ro_pulses, qubits, options) with for_(n, 0, n < options.nshots, n + 1): sweep( diff --git a/src/qibolab/instruments/qm/devices.py b/src/qibolab/instruments/qm/devices.py index 6ec0674d88..e8b3a547bd 100644 --- a/src/qibolab/instruments/qm/devices.py +++ b/src/qibolab/instruments/qm/devices.py @@ -1,12 +1,14 @@ from collections import defaultdict from dataclasses import dataclass, field from itertools import chain -from typing import Dict +from typing import Dict, Literal, Union from qibolab.instruments.abstract import Instrument from .ports import ( OPXIQ, + FEMInput, + FEMOutput, OctaveInput, OctaveOutput, OPXInput, @@ -43,6 +45,9 @@ class QMDevice(Instrument): inputs: Dict[int, QMInput] = field(init=False) """Dictionary containing the instrument's input ports.""" + def __str__(self): + return self.name + def ports(self, number, output=True): """Provides instrument's ports to the user. @@ -94,13 +99,60 @@ def __post_init__(self): self.inputs = PortsDefaultdict(lambda n: OPXInput(self.name, n)) +@dataclass +class FEM: + """Device handling OPX1000 FEMs.""" + + name: int + type: Literal["LF", "MF"] = "LF" + + +@dataclass +class OPX1000(QMDevice): + """Device handling OPX1000 controllers.""" + + fems: Dict[int, FEM] = field(default_factory=dict) + + def __post_init__(self): + def kwargs(fem): + return {"fem_number": fem, "fem_type": self.fems[fem].type} + + self.outputs = PortsDefaultdict( + lambda pair: FEMOutput(self.name, pair[1], **kwargs(pair[0])) + ) + self.inputs = PortsDefaultdict( + lambda pair: FEMInput(self.name, pair[1], **kwargs(pair[0])) + ) + + def ports(self, fem_number: int, number: int, output: bool = True): + ports_ = self.outputs if output else self.inputs + return ports_[(fem_number, number)] + + def connectivity(self, fem_number: int) -> tuple["OPX1000", int]: + return (self, fem_number) + + def setup(self, **kwargs): + for name, settings in kwargs.items(): + fem, port = name.split("/") + fem = int(fem) + number = int(port[1:]) + if port[0] == "o": + self.outputs[(fem, number)].setup(**settings) + elif port[0] == "i": + self.inputs[(fem, number)].setup(**settings) + else: + raise ValueError( + f"Invalid port name {name} in instrument settings for {self.name}." + ) + + @dataclass class Octave(QMDevice): """Device handling Octaves.""" port: int """Network port of the Octave in the cluster configuration.""" - connectivity: OPXplus + connectivity: Union[OPXplus, tuple[OPX1000, int]] """OPXplus that acts as the waveform generator for the Octave.""" def __post_init__(self): @@ -115,7 +167,12 @@ def ports(self, number, output=True): """ port = super().ports(number, output) if port.opx_port is None: - iport = self.connectivity.ports(2 * number - 1, output) - qport = self.connectivity.ports(2 * number, output) + if isinstance(self.connectivity, OPXplus): + iport = self.connectivity.ports(2 * number - 1, output) + qport = self.connectivity.ports(2 * number, output) + else: + opx, fem_number = self.connectivity + iport = opx.ports(fem_number, 2 * number - 1, output) + qport = opx.ports(fem_number, 2 * number, output) port.opx_port = OPXIQ(iport, qport) return port diff --git a/src/qibolab/instruments/qm/ports.py b/src/qibolab/instruments/qm/ports.py index 1d6ce2d444..5a593746dc 100644 --- a/src/qibolab/instruments/qm/ports.py +++ b/src/qibolab/instruments/qm/ports.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field, fields -from typing import ClassVar, Dict, Optional, Union +from typing import ClassVar, Dict, Literal, Optional, Union DIGITAL_DELAY = 57 DIGITAL_BUFFER = 18 @@ -131,6 +131,37 @@ class OPXIQ: """Port implementing the Q-component of the signal.""" +@dataclass +class FEMOutput(OPXOutput): + fem_number: int = 0 + fem_type: Literal["LF", "MF"] = "LF" + output_mode: Literal["direct", "amplified"] = field( + default="direct", metadata={"config": "output_mode", "settings": True} + ) + + @property + def name(self): + return f"{self.fem_number}/o{self.number}" + + @property + def pair(self): + return (self.device, self.fem_number, self.number) + + +@dataclass +class FEMInput(OPXInput): + fem_number: int = 0 + fem_type: Literal["LF", "MF"] = "LF" + + @property + def name(self): + return f"{self.fem_number}/i{self.number}" + + @property + def pair(self): + return (self.device, self.fem_number, self.number) + + @dataclass class OctaveOutput(QMOutput): key: ClassVar[str] = "RF_outputs" @@ -149,7 +180,7 @@ class OctaveOutput(QMOutput): Can be external or internal. """ - output_mode: str = field(default="triggered", metadata={"config": "output_mode"}) + output_mode: str = field(default="always_on", metadata={"config": "output_mode"}) """Can be: "always_on" / "always_off"/ "triggered" / "triggered_reversed".""" digital_delay: int = DIGITAL_DELAY """Delay for digital output channel.""" @@ -165,11 +196,14 @@ def digital_inputs(self): Digital markers are used to switch LOs on in triggered mode. """ - opx = self.opx_port.i.device - number = self.opx_port.i.number + opx_port = self.opx_port.i + if isinstance(opx_port, (FEMOutput, FEMInput)): + port = (opx_port.device, opx_port.fem_number, opx_port.number) + else: + port = (opx_port.device, opx_port.number) return { "output_switch": { - "port": (opx, number), + "port": port, "delay": self.digital_delay, "buffer": self.digital_buffer, } diff --git a/src/qibolab/instruments/qm/sweepers.py b/src/qibolab/instruments/qm/sweepers.py index 3110e1f3d8..081f682649 100644 --- a/src/qibolab/instruments/qm/sweepers.py +++ b/src/qibolab/instruments/qm/sweepers.py @@ -141,16 +141,18 @@ def _sweep_bias(sweepers, qubits, qmsequence, relaxation_time): for qubit in sweeper.qubits: b0 = qubit.flux.offset max_offset = qubit.flux.max_offset + if max_offset is None: + max_offset = 0.5 max_value = maximum_sweep_value(sweeper.values, b0) check_max_offset(max_value, max_offset) offset0.append(declare(fixed, value=b0)) b = declare(fixed) with for_(*from_array(b, sweeper.values)): for qubit, b0 in zip(sweeper.qubits, offset0): - with qua.if_((b + b0) >= 0.49): - qua.set_dc_offset(f"flux{qubit.name}", "single", 0.49) - with qua.elif_((b + b0) <= -0.49): - qua.set_dc_offset(f"flux{qubit.name}", "single", -0.49) + with qua.if_((b + b0) >= max_offset): + qua.set_dc_offset(f"flux{qubit.name}", "single", max_offset) + with qua.elif_((b + b0) <= -max_offset): + qua.set_dc_offset(f"flux{qubit.name}", "single", -max_offset) with qua.else_(): qua.set_dc_offset(f"flux{qubit.name}", "single", (b + b0)) diff --git a/src/qibolab/instruments/zhinst/executor.py b/src/qibolab/instruments/zhinst/executor.py index bfba7a6d98..995863bf94 100644 --- a/src/qibolab/instruments/zhinst/executor.py +++ b/src/qibolab/instruments/zhinst/executor.py @@ -559,6 +559,34 @@ def get_channel_node_path(self, channel_name: str) -> str: f"Could not find instrument node corresponding to channel {channel_name}" ) + def _calculate_weight(self, pulse, qubit, exp_options): + if ( + qubit.kernel is not None + and exp_options.acquisition_type == lo.AcquisitionType.DISCRIMINATION + ): + return lo.pulse_library.sampled_pulse_complex( + samples=qubit.kernel * np.exp(1j * qubit.iq_angle), + ) + + elif exp_options.acquisition_type == lo.AcquisitionType.DISCRIMINATION: + return lo.pulse_library.sampled_pulse_complex( + samples=np.ones( + [ + int( + pulse.pulse.duration * 2 + - 3 * self.smearing * NANO_TO_SECONDS + ) + ] + ) + * np.exp(1j * qubit.iq_angle), + ) + else: + return lo.pulse_library.const( + length=round(pulse.pulse.duration * NANO_TO_SECONDS, 9) + - 1.5 * self.smearing * NANO_TO_SECONDS, + amplitude=1, + ) + def select_exp(self, exp, qubits, exp_options): """Build Zurich Experiment selecting the relevant sections.""" # channels that were not split are just applied in parallel to the rest of the experiment @@ -607,53 +635,17 @@ def select_exp(self, exp, qubits, exp_options): with exp.section(uid=section_uid, play_after=previous_section): for ch, pulse in seq.measurements: qubit = qubits[pulse.pulse.qubit] - q = qubit.name exp.delay( signal=acquire_channel_name(qubit), time=self.smearing * NANO_TO_SECONDS, ) - if ( - qubit.kernel is not None - and exp_options.acquisition_type - == lo.AcquisitionType.DISCRIMINATION - ): - weight = lo.pulse_library.sampled_pulse_complex( - samples=qubit.kernel * np.exp(1j * qubit.iq_angle), + if qubit.name not in weights: + weights[qubit.name] = self._calculate_weight( + pulse, qubit, exp_options ) - else: - if i == 0: - if ( - exp_options.acquisition_type - == lo.AcquisitionType.DISCRIMINATION - ): - weight = lo.pulse_library.sampled_pulse_complex( - samples=np.ones( - [ - int( - pulse.pulse.duration * 2 - - 3 * self.smearing * NANO_TO_SECONDS - ) - ] - ) - * np.exp(1j * qubit.iq_angle), - ) - weights[q] = weight - else: - weight = lo.pulse_library.const( - length=round( - pulse.pulse.duration * NANO_TO_SECONDS, 9 - ) - - 1.5 * self.smearing * NANO_TO_SECONDS, - amplitude=1, - ) - - weights[q] = weight - elif i != 0: - weight = weights[q] - measure_pulse_parameters = {"phase": 0} if i == len(self.sequence[measure_channel_name(qubit)]) - 1: @@ -663,8 +655,8 @@ def select_exp(self, exp, qubits, exp_options): exp.measure( acquire_signal=acquire_channel_name(qubit), - handle=f"sequence{q}_{i}", - integration_kernel=weight, + handle=f"sequence{qubit.name}_{i}", + integration_kernel=weights[qubit.name], integration_kernel_parameters=None, integration_length=None, measure_signal=measure_channel_name(qubit), diff --git a/src/qibolab/platform/platform.py b/src/qibolab/platform/platform.py index 7477405caf..88f3a310f9 100644 --- a/src/qibolab/platform/platform.py +++ b/src/qibolab/platform/platform.py @@ -1,5 +1,6 @@ """A platform for executing quantum algorithms.""" +import signal from collections import defaultdict from dataclasses import dataclass, field, replace from typing import Dict, List, Optional, Tuple @@ -10,7 +11,14 @@ from qibolab.couplers import Coupler from qibolab.execution_parameters import ExecutionParameters from qibolab.instruments.abstract import Controller, Instrument, InstrumentId -from qibolab.pulses import Drag, FluxPulse, PulseSequence, ReadoutPulse +from qibolab.pulses import ( + CouplerFluxPulse, + Drag, + FluxPulse, + PulseSequence, + ReadoutPulse, + Rectangular, +) from qibolab.qubits import Qubit, QubitId, QubitPair, QubitPairId from qibolab.sweeper import Sweeper from qibolab.unrolling import batch @@ -110,6 +118,9 @@ class Platform: """Graph representing the qubit connectivity in the quantum chip.""" def __post_init__(self): + signal.signal(signal.SIGTERM, self.termination_handler) + signal.signal(signal.SIGINT, self.termination_handler) + log.info("Loading platform %s", self.name) if self.resonator_type is None: self.resonator_type = "3D" if self.nqubits == 1 else "2D" @@ -161,6 +172,12 @@ def disconnect(self): instrument.disconnect() self.is_connected = False + def termination_handler(self, signum, frame): + self.disconnect() + raise RuntimeError( + f"Platform {self.name} disconnected because job was cancelled. Signal type: {signum}." + ) + def _execute(self, sequence, options, **kwargs): """Executes sequence on the controllers.""" result = {} @@ -385,26 +402,45 @@ def create_qubit_readout_pulse(self, qubit, start): qubit = self.get_qubit(qubit) return self.create_MZ_pulse(qubit, start) - def create_qubit_flux_pulse(self, qubit, start, duration, amplitude=1): + def create_qubit_flux_pulse(self, qubit, start, duration, amplitude=1, shape=None): qubit = self.get_qubit(qubit) + if shape is None: + shape = Rectangular() pulse = FluxPulse( start=start, duration=duration, amplitude=amplitude, - shape="Rectangular", + shape=shape, channel=self.qubits[qubit].flux.name, qubit=qubit, ) pulse.duration = duration return pulse - def create_coupler_pulse(self, coupler, start, duration=None, amplitude=None): + def create_coupler_pulse( + self, coupler, start, duration=None, amplitude=None, shape=None + ): coupler = self.get_coupler(coupler) - pulse = self.couplers[coupler].native_pulse.CP.pulse(start) - if duration is not None: + native_pulse = self.couplers[coupler].native_pulse.CP.pulse(start) + + if duration is None: + duration = native_pulse.duration + if amplitude is None: + amplitude = native_pulse.amplitude + + if shape is None: + pulse = native_pulse pulse.duration = duration - if amplitude is not None: pulse.amplitude = amplitude + else: + pulse = CouplerFluxPulse( + start=start, + duration=duration, + amplitude=amplitude, + shape=shape, + channel=self.qubits[coupler].flux.name, + qubit=coupler, + ) return pulse # TODO Remove RX90_drag_pulse and RX_drag_pulse, replace them with create_qubit_drive_pulse diff --git a/src/qibolab/pulses.py b/src/qibolab/pulses.py index 8da469bb9e..45eae38c28 100644 --- a/src/qibolab/pulses.py +++ b/src/qibolab/pulses.py @@ -19,6 +19,22 @@ """ +def _np_array_from_string_or_array_like(x) -> np.ndarray: + """Convert input into numpy array. + + The input can be in one of these forms: + 1. string in form '[1, 2, 3, ...]' + 2. list, e.g. [1, 2, 3, ...] + 3. numpy array. This will be identity conversion. + """ + if isinstance(x, str): + return np.fromstring(x[1:-1], sep=",") + elif isinstance(x, (list, np.ndarray)): + return np.array(x) + else: + raise ValueError(f"Data in unrecognized format: {x}") + + class PulseType(Enum): """An enumeration to distinguish different types of pulses. @@ -215,12 +231,19 @@ def eval(value: str) -> "PulseShape": To be replaced by proper serialization. """ - shape_name = re.findall(r"(\w+)", value)[0] - if shape_name not in globals(): - raise ValueError(f"shape {value} not found") - shape_parameters = re.findall(r"[-\w+\d\.\d]+", value)[1:] - # TODO: create multiple tests to prove regex working correctly - return globals()[shape_name](*shape_parameters) + match = re.fullmatch(r"(\w+)\((.*)\)", value) + shape_name, params = None, None + if match is not None: + shape_name, params = match.groups() + if match is None or shape_name not in globals(): + raise ValueError(f"shape {value} not recognized") + + single_item_pattern = r"[^,\s\[\]\(\)]+" + csv_items_pattern = rf"(?:{single_item_pattern}(?:,\s*)?)*" + param_pattern = ( + rf"\[{csv_items_pattern}\]|\w+\({csv_items_pattern}\)|{single_item_pattern}" + ) + return globals()[shape_name](*re.findall(param_pattern, params)) class Rectangular(PulseShape): @@ -509,12 +532,14 @@ class IIR(PulseShape): # p = [b0 = 1−k +k ·α, b1 = −(1−k)·(1−α),a0 = 1 and a1 = −(1−α)] # p = [b0, b1, a0, a1] - def __init__(self, b, a, target: PulseShape): + def __init__(self, b, a, target): self.name = "IIR" - self.target: PulseShape = target + self.target: PulseShape = ( + PulseShape.eval(target) if isinstance(target, str) else target + ) self._pulse: Pulse = None - self.a: np.ndarray = np.array(a) - self.b: np.ndarray = np.array(b) + self.a: np.ndarray = _np_array_from_string_or_array_like(a) + self.b: np.ndarray = _np_array_from_string_or_array_like(b) # Check len(a) = len(b) = 2 def __eq__(self, item) -> bool: @@ -596,8 +621,10 @@ class SNZ(PulseShape): def __init__(self, t_idling, b_amplitude=None): self.name = "SNZ" self.pulse: Pulse = None - self.t_idling: float = t_idling - self.b_amplitude = b_amplitude + self.t_idling: float = float(t_idling) + self.b_amplitude = ( + float(b_amplitude) if b_amplitude is not None else b_amplitude + ) def __eq__(self, item) -> bool: """Overloads == operator.""" @@ -705,11 +732,14 @@ class Custom(PulseShape): """Arbitrary shape.""" def __init__(self, envelope_i, envelope_q=None): + self.name = "Custom" self.pulse: Pulse = None - self.envelope_i: np.ndarray = np.array(envelope_i) + self.envelope_i: np.ndarray = _np_array_from_string_or_array_like(envelope_i) if envelope_q is not None: - self.envelope_q: np.ndarray = np.array(envelope_q) + self.envelope_q: np.ndarray = _np_array_from_string_or_array_like( + envelope_q + ) else: self.envelope_q = self.envelope_i @@ -717,11 +747,13 @@ def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: - if self.pulse.duration != len(self.envelope_i): - raise ValueError("Length of envelope_i must be equal to pulse duration") num_samples = int(np.rint(self.pulse.duration * sampling_rate)) + if len(self.envelope_i) < num_samples: + raise ValueError( + "Length of envelope_i must not be shorter than pulse duration in samples" + ) - waveform = Waveform(self.envelope_i * self.pulse.amplitude) + waveform = Waveform(self.envelope_i[:num_samples] * self.pulse.amplitude) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError @@ -730,17 +762,19 @@ def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: - if self.pulse.duration != len(self.envelope_q): - raise ValueError("Length of envelope_q must be equal to pulse duration") num_samples = int(np.rint(self.pulse.duration * sampling_rate)) + if len(self.envelope_q) < num_samples: + raise ValueError( + "Length of envelope_q must not be shorter than pulse duration in samples" + ) - waveform = Waveform(self.envelope_q * self.pulse.amplitude) + waveform = Waveform(self.envelope_q[:num_samples] * self.pulse.amplitude) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError def __repr__(self): - return f"{self.name}({self.envelope_i[:3]}, ..., {self.envelope_q[:3]}, ...)" + return f"{self.name}({self.envelope_i[:3]}, {self.envelope_q[:3]})" @dataclass @@ -929,6 +963,15 @@ def copy(self): # -> Pulse|ReadoutPulse|DrivePulse|FluxPulse: self.channel, self.qubit, ) + elif type(self) == CouplerFluxPulse: + return CouplerFluxPulse( + self.start, + self.duration, + self.amplitude, + self.shape, + self.channel, + self.qubit, + ) else: # return eval(self.serial) return Pulse( @@ -1221,6 +1264,7 @@ class PulseConstructor(Enum): READOUT = ReadoutPulse DRIVE = DrivePulse FLUX = FluxPulse + COUPLERFLUX = CouplerFluxPulse class PulseSequence: @@ -1543,30 +1587,39 @@ def separate_overlapping_pulses(self): # -> dict((int,int): PulseSequence): """Separates a sequence of overlapping pulses into a list of non- overlapping sequences.""" - # This routine separates the pulses of a sequence into non-overlapping sets - # but it does not check if the frequencies of the pulses within a set have the same frequency - - separated_pulses = [] - for new_pulse in self.pulses: - stored = False - for ps in separated_pulses: - overlaps = False - for existing_pulse in ps: - if ( - new_pulse.start < existing_pulse.finish - and new_pulse.finish > existing_pulse.start - ): - overlaps = True + # This routine separates the pulses of a sequence into sets of different frequecy, non-overlapping + # pulses + + freqs = set() + for pulse in self.pulses: + freqs |= {pulse.frequency} + PS_freq = {} + separated_pulses = {} + for freq in freqs: + PS_freq[freq] = PulseSequence() + separated_pulses[freq] = [] + for pulse in self.pulses: + if pulse.frequency == freq: + PS_freq[freq].add(pulse) + + for new_pulse in PS_freq[freq]: + stored = False + for ps in separated_pulses[freq]: + overlaps = False + for existing_pulse in ps: + if ( + new_pulse.start < existing_pulse.finish + and new_pulse.finish > existing_pulse.start + ): + overlaps = True + break + if not overlaps: + ps.add(new_pulse) + stored = True break - if not overlaps: - ps.add(new_pulse) - stored = True - break - if not stored: - separated_pulses.append(PulseSequence(new_pulse)) - return separated_pulses - - # TODO: Implement separate_different_frequency_pulses() + if not stored: + separated_pulses[freq].append(PulseSequence(new_pulse)) + return [ps for freq in freqs for ps in separated_pulses[freq]] @property def pulses_overlap(self) -> bool: diff --git a/src/qibolab/result.py b/src/qibolab/result.py index 837d7fba10..19ecb0c7fd 100644 --- a/src/qibolab/result.py +++ b/src/qibolab/result.py @@ -38,7 +38,7 @@ def magnitude(self): @cached_property def phase(self): """Signal phase in radians.""" - return np.unwrap(np.arctan2(self.voltage_i, self.voltage_q)) + return np.unwrap(np.arctan2(self.voltage_q, self.voltage_i)) @cached_property def phase_std(self): @@ -95,7 +95,7 @@ def phase_std(self): @cached_property def phase(self): """Phase not unwrapped because it is a single value.""" - return np.arctan2(self.voltage_i, self.voltage_q) + return np.arctan2(self.voltage_q, self.voltage_i) class RawWaveformResults(IntegratedResults): diff --git a/tests/dummy_qrc/qblox/parameters.json b/tests/dummy_qrc/qblox/parameters.json index 7a5099b5fe..e8797d7266 100644 --- a/tests/dummy_qrc/qblox/parameters.json +++ b/tests/dummy_qrc/qblox/parameters.json @@ -11,24 +11,30 @@ 3, 4 ], - "topology": [ - [ + "couplers": [ + 0, + 1, + 3, + 4 + ], + "topology": { + "0": [ 0, 2 ], - [ + "1": [ 1, 2 ], - [ + "3": [ 2, 3 ], - [ + "4": [ 2, 4 ] - ], + }, "instruments": { "qblox_controller": { "bounds": { @@ -76,7 +82,8 @@ "o1": { "attenuation": 36, "lo_frequency": 7300000000, - "gain": 0.6 + "gain": 0.6, + "mixer_calibration": [-3.2, 1.4] }, "i1": { "acquisition_hold_off": 500, @@ -87,7 +94,8 @@ "o1": { "attenuation": 36, "lo_frequency": 7850000000, - "gain": 0.6 + "gain": 0.6, + "mixer_calibration": [0.3, -3.8] }, "i1": { "acquisition_hold_off": 500, @@ -243,6 +251,48 @@ } } }, + "coupler": { + "0": { + "CP": { + "type": "coupler", + "duration": 0, + "amplitude": 0, + "shape": "Rectangular()", + "coupler": 0, + "relative_start": 0 + } + }, + "1": { + "CP": { + "type": "coupler", + "duration": 0, + "amplitude": 0, + "shape": "Rectangular()", + "coupler": 1, + "relative_start": 0 + } + }, + "3": { + "CP": { + "type": "coupler", + "duration": 0, + "amplitude": 0, + "shape": "Rectangular()", + "coupler": 3, + "relative_start": 0 + } + }, + "4": { + "CP": { + "type": "coupler", + "duration": 0, + "amplitude": 0, + "shape": "Rectangular()", + "coupler": 4, + "relative_start": 0 + } + } + }, "two_qubit": { "2-3": { "CZ": [ @@ -401,22 +451,18 @@ "threshold": 0.002323 } }, - "two_qubit":{ - "0-2": { - "gate_fidelity": [0.0, 0.0], - "cz_fidelity": [0.0, 0.0] + "coupler": { + "0": { + "sweetspot": 0.1 }, - "1-2": { - "gate_fidelity": [0.0, 0.0], - "cz_fidelity": [0.0, 0.0] + "1": { + "sweetspot": 0.2 }, - "2-3": { - "gate_fidelity": [0.0, 0.0], - "cz_fidelity": [0.0, 0.0] + "3": { + "sweetspot": -0.3 }, - "2-4": { - "gate_fidelity": [0.0, 0.0], - "cz_fidelity": [0.0, 0.0] + "4": { + "sweetspot": 0.0 } } } diff --git a/tests/dummy_qrc/qblox/platform.py b/tests/dummy_qrc/qblox/platform.py index a60a600d8f..6129c716d9 100644 --- a/tests/dummy_qrc/qblox/platform.py +++ b/tests/dummy_qrc/qblox/platform.py @@ -30,6 +30,7 @@ def create(): modules = { "qcm_bb0": QcmBb("qcm_bb0", f"{ADDRESS}:2"), "qcm_bb1": QcmBb("qcm_bb1", f"{ADDRESS}:4"), + "qcm_bb2": QcmBb("qcm_bb2", f"{ADDRESS}:3"), "qcm_rf0": QcmRf("qcm_rf0", f"{ADDRESS}:6"), "qcm_rf1": QcmRf("qcm_rf1", f"{ADDRESS}:8"), "qcm_rf2": QcmRf("qcm_rf2", f"{ADDRESS}:10"), @@ -61,12 +62,17 @@ def create(): channels |= Channel(name="L3-12", port=modules["qcm_rf1"].ports("o1")) channels |= Channel(name="L3-13", port=modules["qcm_rf1"].ports("o2")) channels |= Channel(name="L3-14", port=modules["qcm_rf2"].ports("o1")) - # Flux + # Qubit flux channels |= Channel(name="L4-5", port=modules["qcm_bb0"].ports("o1")) channels |= Channel(name="L4-1", port=modules["qcm_bb0"].ports("o2")) channels |= Channel(name="L4-2", port=modules["qcm_bb0"].ports("o3")) channels |= Channel(name="L4-3", port=modules["qcm_bb0"].ports("o4")) channels |= Channel(name="L4-4", port=modules["qcm_bb1"].ports("o1")) + # Coupler flux + channels |= Channel(name="L4-12", port=modules["qcm_bb1"].ports("o2")) + channels |= Channel(name="L4-13", port=modules["qcm_bb1"].ports("o3")) + channels |= Channel(name="L4-14", port=modules["qcm_bb1"].ports("o4")) + channels |= Channel(name="L4-5", port=modules["qcm_bb2"].ports("o1")) # TWPA channels |= Channel(name="L3-28", port=None) channels["L3-28"].local_oscillator = twpa_pump @@ -98,8 +104,20 @@ def create(): for q in range(5): qubits[q].flux.max_bias = 2.5 + for i, coupler in enumerate(couplers): + couplers[coupler].flux = ( + channels[f"L4-{11 + i}"] if i > 0 else channels[f"L4-5"] + ) + couplers[coupler].flux.max_bias = 2.5 + settings = load_settings(runcard) return Platform( - str(FOLDER), qubits, pairs, instruments, settings, resonator_type="2D" + str(FOLDER), + qubits, + pairs, + instruments, + settings, + resonator_type="2D", + couplers=couplers, ) diff --git a/tests/test_backends.py b/tests/test_backends.py index 886d1a5bdc..bf5065723e 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -11,10 +11,11 @@ from qibolab.backends import QibolabBackend -def generate_circuit_with_gate(nqubits, gate, **kwargs): +def generate_circuit_with_gate(nqubits, gate, names, **kwargs): circuit = Circuit(nqubits) circuit.add(gate(qubit, **kwargs) for qubit in range(nqubits)) circuit.add(gates.M(*range(nqubits))) + circuit._wire_names = names return circuit @@ -23,11 +24,70 @@ def connected_backend(connected_platform): yield QibolabBackend(connected_platform) +def test_qubits(): + backend = QibolabBackend("dummy") + assert isinstance(backend.qubits, list) + assert set(backend.qubits) == {0, 1, 2, 3, 4} + + +def test_connectivity(): + backend = QibolabBackend("dummy") + assert isinstance(backend.connectivity, list) + assert set(backend.connectivity) == { + (0, 2), + (2, 0), + (1, 2), + (2, 1), + (2, 3), + (3, 2), + (2, 4), + (4, 2), + } + + +def test_natives(): + backend = QibolabBackend("dummy") + assert isinstance(backend.natives, list) + assert set(backend.natives) == { + "I", + "Z", + "RZ", + "U3", + "CZ", + "CNOT", + "GPI2", + "GPI", + "M", + } + + +def test_natives_no_cz_cnot(): + platform = create_platform("dummy") + backend = QibolabBackend(platform) + assert set(backend.natives) == { + "I", + "Z", + "RZ", + "U3", + "GPI2", + "GPI", + "M", + "CZ", + "CNOT", + } + + for gate in ["CZ", "CNOT"]: + for p in platform.pairs: + setattr(platform.pairs[p].native_gates, gate, None) + assert gate not in set(backend.natives) + + def test_execute_circuit_initial_state(): backend = QibolabBackend("dummy") circuit = Circuit(1) circuit.add(gates.GPI2(0, phi=0)) circuit.add(gates.M(0)) + circuit._wire_names = [0] with pytest.raises(ValueError): backend.execute_circuit(circuit, initial_state=np.ones(2)) @@ -49,7 +109,7 @@ def test_execute_circuit_initial_state(): def test_execute_circuit(gate, kwargs): backend = QibolabBackend("dummy") nqubits = backend.platform.nqubits - circuit = generate_circuit_with_gate(nqubits, gate, **kwargs) + circuit = generate_circuit_with_gate(nqubits, gate, list(range(nqubits)), **kwargs) result = backend.execute_circuit(circuit, nshots=100) @@ -59,12 +119,14 @@ def test_measurement_samples(): circuit = Circuit(nqubits) circuit.add(gates.M(*range(nqubits))) + circuit._wire_names = list(range(nqubits)) result = backend.execute_circuit(circuit, nshots=100) assert result.samples().shape == (100, nqubits) assert sum(result.frequencies().values()) == 100 circuit = Circuit(nqubits) circuit.add(gates.M(0, 2)) + circuit._wire_names = list(range(nqubits)) result = backend.execute_circuit(circuit, nshots=100) assert result.samples().shape == (100, 2) assert sum(result.frequencies().values()) == 100 @@ -77,6 +139,7 @@ def test_execute_circuits(): circuit = Circuit(3) circuit.add(gates.GPI2(i, phi=np.pi / 2) for i in range(3)) circuit.add(gates.M(0, 1, 2)) + circuit._wire_names = list(range(3)) results = backend.execute_circuits( 5 * [circuit], initial_states=initial_state_circuit, nshots=100 @@ -93,6 +156,7 @@ def test_multiple_measurements(): circuit = Circuit(4) circuit.add(gates.GPI2(i, phi=np.pi / 2) for i in range(2)) circuit.add(gates.CZ(1, 2)) + circuit._wire_names = list(range(4)) res0 = circuit.add(gates.M(0)) res1 = circuit.add(gates.M(3)) res2 = circuit.add(gates.M(1)) @@ -113,6 +177,7 @@ def dummy_string_qubit_names(): platform.pairs = { (f"A{q0}", f"A{q1}"): pair for (q0, q1), pair in platform.pairs.items() } + platform.wire_names = [f"A{q}" for q in range(platform.nqubits)] return platform @@ -124,6 +189,7 @@ def test_execute_circuit_str_qubit_names(): circuit.add(gates.GPI2(i, phi=np.pi / 2) for i in range(2)) circuit.add(gates.CZ(1, 2)) circuit.add(gates.M(0, 1)) + circuit._wire_names = ["A0", "A1", "A2"] result = backend.execute_circuit(circuit, nshots=20) assert result.samples().shape == (20, 2) @@ -137,6 +203,7 @@ def test_ground_state_probabilities_circuit(connected_backend): nqubits = connected_backend.platform.nqubits circuit = Circuit(nqubits) circuit.add(gates.M(*range(nqubits))) + circuit._wire_names = list(range(nqubits)) result = connected_backend.execute_circuit(circuit, nshots=nshots) freqs = result.frequencies(binary=False) probs = [freqs[i] / nshots for i in range(2**nqubits)] @@ -156,6 +223,7 @@ def test_excited_state_probabilities_circuit(connected_backend): circuit = Circuit(nqubits) circuit.add(gates.X(q) for q in range(nqubits)) circuit.add(gates.M(*range(nqubits))) + circuit._wire_names = list(range(nqubits)) result = connected_backend.execute_circuit(circuit, nshots=nshots) freqs = result.frequencies(binary=False) probs = [freqs[i] / nshots for i in range(2**nqubits)] @@ -179,6 +247,7 @@ def test_superposition_for_all_qubits(connected_backend): circuit = Circuit(nqubits) circuit.add(gates.GPI2(q=q, phi=np.pi / 2)) circuit.add(gates.M(q)) + circuit._wire_names = list(range(nqubits)) freqs = connected_backend.execute_circuit(circuit, nshots=nshots).frequencies( binary=False ) diff --git a/tests/test_compilers_default.py b/tests/test_compilers_default.py index a1a85c3667..5ba3d95f18 100644 --- a/tests/test_compilers_default.py +++ b/tests/test_compilers_default.py @@ -32,6 +32,8 @@ def test_u3_sim_agreement(): def compile_circuit(circuit, platform): """Compile a circuit to a pulse sequence.""" compiler = Compiler.default() + # Temporary fix: overwrite the wire names + circuit._wire_names = list(platform.qubits) sequence, _ = compiler.compile(circuit, platform) return sequence @@ -183,6 +185,21 @@ def test_cz_to_sequence(platform): assert sequence == test_sequence +def test_twocz_to_sequence(platform): + if (1, 2) not in platform.pairs: + pytest.skip( + f"Skipping CZ test for {platform} because pair (1, 2) is not available." + ) + + circuit = Circuit(3) + circuit.add(gates.CZ(1, 2)) + circuit.add(gates.CZ(1, 2)) + + sequence = compile_circuit(circuit, platform) + assert sequence[0].relative_phase == 0 + assert sequence[1].relative_phase == 0 + + def test_cnot_to_sequence(): platform = create_platform("dummy") circuit = Circuit(4) diff --git a/tests/test_dummy.py b/tests/test_dummy.py index 42f454f252..145e9a74d6 100644 --- a/tests/test_dummy.py +++ b/tests/test_dummy.py @@ -88,7 +88,7 @@ def test_dummy_execute_pulse_sequence_couplers(): result = platform.execute_pulse_sequence(sequence, options) test_pulses = "PulseSequence\nFluxPulse(0, 30, 0.05, GaussianSquare(5, 0.75), flux-2, 2)\nCouplerFluxPulse(0, 30, 0.05, GaussianSquare(5, 0.75), flux_coupler-1, 1)" - test_phases = {1: 0.0, 2: 0.0} + test_phases = {1: 0.1, 2: 0.2} assert test_pulses == cz.serial assert test_phases == cz_phases diff --git a/tests/test_instruments_qblox_controller.py b/tests/test_instruments_qblox_controller.py index fde3682f07..c5c5fba003 100644 --- a/tests/test_instruments_qblox_controller.py +++ b/tests/test_instruments_qblox_controller.py @@ -61,6 +61,25 @@ def test_sweep_too_many_sweep_points(platform, controller): controller.sweep({0: qubit}, {}, PulseSequence(pulse), params, sweep) +def test_sweep_coupler(platform, controller): + """Test that coupler related sweep is accepted.""" + ro_pulse = platform.create_MZ_pulse(qubit=0, start=0) + sequence = PulseSequence(ro_pulse) + + sweeper = Sweeper(Parameter.bias, np.random.rand(4), couplers=[0]) + params = ExecutionParameters( + nshots=10, relaxation_time=1000, averaging_mode=AveragingMode.CYCLIC + ) + mock_data = np.array([1, 2, 3, 4]) + controller._execute_pulse_sequence = Mock( + return_value={ro_pulse.serial: IntegratedResults(mock_data)} + ) + res = controller.sweep( + platform.qubits, platform.couplers, sequence, params, sweeper + ) + assert np.array_equal(res[ro_pulse.serial].voltage, mock_data) + + @pytest.mark.qpu def connect(connected_controller: QbloxController): connected_controller.connect() diff --git a/tests/test_instruments_qm.py b/tests/test_instruments_qm.py index fee8633709..f018e554e5 100644 --- a/tests/test_instruments_qm.py +++ b/tests/test_instruments_qm.py @@ -158,7 +158,7 @@ def test_qm_register_port(qmcontroller, offset): controllers = qmcontroller.config.controllers assert controllers == { "con1": { - "analog_inputs": {1: {}, 2: {}}, + "analog_inputs": {1: {"offset": 0}, 2: {"offset": 0}}, "analog_outputs": {1: {"offset": offset, "filter": {}}}, "digital_outputs": {}, } @@ -173,7 +173,7 @@ def test_qm_register_port_filter(qmcontroller): controllers = qmcontroller.config.controllers assert controllers == { "con1": { - "analog_inputs": {1: {}, 2: {}}, + "analog_inputs": {1: {"offset": 0}, 2: {"offset": 0}}, "analog_outputs": { 2: { "filter": {"feedback": [0.95], "feedforward": [1, -1]}, diff --git a/tests/test_pulses.py b/tests/test_pulses.py index 9939b8faec..62f7a64b63 100644 --- a/tests/test_pulses.py +++ b/tests/test_pulses.py @@ -12,6 +12,7 @@ Custom, Drag, DrivePulse, + Exponential, FluxPulse, Gaussian, GaussianSquare, @@ -275,8 +276,70 @@ def test_pulses_pulseshape_sampling_rate(shape): def test_pulseshape_eval(): shape = PulseShape.eval("Rectangular()") assert isinstance(shape, Rectangular) - with pytest.raises(ValueError): - shape = PulseShape.eval("Ciao()") + + shape = PulseShape.eval("Exponential(1, 2)") + assert isinstance(shape, Exponential) + assert shape.tau == 1 + assert shape.upsilon == 2 + + shape = PulseShape.eval("Exponential(4, 5, 6)") + assert isinstance(shape, Exponential) + assert shape.tau == 4 + assert shape.upsilon == 5 + assert shape.g == 6 + + shape = PulseShape.eval("Gaussian(3.1)") + assert isinstance(shape, Gaussian) + assert shape.rel_sigma == 3.1 + + shape = PulseShape.eval("GaussianSquare(5, 78)") + assert isinstance(shape, GaussianSquare) + assert shape.rel_sigma == 5 + assert shape.width == 78 + + shape = PulseShape.eval("Drag(4, 0.1)") + assert isinstance(shape, Drag) + assert shape.rel_sigma == 4 + assert shape.beta == 0.1 + + shape = PulseShape.eval("IIR([1, 2, 3], [5], Drag(3, 0.2))") + assert isinstance(shape, IIR) + assert np.array_equal(shape.b, np.array([1, 2, 3])) + assert np.array_equal(shape.a, np.array([5])) + assert isinstance(shape.target, Drag) + assert shape.target.rel_sigma == 3 + assert shape.target.beta == 0.2 + + shape = PulseShape.eval("SNZ(10, 20)") + assert isinstance(shape, SNZ) + assert shape.t_idling == 10 + assert shape.b_amplitude == 20 + + shape = PulseShape.eval("eCap(3.14)") + assert isinstance(shape, eCap) + assert shape.alpha == 3.14 + + shape = PulseShape.eval("Custom([1, 2, 3], [4, 5, 6])") + assert isinstance(shape, Custom) + assert np.array_equal(shape.envelope_i, np.array([1, 2, 3])) + assert np.array_equal(shape.envelope_q, np.array([4, 5, 6])) + + with pytest.raises(ValueError, match="shape .* not recognized"): + _ = PulseShape.eval("Ciao()") + + +@pytest.mark.parametrize( + "value_str", + ["-0.1", "+0.1", "1.", "-3.", "+1.", "-0.1e2", "1e-2", "+1e3", "-3e-1", "-.4"], +) +def test_pulse_shape_eval_numeric_varieties(value_str): + shape = PulseShape.eval(f"Drag(1, {value_str})") + assert isinstance(shape, Drag) + assert shape.beta == float(value_str) + + shape = PulseShape.eval(f"Custom([0.1, {value_str}])") + assert isinstance(shape, Custom) + assert np.array_equal(shape.envelope_i, np.array([0.1, float(value_str)])) @pytest.mark.parametrize("rel_sigma,beta", [(5, 1), (5, -1), (3, -0.03), (4, 0.02)]) diff --git a/tests/test_result.py b/tests/test_result.py index e861f56796..7ba339bbae 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -71,7 +71,7 @@ def test_integrated_result_properties(result): np.sqrt(results.voltage_i**2 + results.voltage_q**2), results.magnitude ) np.testing.assert_equal( - np.unwrap(np.arctan2(results.voltage_i, results.voltage_q)), results.phase + np.unwrap(np.arctan2(results.voltage_q, results.voltage_i)), results.phase ) @@ -119,7 +119,7 @@ def test_serialize(average, result): "MSR[V]": np.sqrt(avg.voltage_i**2 + avg.voltage_q**2), "i[V]": avg.voltage_i, "q[V]": avg.voltage_q, - "phase[rad]": np.unwrap(np.arctan2(avg.voltage_i, avg.voltage_q)), + "phase[rad]": np.unwrap(np.arctan2(avg.voltage_q, avg.voltage_i)), } assert avg.serialize.keys() == target_dict.keys() for key in output: @@ -155,7 +155,7 @@ def test_serialize_averaged_iq_results(result): "MSR[V]": np.sqrt(results.voltage_i**2 + results.voltage_q**2), "i[V]": results.voltage_i, "q[V]": results.voltage_q, - "phase[rad]": np.unwrap(np.arctan2(results.voltage_i, results.voltage_q)), + "phase[rad]": np.unwrap(np.arctan2(results.voltage_q, results.voltage_i)), } assert output.keys() == target_dict.keys() for key in output: