diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa754771..d1b4ea9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,21 +15,8 @@ env: RAW_VERSION: ${{ github.ref_name }} jobs: - # ------------------------------------------------------------------------- - # Core lint-and-test matrix (server + driver) - # ------------------------------------------------------------------------- lint-and-test-server-and-driver: runs-on: ubuntu-latest - strategy: - matrix: - go-version: ['1.25.x'] - python-version: ['3.12'] - node-version: ['20.x'] - # 'sim' is not an executor — it is the simulated chip the tier-3 - # physics tests and the whole calibrate/execute loop run against. - # Without it here, none of those ever run on CI. - extra: ['mock', 'aer', 'quantify', 'qblox', 'sim'] - steps: - name: Checkout Code uses: actions/checkout@v4 @@ -37,80 +24,106 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version }} + go-version: '1.26.x' cache-dependency-path: 'qpi-ui/go.sum' - name: Set up uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b with: - python-version: ${{ matrix.python-version }} + python-version: '3.12' enable-cache: true cache-dependency-glob: "qpi-driver/py/pyproject.toml" - name: Set up nodejs uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: '22.x' - - name: Install dependencies & formatting tools - run: | - if [ "${{ matrix.extra }}" = "mock" ]; then - uv sync --project qpi-driver/py --extra cli --dev - elif [ "${{ matrix.extra }}" = "aer" ]; then - uv sync --project qpi-driver/py --extra cli --extra aer --dev - elif [ "${{ matrix.extra }}" = "quantify" ]; then - uv sync --project qpi-driver/py --extra cli --extra quantify --dev - elif [ "${{ matrix.extra }}" = "qblox" ]; then - uv sync --project qpi-driver/py --extra cli --extra qblox --dev - elif [ "${{ matrix.extra }}" = "sim" ]; then - # Both schedulers: the loop tests are parametrised over them. - uv sync --project qpi-driver/py --extra cli --extra quantify --extra qblox --extra sim --dev - fi - sudo apt-get update && sudo apt-get install -y zip + - name: Install dependencies + run: uv sync --project qpi-driver/py --extra cli --dev - - name: Build dashboard - run: make build-dashboard + # Builds the dashboard itself, which `go vet` needs: main.go embeds its dist. + - name: Lint orchestrator + run: make lint-go - - name: Lint ochestrator - run: make lint-go - - name: Lint driver run: make lint-py - - name: Test CLI + - name: Test CLI run: make test-py-cli + + - name: Test simulator + run: | + make test-py-sim + + lint-and-test-server-and-driver-per-extra: + runs-on: ubuntu-latest + strategy: + matrix: + extra: ['mock', 'aer', 'quantify', 'qblox'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.26.x' + cache-dependency-path: 'qpi-ui/go.sum' + + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + python-version: '3.12' + enable-cache: true + cache-dependency-glob: "qpi-driver/py/pyproject.toml" + + - name: Set up nodejs + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Install dependencies + run: uv sync --project qpi-driver/py --extra cli --extra ${{ matrix.extra }} --dev + + - name: Build dashboard + run: make build-dashboard - name: Run Python Unit Tests for '${{ matrix.extra }}' run: | - if [ "${{ matrix.extra }}" = "mock" ]; then - make test-py-base - elif [ "${{ matrix.extra }}" = "aer" ]; then - make test-py-aer - elif [ "${{ matrix.extra }}" = "quantify" ]; then - make test-py-quantify - elif [ "${{ matrix.extra }}" = "qblox" ]; then - make test-py-qblox - elif [ "${{ matrix.extra }}" = "sim" ]; then - # The routines against real physics, then the whole loop: calibrate a - # simulated chip and run circuits on what the calibration wrote. - make test-py-sim - make test-py-loop - fi - - # 'sim' names no executor a driver can be started with, so there is no - # driver end-to-end run for it — its own end-to-end coverage is test-py-loop. + make test-py-driver EXECUTOR=${{ matrix.extra }} + - name: Run E2E Integration Tests for driver '${{ matrix.extra }}' - if: matrix.extra != 'sim' run: make test-e2e-driver EXECUTOR=${{ matrix.extra }} + + test-simulated-calibrate-and-process-loop: + runs-on: ubuntu-latest + strategy: + matrix: + extra: ['quantify', 'qblox'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + python-version: '3.12' + enable-cache: true + cache-dependency-glob: "qpi-driver/py/pyproject.toml" + + - name: Run Tests for '${{ matrix.extra }}' + run: | + make test-py-loop EXECUTOR=${{ matrix.extra }} + # ------------------------------------------------------------------------- # qpi-client / Go SDK # ------------------------------------------------------------------------- lint-and-test-go-client: runs-on: ubuntu-latest - strategy: - matrix: - go-version: ['1.25.x'] steps: - name: Checkout Code @@ -119,7 +132,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version }} + go-version: '1.26.x' cache-dependency-path: 'qpi-client/go/go.sum' @@ -165,7 +178,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: ['20.x'] + node-version: ['22.x'] steps: - name: Checkout Code @@ -191,7 +204,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.25.x'] + go-version: ['1.26.x'] steps: - name: Checkout Code @@ -216,7 +229,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: ['20.x'] + node-version: ['22.x'] steps: - name: Checkout Code @@ -270,6 +283,11 @@ jobs: - name: Lint dashboard run: make lint-dashboard + # The pure helpers, before the four-minute Cypress run: a broken one fails + # here in seconds instead of as a puzzling assertion against a live server. + - name: Run Dashboard Unit Tests + run: make test-dashboard + - name: Run E2E Cypress Dashboard Tests run: make test-e2e-dashboard @@ -328,7 +346,9 @@ jobs: # Publish qpi-driver to PyPI # ------------------------------------------------------------------------- publish-pypi-driver: - needs: lint-and-test-server-and-driver + needs: + - lint-and-test-server-and-driver-per-extra + - lint-and-test-server-and-driver runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && vars.PUBLISH_TO_PYPI == 'true' environment: @@ -454,7 +474,10 @@ jobs: # Publish Go server binaries to GitHub Release # ------------------------------------------------------------------------- publish-go-release: - needs: [lint-and-test-server-and-driver, test-dashboard-cypress] + needs: + - lint-and-test-server-and-driver + - lint-and-test-server-and-driver + - test-dashboard-cypress runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && vars.PUBLISH_GO_APP == 'true' permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e08db3..cdbad7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project follows versions of format `{year}.{month}.{patch_number}`. ## [Unreleased] +### Changed + +- `repo`: Cleaned up and refactored `Makefile`. +- `repo`: Cleaned up `.github/workflows/ci.yml`. +- `qpi-driver/py`: Optimized `test-py-loop` execution speed with +`@functools.lru_cache` to `_cached_scqubits_eigenvals` in `transmon.py`. + ## [0.4.1] - 2026-08-07 ### Fixed diff --git a/Makefile b/Makefile index c8b3b6ed..4d8a5cd5 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -.PHONY: test-dashboard test-docs test-docs-static test-docs-snippets test-docs-example test-docs-site all build build-dashboard test test-js-driver test-go-driver lint lint-go lint-py lint-js lint-dashboard lint-go-client lint-py-client lint-js-driver lint-go-driver format format-go format-py format-js format-dashboard format-go-client format-py-client format-js-driver format-go-driver package package-driver package-driver-js package-driver-go package-js package-py package-go publish-js publish-driver-js publish-py clean venv-check test-e2e-dashboard - VERSION ?= 0.4.1 UV := $(shell command -v uv 2> /dev/null || echo "$$HOME/.local/bin/uv") +EXECUTOR ?= mock +EXECUTORS := mock aer quantify qblox # Scratch locations for the documentation checks. Under bin/, which is already # ignored, so a failed run leaves nothing in the working tree for git to notice. @@ -9,6 +9,33 @@ DOCS_EXAMPLE_VENV := bin/.docs-example-venv DOCS_SITE_VENV := bin/.docs-site-venv DOCS_SITE_OUT := bin/.docs-site +# The framework modules the coverage floor applies to: the SDK, the CLI, the device +# registry and its options, and the executors that need no hardware. Everything else +# is reported but not gated — see cov-py. +PY_COV_INCLUDE := qpi_driver/cli.py,qpi_driver/sdk.py,qpi_driver/events.py,qpi_driver/paths.py,qpi_driver/options.py,qpi_driver/builtins/*.py,qpi_driver/executors/__init__.py,qpi_driver/executors/base/*.py,qpi_driver/executors/mock/*.py +PY_COV_MIN := 96 + +# `uv sync` reinstalls qblox_instruments, and macOS strips the code signature +# from the q1asm assembler it bundles — after which every schedule that +# assembles dies with "Assembly failed". So each target that syncs has to put +# the signature back. Silent and `|| true`: on Linux, and in an environment +# without qblox_instruments at all, there is nothing to sign and that is fine. +RESIGN_Q1ASM = @if [ "$$(uname)" = "Darwin" ]; then codesign --force --deep --sign - qpi-driver/py/.venv/lib/python3.12/site-packages/qblox_instruments/assemblers/q1asm_macos 2>/dev/null || true; fi + +# The Go packages the coverage floor applies to: the device registry and the CLI over +# it, both of which need no server. The base SDK (driver.go: Run, recvLoop, the TLS +# dialling) and `qpi-driver/main.go` are reported but not gated — the first needs a +# live server and is covered by `make test-e2e-driver`, and the second is `main`, +# which no in-process test can call. `cli.Execute` is inside a gated package but +# calls os.Exit, hence 94 rather than 96. +GO_COV_GATED := ./devices/... ./cli/... +GO_COV_MIN := 94 + +# --------------------------------------------------------------------------- +# Build & Setup targets +# --------------------------------------------------------------------------- +.PHONY: all build build-dashboard serve-docs venv-check + all: build # Automatically create the virtual environment if not already in one and uv is missing. @@ -18,13 +45,6 @@ venv-check: curl -LsSf https://astral.sh/uv/install.sh | sh; \ fi -# `uv sync` reinstalls qblox_instruments, and macOS strips the code signature -# from the q1asm assembler it bundles — after which every schedule that -# assembles dies with "Assembly failed". So each target that syncs has to put -# the signature back. Silent and `|| true`: on Linux, and in an environment -# without qblox_instruments at all, there is nothing to sign and that is fine. -RESIGN_Q1ASM = @if [ "$$(uname)" = "Darwin" ]; then codesign --force --deep --sign - qpi-driver/py/.venv/lib/python3.12/site-packages/qblox_instruments/assemblers/q1asm_macos 2>/dev/null || true; fi - build: venv-check build-dashboard @echo "Building Go server..." mkdir -p bin @@ -51,6 +71,13 @@ serve-docs: # --------------------------------------------------------------------------- # Test targets # --------------------------------------------------------------------------- +.PHONY: test test-docs test-docs-static test-docs-snippets test-docs-example test-docs-site \ + test-go test-py test-py-driver \ + test-py-cli test-py-sim test-py-loop \ + test-dashboard test-js-client test-go-client test-py-client \ + test-js-driver test-go-driver test-e2e test-e2e-driver \ + test-e2e-client-py test-e2e-client-js test-e2e-client-go \ + test-e2e-dashboard test-e2e-dashboard-visual test-e2e-systemd test: test-go test-py test-js-client test-go-client test-py-client test-js-driver test-go-driver test-dashboard test-docs test-e2e @@ -126,21 +153,15 @@ test-go: build-dashboard @echo "Running Go unit tests (server)..." (cd qpi-ui && go test -race -v ./...) -test-go-minimal: - @echo "Running Go server unit tests..." - (cd qpi-ui && go test -race -cover ./...) - -test-py: test-py-base test-py-cli test-py-aer test-py-quantify test-py-qblox test-py-sim test-py-loop - -# The framework modules the coverage floor applies to: the SDK, the CLI, the device -# registry and its options, and the executors that need no hardware. Everything else -# is reported but not gated — see cov-py. -PY_COV_INCLUDE := qpi_driver/cli.py,qpi_driver/sdk.py,qpi_driver/events.py,qpi_driver/paths.py,qpi_driver/options.py,qpi_driver/builtins/*.py,qpi_driver/executors/__init__.py,qpi_driver/executors/base/*.py,qpi_driver/executors/mock/*.py -PY_COV_MIN := 96 +test-py: test-py-sim + @for exec in $(EXECUTORS); do \ + $(MAKE) test-py-driver EXECUTOR=$$exec || exit 1; \ + $(MAKE) test-py-loop EXECUTOR=$$exec || exit 1; \ + done -test-py-base: - @echo "Running Python driver tests with base deps only (mock executor)..." - $(UV) sync --project qpi-driver/py --dev +test-py-driver: + @echo "Running Python driver tests with [$(EXECUTOR)] extra..." + $(UV) sync --project qpi-driver/py --extra $(EXECUTOR) --dev $(RESIGN_Q1ASM) $(UV) run --project qpi-driver/py pytest qpi-driver/py/tests/ -v @@ -158,29 +179,6 @@ test-py-cli: -(cd qpi-driver/py && $(UV) run coverage report \ --include='qpi_driver/executors/qblox/*,qpi_driver/executors/quantify/*,qpi_driver/executors/presto/*,qpi_driver/executors/qiskit_aer/*,qpi_driver/executors/utils/*,qpi_driver/compat/*') -test-py-aer: - @echo "Running Python driver tests with [aer] extra..." - $(UV) sync --project qpi-driver/py --extra aer --dev - $(RESIGN_Q1ASM) - $(UV) run --project qpi-driver/py pytest qpi-driver/py/tests/ -v - -test-py-quantify: - @echo "Running Python driver tests with [quantify] extra..." - $(UV) sync --project qpi-driver/py --extra quantify --dev - $(RESIGN_Q1ASM) - $(UV) run --project qpi-driver/py pytest qpi-driver/py/tests/ -v - -test-py-qblox: - @echo "Running Python driver tests with [qblox] extra..." - $(UV) sync --project qpi-driver/py --extra qblox --dev - $(RESIGN_Q1ASM) - $(UV) run --project qpi-driver/py pytest qpi-driver/py/tests/ -v - -# Tier 3 of the calibration testing strategy (RFC 0004 §7): the routines against -# acquisition data generated from a real transmon Hamiltonian (scqubits) and the -# Lindblad master equation (qutip), rather than from the analytic form each fit -# already assumes — and then the whole calibration end to end over that same -# simulator. Needs no scheduler: it supplies the acquisition itself. test-py-sim: @echo "Running Python calibration tests against the physics simulator..." $(UV) sync --project qpi-driver/py --extra sim --dev @@ -189,22 +187,11 @@ test-py-sim: qpi-driver/py/tests/test_physics_simulation.py \ qpi-driver/py/tests/test_calibration_e2e.py -# The two operations against one another, with only the cluster replaced: a -# tuner calibrates a simulated chip, writes quantify.device.yml, and an executor -# loads that file and runs circuits on the same chip. Needs a scheduler *and* -# the simulator, which is why it is its own environment rather than folded into -# test-py-quantify or test-py-sim. test-py-loop: - @echo "Running the calibrate/process loop against the simulated chip..." - # Both schedulers: the loop is parametrised over them, and syncing only one - # silently skips half the tests — which is how qblox stayed a stub. - $(UV) sync --project qpi-driver/py --extra quantify --extra qblox --extra sim --dev + @echo "Running the calibrate/process loop against the $(EXECUTOR) simulated chip..." + $(UV) sync --project qpi-driver/py --extra $(EXECUTOR) --extra sim --dev $(RESIGN_Q1ASM) # --no-sync, because `uv run` otherwise re-syncs to the project's *default* - # dependency set and prunes the `sim` group the line above just installed. Then - # every test in the file skips on `importorskip("scqubits")` and pytest exits 0, - # so the target reports success having run nothing. Seen when this follows - # test-py-sim in one `make test`, which is the order `make test` uses. $(UV) run --no-sync --project qpi-driver/py pytest \ qpi-driver/py/tests/test_calibration_loop.py -v @@ -223,10 +210,6 @@ test-go-client: @echo "Running Go client tests..." (cd qpi-client/go && go test -race -v ./...) -test-go-client-minimal: - @echo "Running Go client tests..." - (cd qpi-client/go && go test -race -cover ./...) - test-py-client: @echo "Running Python client tests..." $(UV) sync --project qpi-client/py --dev @@ -237,15 +220,6 @@ test-js-driver: @echo "Running JS/TS driver SDK tests..." (cd qpi-driver/js && npm ci && npm test) -# The Go packages the coverage floor applies to: the device registry and the CLI over -# it, both of which need no server. The base SDK (driver.go: Run, recvLoop, the TLS -# dialling) and `qpi-driver/main.go` are reported but not gated — the first needs a -# live server and is covered by `make test-e2e-driver`, and the second is `main`, -# which no in-process test can call. `cli.Execute` is inside a gated package but -# calls os.Exit, hence 94 rather than 96. -GO_COV_GATED := ./devices/... ./cli/... -GO_COV_MIN := 94 - test-go-driver: @echo "Running Go driver SDK tests..." (cd qpi-driver/go && go test -race -v ./...) @@ -259,10 +233,6 @@ test-go-driver: @echo "Coverage of the base SDK transport, for information only:" -(cd qpi-driver/go && go test -cover ./... | grep coverage) -test-go-driver-minimal: - @echo "Running Go driver SDK tests..." - (cd qpi-driver/go && go test -race -cover ./...) - test-e2e: test-e2e-driver test-e2e-client-py test-e2e-client-js test-e2e-client-go test-e2e-dashboard test-e2e-systemd test-e2e-driver: @@ -299,6 +269,7 @@ test-e2e-systemd: # --------------------------------------------------------------------------- # Lint targets # --------------------------------------------------------------------------- +.PHONY: lint lint-go lint-py lint-js lint-dashboard lint-go-client lint-py-client lint-js-driver lint-go-driver lint: lint-go lint-py lint-js lint-dashboard lint-go-client lint-py-client lint-js-driver lint-go-driver @@ -343,6 +314,7 @@ lint-go-driver: # --------------------------------------------------------------------------- # Format targets # --------------------------------------------------------------------------- +.PHONY: format format-go format-py format-js format-dashboard format-go-client format-py-client format-js-driver format-go-driver format: format-go format-py format-js format-dashboard format-go-client format-py-client format-js-driver format-go-driver @@ -383,6 +355,11 @@ format-go-driver: # --------------------------------------------------------------------------- # Package / Publish targets # --------------------------------------------------------------------------- +.PHONY: package package-js package-py package-driver package-driver-js package-driver-go package-go \ + publish-js publish-driver-js publish-py + +package: package-js package-py package-driver package-driver-js + package-js: @echo "Packaging JS client..." (cd qpi-client/js && npm ci && npm run build) @@ -423,6 +400,7 @@ publish-py: # --------------------------------------------------------------------------- # Clean # --------------------------------------------------------------------------- +.PHONY: clean clean: @echo "Cleaning up..." diff --git a/docs/rfcs/0004-calibration-tuners.md b/docs/rfcs/0004-calibration-tuners.md index 9ca56360..ed798977 100644 --- a/docs/rfcs/0004-calibration-tuners.md +++ b/docs/rfcs/0004-calibration-tuners.md @@ -764,13 +764,13 @@ alone. The last column below is part of the design, not bookkeeping: | Test file | Tier | Runs under | Tests | |-----------|------|------------|-------| -| `tests/test_calibration_dag.py` | 1 | `test-py-base` | DAG topological sort, cycle detection, partial DAG, `recalibrate`'s narrowing | -| `tests/test_calibration_config.py` | 1 | `test-py-base` | Config parsing, defaults, validation | -| `tests/test_fitting.py` | 1 | `test-py-base` | All fitting functions against synthetic data | -| `tests/test_persistence.py` | 1 | `test-py-base` | YAML write-back round-trip | -| `tests/test_clifford.py` | 1 | `test-py-base` | Clifford group generation + inverse correctness | -| `tests/test_calibrate_driver.py` | 1 | `test-py-base` | Driver event handling, worker lifecycle — over a stub `Tuner`, no scheduler | -| `tests/test_tuner_routines.py` | 2 | `test-py-quantify` + `test-py-qblox` | Schedule compilation for each routine | +| `tests/test_calibration_dag.py` | 1 | `test-py-driver EXECUTOR=mock` | DAG topological sort, cycle detection, partial DAG, `recalibrate`'s narrowing | +| `tests/test_calibration_config.py` | 1 | `test-py-driver EXECUTOR=mock` | Config parsing, defaults, validation | +| `tests/test_fitting.py` | 1 | `test-py-driver EXECUTOR=mock` | All fitting functions against synthetic data | +| `tests/test_persistence.py` | 1 | `test-py-driver EXECUTOR=mock` | YAML write-back round-trip | +| `tests/test_clifford.py` | 1 | `test-py-driver EXECUTOR=mock` | Clifford group generation + inverse correctness | +| `tests/test_calibrate_driver.py` | 1 | `test-py-driver EXECUTOR=mock` | Driver event handling, worker lifecycle — over a stub `Tuner`, no scheduler | +| `tests/test_tuner_routines.py` | 2 | `test-py-driver EXECUTOR=quantify` + `test-py-driver EXECUTOR=qblox` | Schedule compilation for each routine | | `tests/test_physics_simulation.py` | 3 | `test-py-sim` | Routines against scqubits/qutip data; RB against real Clifford unitaries; the CZ routines against a coupled pair; the readout resonator's own lineshape and punchout curve | | `tests/test_calibration_e2e.py` | 3 | `test-py-sim` | A whole calibration through `_execute_calibration`: full, partial, drift and the job it queues, write-back | | `tests/fixtures/simulation.py` | 3 | — | The backends, fake device and tuner built on the simulators | @@ -783,7 +783,7 @@ testable without a lab: the tuner is resolved by name, class *or instance* hardware. There is deliberately **no** dedicated calibration Makefile target. The tier-1 files run -under `test-py-base`, the tier-2 file runs under the two existing scheduler +under `test-py-driver EXECUTOR=mock`, the tier-2 file runs under the two existing scheduler targets, and a sixth target would mean a sixth environment that installs a scheduler in order to run tests that do not need one. §9 lists the targets that actually cover this feature. @@ -838,10 +838,10 @@ this has nothing. A driver warns once per change and keeps running. ### Automated tests ```bash -make test-py-base # Fitting, DAG, config, Clifford, persistence, driver lifecycle -make test-py-cli # the same suite under [cli], plus the coverage floor on the CLI and registry -make test-py-quantify # quantify_tuner routine compilation (dummy Cluster) -make test-py-qblox # qblox_tuner routine compilation (dummy Cluster) +make test-py-driver EXECUTOR=mock # Fitting, DAG, config, Clifford, persistence, driver lifecycle +make test-py-cli # the same suite under [cli], plus the coverage floor on the CLI and registry +make test-py-driver EXECUTOR=quantify # quantify_tuner routine compilation (dummy Cluster) +make test-py-driver EXECUTOR=qblox # qblox_tuner routine compilation (dummy Cluster) make test-py-sim # the routines against scqubits/qutip physics make test-py-loop # the whole DAG, then circuits on what it wrote — both schedulers make test-go # Event routing, handler, dispatcher queue, catalog invariants diff --git a/qpi-driver/py/qpi_driver/executors/qblox/__init__.py b/qpi-driver/py/qpi_driver/executors/qblox/__init__.py index 093d62fc..564fa700 100644 --- a/qpi-driver/py/qpi_driver/executors/qblox/__init__.py +++ b/qpi-driver/py/qpi_driver/executors/qblox/__init__.py @@ -22,7 +22,6 @@ load_quantum_device, ) from qpi_driver.executors.qblox.conv import generate_schedule -from qpi_driver.reload import ConfigFile from qpi_driver.executors.utils.batch import ( combine_circuit_datasets, iter_circuit_datasets, @@ -40,6 +39,7 @@ ) from qpi_driver.executors.utils.qiskit import load_qasm, measured_qubits from qpi_driver.executors.utils.types import cast_to +from qpi_driver.reload import ConfigFile log = logging.getLogger(__name__) diff --git a/qpi-driver/py/qpi_driver/executors/quantify/__init__.py b/qpi-driver/py/qpi_driver/executors/quantify/__init__.py index 278060d5..f1373500 100644 --- a/qpi-driver/py/qpi_driver/executors/quantify/__init__.py +++ b/qpi-driver/py/qpi_driver/executors/quantify/__init__.py @@ -28,7 +28,6 @@ load_quantum_device, ) from qpi_driver.executors.quantify.conv import to_quantify_gates -from qpi_driver.reload import ConfigFile from qpi_driver.executors.utils.batch import ( combine_circuit_datasets, iter_circuit_datasets, @@ -46,6 +45,7 @@ ) from qpi_driver.executors.utils.qiskit import load_qasm, measured_qubits from qpi_driver.executors.utils.types import cast_to +from qpi_driver.reload import ConfigFile log = logging.getLogger(__name__) diff --git a/qpi-driver/py/qpi_driver/simulation/transmon.py b/qpi-driver/py/qpi_driver/simulation/transmon.py index d2ecc21f..6d8a4e84 100644 --- a/qpi-driver/py/qpi_driver/simulation/transmon.py +++ b/qpi-driver/py/qpi_driver/simulation/transmon.py @@ -16,6 +16,7 @@ this package. """ +import functools from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any @@ -40,6 +41,16 @@ _RABI_GHZ_PER_UNIT = 1.0 / (2 * 0.2 * 20.0) +@functools.lru_cache(maxsize=256) +def _cached_scqubits_eigenvals( + EJ: float, EC: float, ng: float, ncut: int, evals_count: int +) -> tuple[float, ...]: + import scqubits + + transmon = scqubits.Transmon(EJ=EJ, EC=EC, ng=ng, ncut=ncut) + return tuple(transmon.eigenvals(evals_count=evals_count)) + + def require_simulation_deps() -> None: """Raise unless the ``sim`` extra is installed. @@ -145,10 +156,10 @@ def __post_init__(self) -> None: def eigenvalues(self, count: int | None = None) -> np.ndarray: """Transmon eigenenergies in GHz, by diagonalising the real Hamiltonian.""" - import scqubits - - transmon = scqubits.Transmon(EJ=self.EJ, EC=self.EC, ng=self.ng, ncut=self.ncut) - return transmon.eigenvals(evals_count=count or self.levels) + evals = _cached_scqubits_eigenvals( + self.EJ, self.EC, self.ng, self.ncut, count or self.levels + ) + return np.array(evals) @property def f01(self) -> float: diff --git a/qpi-driver/py/qpi_driver/tuners/base/__init__.py b/qpi-driver/py/qpi_driver/tuners/base/__init__.py index 42789e58..8843126b 100644 --- a/qpi-driver/py/qpi_driver/tuners/base/__init__.py +++ b/qpi-driver/py/qpi_driver/tuners/base/__init__.py @@ -12,6 +12,7 @@ class does the rest. from pathlib import Path from typing import Any +from qpi_driver.reload import ConfigFile from qpi_driver.tuners.base.backend import SchedulerBackend from qpi_driver.tuners.base.config import ( CalibrationConfig, @@ -31,7 +32,6 @@ class does the rest. RoutineError, ) from qpi_driver.tuners.routines import all_routines, routine_names -from qpi_driver.reload import ConfigFile from qpi_driver.tuners.utils.persistence import apply_device_config, save_device_config log = logging.getLogger(__name__) diff --git a/qpi-driver/py/qpi_driver/tuners/quantify/__init__.py b/qpi-driver/py/qpi_driver/tuners/quantify/__init__.py index 900a8996..5f39edc6 100644 --- a/qpi-driver/py/qpi_driver/tuners/quantify/__init__.py +++ b/qpi-driver/py/qpi_driver/tuners/quantify/__init__.py @@ -15,6 +15,7 @@ DRAGPulse, IdlePulse, Instrument, + InstrumentCoordinator, Measure, Reset, Rxy, @@ -74,7 +75,9 @@ def drag_pulse(self, *, amp, drag, duration, port, clock, phase_deg=0.0): # Dimensionless: the derivative component as a fraction of the Gaussian. drag_span = 0.2 - def __init__(self, compiler: Any, instrument_coordinator: Any) -> None: + def __init__( + self, compiler: SerialCompiler, instrument_coordinator: InstrumentCoordinator + ) -> None: self._compiler = compiler self._instrument_coordinator = instrument_coordinator @@ -85,6 +88,8 @@ def run( self, schedule: Any, timeout_s: float = DEFAULT_ROUTINE_TIMEOUT_S ) -> xr.Dataset: compiled = self._compiler.compile(schedule) + # FIXME: Try to log the compiled schedule to see how wrong it could be + # log.info(compiled.to_json()) self._instrument_coordinator.prepare(compiled) self._instrument_coordinator.start() # Floored to whole minutes downstream with a minimum of one, so a ceiling diff --git a/qpi-driver/py/quantify.device.example.yml b/qpi-driver/py/quantify.device.example.yml index 3cf98392..4ee39728 100644 --- a/qpi-driver/py/quantify.device.example.yml +++ b/qpi-driver/py/quantify.device.example.yml @@ -17,6 +17,9 @@ q0: pulse_duration: 300e-9 acq_delay: 100e-9 integration_time: 1e-6 + # FIXME: Add all parameters for the given element type (Check the elements/CalibratedTransmon) + # FIXME: How do we fail early instead of timing out on sequencer not returning results + # - Maybe log the compiled schedule say to a file and check it out later q1: element_type: diff --git a/qpi-driver/py/tests/test_calibration_loop.py b/qpi-driver/py/tests/test_calibration_loop.py index 3e7dea85..386e64ca 100644 --- a/qpi-driver/py/tests/test_calibration_loop.py +++ b/qpi-driver/py/tests/test_calibration_loop.py @@ -1328,15 +1328,15 @@ def test_a_coupler_that_declares_its_own_gap_is_believed( "t1": {"delays": [round(6e-6 * i, 9) for i in range(21)]}, "t2_echo": {"delays": [round(2e-6 * i, 9) for i in range(41)]}, "fine_amplitude": {"repetitions": [1, 3, 5, 7, 9]}, - "rb": {"depths": [1, 4, 16, 32], "circuits_per_depth": 6}, - "interleaved_rb": {"depths": [1, 4, 10, 20], "circuits_per_depth": 4}, + "rb": {"depths": [1, 4, 16, 32], "circuits_per_depth": 2}, + "interleaved_rb": {"depths": [1, 4, 10, 20], "circuits_per_depth": 2}, # Narrow, because the avoided crossing is a few MHz wide and the default grid # steps ~75 MHz per point — see `MIN_CHEVRON_CONTRAST`. "cz_chevron": { - "amplitudes": [round(0.365 + i * 0.00115, 5) for i in range(21)], - "durations": [round(20e-9 + i * 5e-9, 11) for i in range(45)], + "amplitudes": [round(0.365 + i * 0.0023, 5) for i in range(11)], + "durations": [round(20e-9 + i * 10e-9, 11) for i in range(23)], }, - "conditional_phase": {"phases": [round(i * 15.0, 1) for i in range(25)]}, + "conditional_phase": {"phases": [round(i * 30.0, 1) for i in range(13)]}, } diff --git a/qpi-driver/py/tests/test_device_reload.py b/qpi-driver/py/tests/test_device_reload.py index 5d51c891..b5ce14bb 100644 --- a/qpi-driver/py/tests/test_device_reload.py +++ b/qpi-driver/py/tests/test_device_reload.py @@ -9,7 +9,6 @@ import pytest import yaml - from qpi_driver.executors.base import CircuitPayload, JobPayload from .utils.io import load_json_fixture, load_yaml_fixture diff --git a/qpi-driver/py/tests/test_tuner_routines.py b/qpi-driver/py/tests/test_tuner_routines.py index 33868047..f97388ea 100644 --- a/qpi-driver/py/tests/test_tuner_routines.py +++ b/qpi-driver/py/tests/test_tuner_routines.py @@ -1,7 +1,7 @@ """Every routine compiles against a real scheduler (RFC 0004 §7, tier 2). These need a scheduler, so they skip on a base install and run under -``make test-py-quantify`` and ``make test-py-qblox``. What they prove is that +``make test-py-driver EXECUTOR=quantify`` and ``make test-py-driver EXECUTOR=qblox``. What they prove is that each routine builds a schedule the backend's compiler accepts — the dummy cluster returns no real data, so analysis is tier 1's job, not this file's. @@ -241,7 +241,6 @@ def test_a_device_config_changed_under_a_tuner_is_applied_to_the_live_device( that back over whatever changed. """ import yaml - from qpi_driver.tuners.base.device import read_path path = quantify_tuner._device_config_path diff --git a/qpi-ui/internal/dashboard/src/components/tabs/QpuRegistryTab/elements/QpuCard.tsx b/qpi-ui/internal/dashboard/src/components/tabs/QpuRegistryTab/elements/QpuCard.tsx index 3f7e7d1c..f8d19c18 100644 --- a/qpi-ui/internal/dashboard/src/components/tabs/QpuRegistryTab/elements/QpuCard.tsx +++ b/qpi-ui/internal/dashboard/src/components/tabs/QpuRegistryTab/elements/QpuCard.tsx @@ -157,7 +157,9 @@ export function QpuCard({ }`} > - {qpu.status === "maintenance" ? "Under maintenance" : "Maintenance"} + {qpu.status === "maintenance" + ? "Under maintenance" + : "Maintenance"}