Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ quantify.device.yaml
**.nc
**.pkl

# Python bytecode. The SDKs ignore this in their own .gitignore; this catches the
# rest of the tree, such as scripts/.
__pycache__/
*.py[cod]

# Unit test / coverage reports
htmlcov/
.tox/
Expand Down
12 changes: 3 additions & 9 deletions e2e/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
# level (drivers/toggle, drivers/connect) rather than on the QPU itself.
DRIVER_FRAMEWORK = True

# Channels every bluefors_gen1 monitor case polls, in the CLI/-o string form.
_BLUEFORS_CHANNELS = "mapper.bf.tmc:K,mapper.bf.pmc:mbar"

s = requests.Session()


Expand Down Expand Up @@ -133,11 +136,6 @@ def print_summary(jobs):
)


# ---------------------------------------------------------------------------
# New E2E tests for recently added features
# ---------------------------------------------------------------------------


def test_qpu_seconds_deduction():
"""Verify that QPU seconds were deducted from the test user after jobs ran."""
print("\n[verify] Testing QPU seconds deduction …")
Expand Down Expand Up @@ -1564,10 +1562,6 @@ def forward(src, dst):
return True


# Channels every bluefors_gen1 monitor case polls, in the CLI/-o string form.
_BLUEFORS_CHANNELS = "mapper.bf.tmc:K,mapper.bf.pmc:mbar"


def _monitor_flags(token, fingerprint, mock_port):
"""The qpi-driver CLI flags for the bluefors_gen1 monitor. Identical across
languages — the Go (cobra) and TypeScript (commander) CLIs share the Python
Expand Down
20 changes: 0 additions & 20 deletions qpi-client/py/qpi_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ def __init__(self, base_url: str, api_token: str | None = None) -> None:
if api_token:
self._session.headers["X-API-Token"] = api_token

# -- public API ----------------------------------------------------------

def submit_job(
self,
circuits: list[dict[str, Any]],
Expand Down Expand Up @@ -122,8 +120,6 @@ def cancel_job(self, job_id: str) -> dict[str, Any]:
resp.raise_for_status()
return resp.json()

# -- high-level helpers --------------------------------------------------

def get_backend(self, name: str = "qpi") -> "QPIBackend":
"""Return a :class:`QPIBackend` handle for the named QPU.

Expand All @@ -150,8 +146,6 @@ def job(self, job_id: str) -> "QPIJob":

return QPIJob(backend=None, job_id=job_id, client=self)

# -- QPU discovery -------------------------------------------------------

def list_qpus(self) -> list[dict[str, Any]]:
"""List all online QPUs.

Expand All @@ -175,8 +169,6 @@ def get_qpu(self, name: str) -> dict[str, Any]:
resp.raise_for_status()
return resp.json()

# -- QPU Registry & Toggles (admin-only) ---------------------------------

def create_qpu(
self,
name: str,
Expand Down Expand Up @@ -259,8 +251,6 @@ def toggle_qpu(self, qpu_id: str, enabled: bool) -> dict[str, Any]:
resp.raise_for_status()
return resp.json()

# -- Notifications -------------------------------------------------------

def list_notifications(self) -> list[dict[str, Any]]:
"""List notifications visible to the authenticated user.

Expand Down Expand Up @@ -291,8 +281,6 @@ def dismiss_notification(self, notification_id: str) -> dict[str, Any]:
resp.raise_for_status()
return resp.json()

# -- Booking Slots (time_slots) ------------------------------------------

def list_time_slots(self) -> list[dict[str, Any]]:
"""List all booking slots.

Expand Down Expand Up @@ -368,8 +356,6 @@ def delete_time_slot(self, slot_id: str) -> None:
)
resp.raise_for_status()

# -- QPU Time Requests ---------------------------------------------------

def list_time_requests(self) -> list[dict[str, Any]]:
"""List QPU time requests.

Expand Down Expand Up @@ -430,8 +416,6 @@ def update_time_request(
resp.raise_for_status()
return resp.json()

# -- Admin User Management -----------------------------------------------

def list_users(self) -> list[dict[str, Any]]:
"""List all registered users (admin-only).

Expand Down Expand Up @@ -460,8 +444,6 @@ def allocate_qpu_time(self, user_id: str, seconds: int) -> dict[str, Any]:
resp.raise_for_status()
return resp.json()

# -- Auth helpers --------------------------------------------------------

def auth_with_password(self, identity: str, password: str) -> dict[str, Any]:
"""Authenticate as a regular user using email/password.

Expand All @@ -483,8 +465,6 @@ def auth_with_password(self, identity: str, password: str) -> dict[str, Any]:
self._session.headers["Authorization"] = f"Bearer {token}"
return data

# -- lifecycle -----------------------------------------------------------

def close(self) -> None:
"""Close the underlying HTTP session."""
self._session.close()
Expand Down
17 changes: 0 additions & 17 deletions qpi-client/py/qpi_client/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@

from qpi_client.client import QPIClient

# ---------------------------------------------------------------------------
# QPIJob
# ---------------------------------------------------------------------------


class QPIJob(JobV1):
"""A Qiskit-compatible job handle backed by the QPI REST API.
Expand All @@ -62,8 +58,6 @@ def id(self) -> str:
"""Server-assigned job ID."""
return self.job_id()

# -- JobV1 interface -----------------------------------------------------

def submit(self) -> None:
"""No-op — the job was already submitted by the backend."""

Expand Down Expand Up @@ -131,8 +125,6 @@ def cancel(self) -> None:
"""Request cancellation of this job on the server."""
self._client.cancel_job(self.job_id())

# -- internal helpers ----------------------------------------------------

def _build_result(self, data: dict[str, Any]) -> Result:
"""Construct a :class:`qiskit.result.Result` from the API response.

Expand Down Expand Up @@ -201,11 +193,6 @@ def _build_result(self, data: dict[str, Any]) -> Result:
)


# ---------------------------------------------------------------------------
# QPIBackend
# ---------------------------------------------------------------------------


class QPIBackend(BackendV2):
"""A Qiskit ``BackendV2`` that submits circuits to the QPI server.

Expand Down Expand Up @@ -246,8 +233,6 @@ def _resolve_num_qubits(self, name: str) -> int:
f"QPU '{name}' has no valid num_qubits (got {qpu.get('num_qubits')!r})"
) from exp

# -- BackendV2 required properties ---------------------------------------

@property
def target(self) -> Target:
"""Return the transpiler :class:`Target` for this backend."""
Expand All @@ -262,8 +247,6 @@ def max_circuits(self) -> int | None:
def _default_options(cls) -> Options:
return Options(shots=1024, meas_level=2, meas_return="single")

# -- execution -----------------------------------------------------------

def run(
self,
circuit: QuantumCircuit | Sequence[QuantumCircuit] | None = None,
Expand Down
23 changes: 11 additions & 12 deletions qpi-driver/py/qpi_driver/executors/utils/coupler_bias.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@
SPI = "spi"
QCM = "qcm"

#: The keys a bias submodule may carry, in either scheduler's spelling.
BIAS_KEYS = (
"parking_current",
"source",
"spi_module",
"spi_output",
"qcm_module",
"qcm_output",
"line_resistance_ohm",
)


class BiasSource(Protocol):
"""Something that can hold a coupler at a DC current."""
Expand Down Expand Up @@ -181,18 +192,6 @@ def edge_names(device: Any) -> list[str]:
return list(edges)


#: The keys a bias submodule may carry, in either scheduler's spelling.
BIAS_KEYS = (
"parking_current",
"source",
"spi_module",
"spi_output",
"qcm_module",
"qcm_output",
"line_resistance_ohm",
)


def bias_settings(edge: Any) -> dict[str, Any]:
"""Everything an edge's ``bias`` submodule says, as plain values.

Expand Down
56 changes: 56 additions & 0 deletions qpi-driver/py/qpi_driver/simulation/_compiled_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Reading either scheduler's compiled schedule.

quantify-scheduler and qblox-scheduler describe a compiled schedule almost
identically and not quite. The differences are small, undocumented, and would
otherwise be scattered through the coordinator's walk, so they are named here
instead:

- `pulse_info` and `acquisition_info` are a *list* of dicts under quantify and
a single dict under qblox;
- a subschedule is a `Schedule` with `.operations` under quantify, and a
`TimeableSchedule` carrying `operation_dict` inside `.data` under qblox —
and qblox nests them several deep where quantify has one level;
- a drive pulse's amplitude is `G_amp` under quantify and `amplitude` under
qblox, with `amp` used by both for square pulses.

quantify-scheduler is being deprecated, so the qblox spelling is the one that
has to keep working; supporting both is what makes that transition a
non-event rather than a rewrite of the simulator.
"""

from typing import Any


def read_operations(schedule: Any) -> Any:
"""The operation table of a compiled schedule, by either spelling."""
operations = getattr(schedule, "operations", None)
if operations is not None:
return operations
return schedule.data["operation_dict"]


def is_subschedule(operation: Any) -> bool:
"""Whether an operation is itself a schedule to descend into."""
if getattr(operation, "schedulables", None) is not None:
return True
data = getattr(operation, "data", None)
return isinstance(data, dict) and "schedulables" in data


def read_info_entries(operation: Any, key: str) -> list[dict]:
"""``pulse_info`` or ``acquisition_info`` as a list, whichever shape it is."""
data = getattr(operation, "data", None)
if not isinstance(data, dict):
return []
info = data.get(key)
if not info:
return []
return list(info) if isinstance(info, (list, tuple)) else [info]


def read_amplitude(pulse: dict) -> Any:
"""A drive pulse's amplitude, by whichever key the scheduler used."""
for key in ("G_amp", "amplitude", "amp"):
if pulse.get(key) is not None:
return pulse[key]
return None
4 changes: 0 additions & 4 deletions qpi-driver/py/qpi_driver/simulation/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ def __init__(self, agent: Any, coordinator: Any) -> None:
self._agent = agent
self._coordinator = coordinator

# --- the parts that are genuinely the agent's ----------------------------

def compile(self, schedule: Any) -> Any:
"""The real compiler. An invalid schedule fails here exactly as on a node."""
return self._agent.compile(schedule)
Expand All @@ -66,8 +64,6 @@ def latest_compiled_schedule(self) -> Any:
def set_output_data_dir(self, *args: Any, **kwargs: Any) -> Any:
return self._agent.set_output_data_dir(*args, **kwargs)

# --- the part that is the chip's -----------------------------------------

def run(self, schedule: Any, timeout: int = 10, **kwargs: Any) -> xr.Dataset:
"""Compile, then play through the simulator rather than a cluster.

Expand Down
Loading
Loading