diff --git a/samples/notebooks/qdk_stim.ipynb b/samples/notebooks/qdk_stim.ipynb new file mode 100644 index 00000000000..5a69f73777b --- /dev/null +++ b/samples/notebooks/qdk_stim.ipynb @@ -0,0 +1,694 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cell-00", + "metadata": {}, + "source": [ + "# Stim in the QDK\n", + "\n", + "`qdk.stim` compiles [Stim](https://github.com/quantumlib/Stim) circuits to QIR and simulates\n", + "them, with extensions for **qubit loss**, **post-selection**, and **non-Clifford gates**.\n", + "\n", + "| Function | Returns |\n", + "| --- | --- |\n", + "| `stim.compile(src, noise=None)` | `(qir, noise)` |\n", + "| `stim.run(src, shots=1, noise=None, seed=None, type=None)` | one result list per shot |\n", + "\n", + "Every measurement records `Zero`, `One`, or `Loss`, displayed below as `0`, `1`, and `L`.\n", + "\n", + "`type` picks the simulator:\n", + "\n", + "- `\"clifford\"` — stabilizer simulator. Scales to many qubits and absorbs a modest number of\n", + " non-Clifford operations by branching the stabilizer decomposition.\n", + "- `\"cpu\"` / `\"gpu\"` — full state vector, for circuits dominated by non-Clifford gates.\n", + "- `None` (default) — `\"gpu\"` when a GPU is available, otherwise `\"cpu\"`.\n", + "\n", + "> `qdk.stim` is experimental and its API may change." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-01", + "metadata": {}, + "outputs": [], + "source": [ + "from qdk import stim\n", + "from qdk.widgets import Histogram\n", + "\n", + "SHOTS = 2000" + ] + }, + { + "cell_type": "markdown", + "id": "cell-02", + "metadata": {}, + "source": [ + "## Compiling to QIR\n", + "\n", + "Everything in the [Stim gate reference](https://github.com/quantumlib/Stim/blob/main/doc/gates.md)\n", + "is supported, except for the handful of instructions listed at the end of this notebook, and\n", + "the QDK adds the extensions covered below.\n", + "\n", + "`stim.compile` lowers a circuit to QIR and returns it alongside the noise configuration that\n", + "the simulators consume." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-03", + "metadata": {}, + "outputs": [], + "source": [ + "bell = \"\"\"\n", + "H 0\n", + "CX 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "qir, _ = stim.compile(bell)\n", + "print(qir)" + ] + }, + { + "cell_type": "markdown", + "id": "cell-04", + "metadata": {}, + "source": [ + "`stim.run` compiles and simulates in one step. The Bell pair is entangled, so only `00` and\n", + "`11` occur." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-05", + "metadata": {}, + "outputs": [], + "source": [ + "Histogram(stim.run(bell, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "39fe1b22", + "metadata": {}, + "source": [ + "## Noise channels\n", + "\n", + "| Instruction | Effect |\n", + "| --- | --- |\n", + "| `X_ERROR(p)`, `Y_ERROR(p)`, `Z_ERROR(p)` | Independent Pauli error on each target |\n", + "| `DEPOLARIZE1(p)`, `DEPOLARIZE2(p)` | Uniform Pauli error per qubit or per pair |\n", + "| `PAULI_CHANNEL_1(...)`, `PAULI_CHANNEL_2(...)` | Explicit per-Pauli probabilities |\n", + "| `CORRELATED_ERROR(p)`, `E(p)` | One Pauli product applied as a single event |\n", + "| `ELSE_CORRELATED_ERROR(p)` | Another branch of the preceding correlated error |\n", + "| `LOSS_ERROR(p)` | Loses each target with probability $p$ |\n", + "\n", + "Every Pauli target on a `CORRELATED_ERROR` line belongs to one event, so `X0 X1` fires on both\n", + "qubits or on neither. `ELSE_CORRELATED_ERROR` adds a branch that is reached only when no\n", + "earlier link in the chain fired, making the branches mutually exclusive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a51882e", + "metadata": {}, + "outputs": [], + "source": [ + "correlated = \"\"\"\n", + "CORRELATED_ERROR(0.2) X0 X1\n", + "ELSE_CORRELATED_ERROR(0.2) X0\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(correlated, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "47503ec3", + "metadata": {}, + "source": [ + "### Readout noise\n", + "\n", + "Any instruction that appends to the measurement record takes an optional probability that\n", + "flips the recorded bit, leaving the qubit itself untouched: `M` / `MZ`, `MX`, `MY`, the\n", + "`MR` variants, the pair measurements `MXX` / `MYY` / `MZZ`, `MPP`, and `PEEK_LOSS`.\n", + "\n", + "Below both qubits stay in $|0\\rangle$, so every `1` in the histogram is a misread." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "756c204d", + "metadata": {}, + "outputs": [], + "source": [ + "readout_noise = \"\"\"\n", + "M(0.1) 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(readout_noise, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-12", + "metadata": {}, + "source": [ + "### Loss\n", + "\n", + "Qubit loss is a QDK extension. `LOSS_ERROR(p)` loses each target with probability $p$, and\n", + "measuring a lost qubit records `Loss` rather than a bit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-13", + "metadata": {}, + "outputs": [], + "source": [ + "loss = \"\"\"\n", + "LOSS_ERROR(0.15) 0 1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(loss, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "24ea4018", + "metadata": {}, + "source": [ + "Loss also has a target form, `L0`,\n", + "which may be combined with Pauli terms inside a correlated error to build branches that mix\n", + "the two." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-15", + "metadata": {}, + "outputs": [], + "source": [ + "mixed_loss = \"\"\"\n", + "CORRELATED_ERROR(0.1) L0\n", + "ELSE_CORRELATED_ERROR(0.1) L1\n", + "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", + "MR 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(mixed_loss, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-16", + "metadata": {}, + "source": [ + "### Inspecting loss with `PEEK_LOSS`\n", + "\n", + "`PEEK_LOSS` reports whether each target is currently lost, appending `1` for a lost qubit and\n", + "`0` otherwise. It neither measures the qubit nor clears the loss, so a later measurement of a\n", + "lost qubit still records `Loss`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-17", + "metadata": {}, + "outputs": [], + "source": [ + "peek = \"\"\"\n", + "LOSS_ERROR(0.3) 0\n", + "PEEK_LOSS 0\n", + "M 0\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(peek, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-18", + "metadata": {}, + "source": [ + "## Post-selection with `SELECT`\n", + "\n", + "A `SELECT { ... }` block re-runs its own body until every condition inside it passes.\n", + "\n", + "- `REQUIRE rec[...]` restarts the block unless the referenced records have even parity, so\n", + " `REQUIRE rec[-1]` keeps only shots whose last measurement was `0`.\n", + "- A lost qubit has no bit to contribute to that parity, so `REQUIRE` also restarts whenever\n", + " one of its records was lost\n", + "- Prefixing a record with `!` inverts it, so `REQUIRE !rec[-1]` keeps the shots that\n", + " measured `1`.\n", + "- Conditions are checked where they appear, letting one block select several measurements in\n", + " sequence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-19", + "metadata": {}, + "outputs": [], + "source": [ + "select = \"\"\"\n", + "SELECT {\n", + " H 0\n", + " M 0\n", + " REQUIRE rec[-1]\n", + " H 1\n", + " M 1\n", + " REQUIRE !rec[-1]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(select, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-20", + "metadata": {}, + "source": [ + "Listing several records in one `REQUIRE` selects on their joint parity instead of on each\n", + "record individually, which keeps the two qubits below correlated rather than fixed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-21", + "metadata": {}, + "outputs": [], + "source": [ + "parity = \"\"\"\n", + "SELECT {\n", + " H 0\n", + " H 1\n", + " M 0\n", + " M 1\n", + " REQUIRE rec[-1] rec[-2]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(parity, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-22", + "metadata": {}, + "source": [ + "### Discarding lost qubits with `NOTLEAKED`\n", + "\n", + "`NOTLEAKED rec[...]` is the loss half of `REQUIRE` on its own: it restarts the block when a\n", + "referenced measurement was lost, but places no constraint on the recorded bit. Use it when a\n", + "shot should survive with either outcome, just not with `L`.\n", + "\n", + "It cannot be negated, and it cannot reference a `PEEK_LOSS` record — a peek succeeds even\n", + "when the qubit is lost, so the request would be ambiguous." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-23", + "metadata": {}, + "outputs": [], + "source": [ + "not_leaked = \"\"\"\n", + "SELECT {\n", + " H 0\n", + " LOSS_ERROR(0.3) 0\n", + " MR 0\n", + " NOTLEAKED rec[-1]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(not_leaked, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-24", + "metadata": {}, + "source": [ + "### Nesting and record scope\n", + "\n", + "Blocks nest, and a restart re-runs only the body of the block that failed. A record is *in\n", + "scope* if it was produced inside the current block or a nested one; records from an enclosing\n", + "block are out of scope because a restart can no longer change them.\n", + "\n", + "Below, the inner block fixes qubit 0 and the outer block fixes qubit 1, so only `00` survives." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-25", + "metadata": {}, + "outputs": [], + "source": [ + "nested = \"\"\"\n", + "SELECT {\n", + " SELECT {\n", + " H 0\n", + " M 0\n", + " REQUIRE rec[-1]\n", + " }\n", + " H 1\n", + " M 1\n", + " REQUIRE rec[-1]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(nested, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-26", + "metadata": {}, + "source": [ + "Every condition must reference at least one in-scope record, otherwise a restart could never\n", + "satisfy it and the block would loop forever; that case is rejected at compile time. Mixing an\n", + "out-of-scope record with an in-scope one is allowed, and the outer record then acts as a fixed\n", + "value to select against." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-27", + "metadata": {}, + "outputs": [], + "source": [ + "scoping = \"\"\"\n", + "H 0\n", + "M 0\n", + "SELECT {\n", + " H 1\n", + " M 1\n", + " REQUIRE rec[-1] rec[-2]\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(scoping, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-28", + "metadata": {}, + "source": [ + "## `REPEAT` blocks\n", + "\n", + "`REPEAT N { ... }` unrolls its body `N` times at compile time, and each iteration appends its\n", + "own measurement records. `N` must be greater than zero." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-29", + "metadata": {}, + "outputs": [], + "source": [ + "repeat = \"\"\"\n", + "REPEAT 3 {\n", + " X 0\n", + " M 0\n", + "}\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(repeat, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-30", + "metadata": {}, + "source": [ + "## Pauli products\n", + "\n", + "`MPP` measures a Pauli product written with `*`, such as `X0*Y1*Z2`, and `SPP` / `SPP_DAG`\n", + "apply the generalized `S` gate $\\exp(\\mp i \\frac{\\pi}{4} P)$ to one. Multiple products may\n", + "share a line, separated by whitespace. The non-Clifford `TPP`, `TPP_DAG`, and `R_PAULI` take\n", + "the same targets and are covered further below.\n", + "\n", + "- A `!` on any factor negates the whole product, so `MPP !Z0*Z1` and `MPP Z0*!Z1` agree, and\n", + " `SPP !Z0` matches `SPP_DAG Z0`.\n", + "- Repeated factors on the same qubit are folded by Pauli multiplication, so `X0*Y1*Y1`\n", + " reduces to `X0`.\n", + "- Folding can leave a factor of $\\pm i$, which makes the product anti-Hermitian. Those\n", + " products, such as `X0*Z0`, are rejected.\n", + "\n", + "On a Bell pair both $Z_0Z_1$ and $X_0X_1$ measure `0` with certainty, and negating the first\n", + "product flips its outcome." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-31", + "metadata": {}, + "outputs": [], + "source": [ + "pauli_measurement = \"\"\"\n", + "H 0\n", + "CX 0 1\n", + "MPP Z0*Z1 X0*X1 !Z0*Z1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(pauli_measurement, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-32", + "metadata": {}, + "source": [ + "`SPP Z` is exactly `S`. Applying it twice gives `Z`, which the surrounding Hadamards turn into\n", + "a bit flip, while `SPP Z` followed by `SPP !Z` cancels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-33", + "metadata": {}, + "outputs": [], + "source": [ + "generalized_s = \"\"\"\n", + "H 0 1\n", + "SPP Z0\n", + "SPP Z0\n", + "SPP Z1\n", + "SPP !Z1\n", + "H 0 1\n", + "M 0 1\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(generalized_s, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-34", + "metadata": {}, + "source": [ + "## Non-Clifford extensions\n", + "\n", + "The QDK extends Stim with the non-Clifford gates of\n", + "[Clifft](https://github.com/unitaryfoundation/clifft/blob/main/docs/reference/gates.md).\n", + "\n", + "| Category | Instructions | Grouping |\n", + "| --- | --- | --- |\n", + "| Phase gates | `T`, `T_DAG` | one qubit each |\n", + "| | `TPP`, `TPP_DAG` | one Pauli product each |\n", + "| Controlled gates | `CH` | consecutive pairs |\n", + "| | `CCX`, `CCZ` | consecutive triples |\n", + "| Single-qubit rotations | `R_X(a)`, `R_Y(a)`, `R_Z(a)` | one qubit each |\n", + "| | `U3(t, p, l)`, `U(t, p, l)` | one qubit each |\n", + "| Pair rotations | `R_XX(a)`, `R_YY(a)`, `R_ZZ(a)` | consecutive pairs |\n", + "| Pauli rotation | `R_PAULI(a)` | one Pauli product each |\n", + "\n", + "**Angles.** A bare argument counts half turns, so `R_X(0.5)` rotates by $\\pi/2$. Append `rad`\n", + "to give radians directly, as in `R_X(0.5rad)`. `U3(theta, phi, lambda)` applies\n", + "$R_Z(\\varphi) R_Y(\\theta) R_Z(\\lambda)$ and may mix the two units. `U` is an alias for `U3`.\n", + "\n", + "**Simulation.** `type=\"clifford\"` branches the stabilizer decomposition on every non-Clifford\n", + "operation, so it stays efficient while they remain sparse. Reach for `type=\"cpu\"` or\n", + "`type=\"gpu\"` when they do not." + ] + }, + { + "cell_type": "markdown", + "id": "cell-35", + "metadata": {}, + "source": [ + "`TPP Z` is exactly `T`, so qubits 0 and 1 both accumulate $T^4 = Z$ and end up flipped.\n", + "`TPP X` instead rotates about $X$, which leaves $|+\\rangle$ alone, and the `!` on qubit 3\n", + "inverts the second gate so that the pair cancels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-36", + "metadata": {}, + "outputs": [], + "source": [ + "phase_gates = \"\"\"\n", + "H 0 1 2 3\n", + "REPEAT 4 {\n", + " T 0\n", + " TPP Z1\n", + " TPP X2\n", + "}\n", + "T 3\n", + "TPP !Z3\n", + "H 0 1 2 3\n", + "M 0 1 2 3\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(phase_gates, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-37", + "metadata": {}, + "source": [ + "`CCX` flips its target and `CCZ` applies a phase flip once both controls are `1`; the\n", + "Hadamards around `CCZ` expose that phase flip in the computational basis. `CH` applies a\n", + "Hadamard when its control is `1`, leaving qubit 4 in an even superposition." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-38", + "metadata": {}, + "outputs": [], + "source": [ + "controlled_gates = \"\"\"\n", + "X 0 1\n", + "CCX 0 1 2\n", + "H 3\n", + "CCZ 0 1 3\n", + "H 3\n", + "CH 0 4\n", + "M 0 1 2 3 4\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(controlled_gates, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-39", + "metadata": {}, + "source": [ + "`R_X(1)` is a half turn about $X$ and flips qubit 0. `R_Y(1rad)` rotates by a single radian,\n", + "so qubit 1 measures `1` with probability $\\sin^2(1/2) \\approx 0.23$. `U(1, 0, 0)` reduces to\n", + "$R_Y(\\pi)$ and flips qubit 2." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-40", + "metadata": {}, + "outputs": [], + "source": [ + "rotations = \"\"\"\n", + "R_X(1) 0\n", + "R_Y(1rad) 1\n", + "U(1, 0, 0) 2\n", + "M 0 1 2\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(rotations, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-41", + "metadata": {}, + "source": [ + "`R_XX(1)` and `R_PAULI(1) X*X` are the same half turn about $X \\otimes X$ and flip both of\n", + "their qubits, while `R_ZZ(0.5)` only adds a relative phase that the computational basis cannot\n", + "see." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cell-42", + "metadata": {}, + "outputs": [], + "source": [ + "pauli_rotations = \"\"\"\n", + "R_ZZ(0.5) 0 1\n", + "R_XX(1) 2 3\n", + "R_PAULI(1) X4*X5\n", + "M 0 1 2 3 4 5\n", + "\"\"\"\n", + "\n", + "Histogram(stim.run(pauli_rotations, shots=SHOTS, type=\"clifford\"), labels=\"kets\")" + ] + }, + { + "cell_type": "markdown", + "id": "cell-43", + "metadata": {}, + "source": [ + "## Not yet supported\n", + "\n", + "Tracked in [microsoft/qdk#3518](https://github.com/microsoft/qdk/issues/3518).\n", + "\n", + "| Feature | Current behavior |\n", + "| --- | --- |\n", + "| `MPAD` | Parsed and ignored instead of appending bits to the measurement record |\n", + "| `HERALDED_ERASE`, `HERALDED_PAULI_CHANNEL_1` | Compile error: unsupported instruction |\n", + "| Pauli products that fold to the identity, such as `MPP Z0*Z0` | Compile error: unsupported target |\n", + "| Sweep-bit targets, such as `CX sweep[5] 7` | Compile error: unsupported target |\n", + "| `DETECTOR`, `OBSERVABLE_INCLUDE` | Parsed and ignored; there is no detector or observable sampling |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.9.6)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/samples/notebooks/stim_select.ipynb b/samples/notebooks/stim_select.ipynb deleted file mode 100644 index 1c7cf7019a5..00000000000 --- a/samples/notebooks/stim_select.ipynb +++ /dev/null @@ -1,261 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "24104626", - "metadata": {}, - "source": [ - "# Stim `SELECT` blocks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8148c079", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk import stim\n", - "from qdk.widgets import Histogram" - ] - }, - { - "cell_type": "markdown", - "id": "826867b4", - "metadata": {}, - "source": [ - "## Selecting a measurement outcome" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b796200d", - "metadata": {}, - "outputs": [], - "source": [ - "select_zero = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " M 0\n", - " REQUIRE rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# REQUIRE rec[-1] restarts while M 0 == 1, so every shot reports 0.\n", - "Histogram(stim.run(select_zero, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "695f6677", - "metadata": {}, - "source": [ - "## Negating a requirement with `!`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "853c5cad", - "metadata": {}, - "outputs": [], - "source": [ - "select_one = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " M 0\n", - " REQUIRE !rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Negating with `!` flips the condition: restart while M 0 == 0, so every shot reports 1.\n", - "Histogram(stim.run(select_one, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "522169df", - "metadata": {}, - "source": [ - "## Multiple `REQUIRE` statements" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9a106982", - "metadata": {}, - "outputs": [], - "source": [ - "multi_require = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " M 0\n", - " REQUIRE rec[-1]\n", - " H 1\n", - " M 1\n", - " REQUIRE rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Both qubits preselected to 0 → only 00 appears.\n", - "Histogram(stim.run(multi_require, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "0dccc4b8", - "metadata": {}, - "source": [ - "## Parity over several records" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "746161ea", - "metadata": {}, - "outputs": [], - "source": [ - "parity_check = \"\"\"\n", - "SELECT {\n", - " R 0\n", - " R 1\n", - " H 0\n", - " H 1\n", - " M 0\n", - " M 1\n", - " REQUIRE rec[-1] rec[-2]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Even parity enforced → only 00 and 11 survive.\n", - "Histogram(stim.run(parity_check, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "575121f5", - "metadata": {}, - "source": [ - "## Rejecting lost qubits with `NOTLEAKED`\n", - "\n", - "`NOTLEAKED` is the loss counterpart of `REQUIRE`: it restarts the enclosing `SELECT` block whenever a referenced measurement's qubit was *lost* instead of measured. Without it, `LOSS_ERROR` would make some shots report `L`; `NOTLEAKED` discards those so every reported shot has a genuine `0`/`1` outcome.\n", - "\n", - "Do not use `NOTLEAKED` to check a measurement record generated by a `PEEK_LOSS`. This results in an error because the request is ambiguous." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c22c1002", - "metadata": {}, - "outputs": [], - "source": [ - "survived = \"\"\"\n", - "SELECT {\n", - " H 0\n", - " LOSS_ERROR(0.3) 0\n", - " MR 0\n", - " NOTLEAKED rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# NOTLEAKED restarts the block whenever qubit 0 is lost, so no shot reports L.\n", - "Histogram(stim.run(survived, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "13032af8", - "metadata": {}, - "source": [ - "## Nested `SELECT` blocks" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "81a0f59b", - "metadata": {}, - "outputs": [], - "source": [ - "nested = \"\"\"\n", - "SELECT {\n", - " R 1\n", - " SELECT {\n", - " R 0\n", - " H 0\n", - " M 0\n", - " REQUIRE rec[-1]\n", - " }\n", - " H 1\n", - " M 1\n", - " REQUIRE rec[-1]\n", - "}\n", - "\"\"\"\n", - "\n", - "# Inner block selects qubit 0; outer block selects qubit 1 → only 00.\n", - "Histogram(stim.run(nested, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "d502d3b4", - "metadata": {}, - "source": [ - "## Measurement record scoping\n", - "\n", - "When a `SELECT` block restarts, it re-runs **only its own body** — measurements made in an enclosing scope are not repeated. A record is *in scope* if it was produced inside the current block (or an inner one); records from an outer block are *out of scope*.\n", - "\n", - "Since a restart can only change in-scope measurements, every `REQUIRE` / `NOTLEAKED` must reference **at least one** in-scope record. Referencing only out-of-scope records could never change the outcome, so it would loop forever and is rejected at compile time.\n", - "\n", - "You *can*, however, combine an out-of-scope record with an in-scope one: the out-of-scope record acts as a fixed condition that the in-scope measurement is selected against.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "45f03f78", - "metadata": {}, - "outputs": [], - "source": [ - "scoping = \"\"\"\n", - "H 0\n", - "M 0\n", - "SELECT {\n", - " H 1\n", - " M 1\n", - " REQUIRE rec[-1] rec[-2]\n", - "}\n", - "\"\"\"\n", - "\n", - "# rec[-1] (M 1) is in scope; rec[-2] (M 0) is fixed from outside the block.\n", - "# Only M 1 is re-rolled on restart, until it matches M 0 → just 00 and 11 survive.\n", - "Histogram(stim.run(scoping, shots=2000, type=\"clifford\"), labels=\"kets\")\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv (3.12.12)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/samples/notebooks/stim_to_qir.ipynb b/samples/notebooks/stim_to_qir.ipynb deleted file mode 100644 index 96d6a701ec5..00000000000 --- a/samples/notebooks/stim_to_qir.ipynb +++ /dev/null @@ -1,245 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "772299d0", - "metadata": {}, - "source": [ - "# Stim → QIR\n", - "\n", - "Compile [Stim](https://github.com/quantumlib/Stim) circuits to QIR and simulate them with `qdk.stim`.\n", - "\n", - "- `stim.compile(src, None)` → `(qir, noise)`\n", - "- `stim.run(src, shots=..., type=\"clifford\")` → per-shot results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6c9df231", - "metadata": {}, - "outputs": [], - "source": [ - "from qdk import stim\n", - "from qdk.widgets import Histogram" - ] - }, - { - "cell_type": "markdown", - "id": "39a2e4f8", - "metadata": {}, - "source": [ - "## Basics\n", - "\n", - "Compile a Bell pair to QIR." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a41fe64d", - "metadata": {}, - "outputs": [], - "source": [ - "bell = \"\"\"H 0\n", - "CX 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "\n", - "qir, _ = stim.compile(bell, None)\n", - "print(qir)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d859fe1a", - "metadata": {}, - "outputs": [], - "source": [ - "# Entangled: only 00 and 11 appear.\n", - "Histogram(stim.run(bell, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "ca62aa9f", - "metadata": {}, - "source": [ - "## Noise channels\n", - "The following sections detail specifying noise\n", - "\n", - "### Correlated error\n", - "\n", - "The `X0 X1` fire together → only `00` and `11`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0c2b7a13", - "metadata": {}, - "outputs": [], - "source": [ - "correlated = \"\"\"CORRELATED_ERROR(0.2) X0 X1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(correlated, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "08be56a0", - "metadata": {}, - "source": [ - "### Pauli error\n", - "\n", - "Independent `X` on each qubit (p = 0.1)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "57943533", - "metadata": {}, - "outputs": [], - "source": [ - "xerr = \"\"\"X_ERROR(0.1) 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(xerr, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "8a236142", - "metadata": {}, - "source": [ - "### Loss\n", - "\n", - "Lost qubits show up as `L` (p = 0.15)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a10524be", - "metadata": {}, - "outputs": [], - "source": [ - "loss = \"\"\"LOSS_ERROR(0.15) 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(loss, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "c53f5277", - "metadata": {}, - "source": [ - "### Inspecting loss with `PEEK_LOSS`\n", - "\n", - "`PEEK_LOSS` checks whether each target qubit is lost without measuring the qubit or changing its loss state. It appends one result per target to the measurement record: `1` if the qubit is lost and `0` otherwise. The result can be referenced with `rec[...]`.\n", - "\n", - "Like measurement instructions, `PEEK_LOSS` accepts an optional readout-noise probability argument. For example, `PEEK_LOSS(0.1) 0` flips the appended result with probability 0.1." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "acec3644", - "metadata": {}, - "outputs": [], - "source": [ - "peek_loss = \"\"\"LOSS_ERROR(1) 0\n", - "PEEK_LOSS 0\n", - "\"\"\"\n", - "Histogram(stim.run(peek_loss, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "0d45640f", - "metadata": {}, - "source": [ - "### Loss in a correlated error\n", - "\n", - "Branches mix loss (`L`) and Pauli (`X`) terms." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5aadceb8", - "metadata": {}, - "outputs": [], - "source": [ - "mixed = \"\"\"CORRELATED_ERROR(0.1) L0\n", - "ELSE_CORRELATED_ERROR(0.1) L1\n", - "ELSE_CORRELATED_ERROR(0.1) L0 X1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(mixed, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "markdown", - "id": "1c40106e", - "metadata": {}, - "source": [ - "### Depolarizing\n", - "\n", - "`DEPOLARIZE1(p)`: one of 3 Paulis per qubit. `DEPOLARIZE2(p)`: one of 15 two-qubit Paulis." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10a92131", - "metadata": {}, - "outputs": [], - "source": [ - "dep1 = \"\"\"DEPOLARIZE1(0.3) 0 1\n", - "MR 0 1\n", - "\"\"\"\n", - "Histogram(stim.run(dep1, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c65f6416", - "metadata": {}, - "outputs": [], - "source": [ - "dep2 = \"\"\"DEPOLARIZE2(0.3) 0 1\n", - "MR 0 1\n", - "\n", - "\"\"\"\n", - "Histogram(stim.run(dep2, shots=2000, type=\"clifford\"), labels=\"kets\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv (3.14.3)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.14.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/source/compiler/stim_compiler/src/lex.rs b/source/compiler/stim_compiler/src/lex.rs index 02219b0750a..740b9834393 100644 --- a/source/compiler/stim_compiler/src/lex.rs +++ b/source/compiler/stim_compiler/src/lex.rs @@ -61,20 +61,20 @@ impl Display for Token { #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum TokenKind { - Newline, // \n - Uint, // unsigned integers - Double, // floating-point numbers - InstructionName, // H, X, CNOT, etc. - Pauli, // X1, Y2, Z3, etc. - Loss, // L1, L2, L3, etc. - Rec, // rec[- ...] - Sweep, // sweep[...] - Tag, // "[...]" - Open(Delim), // ( { - Close(Delim), // ) } - Star, // * - Bang, // ! - Comma, // , + Newline, // \n + Uint, // unsigned integers + Double(DoubleUnit), // floating-point numbers, can be radians or not + InstructionName, // H, X, CNOT, etc. + Pauli, // X1, Y2, Z3, etc. + Loss, // L1, L2, L3, etc. + Rec, // rec[- ...] + Sweep, // sweep[...] + Tag, // "[...]" + Open(Delim), // ( { + Close(Delim), // ) } + Star, // * + Bang, // ! + Comma, // , } impl Display for TokenKind { @@ -82,7 +82,7 @@ impl Display for TokenKind { match self { TokenKind::Newline => f.write_str("newline"), TokenKind::Uint => f.write_str("uint"), - TokenKind::Double => f.write_str("double"), + TokenKind::Double(_) => f.write_str("double"), TokenKind::InstructionName => f.write_str("instruction_name"), TokenKind::Pauli => f.write_str("pauli"), TokenKind::Loss => f.write_str("loss"), @@ -98,6 +98,12 @@ impl Display for TokenKind { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] +pub enum DoubleUnit { + Default, // for angles, interpret as half turns (pi radians) + Radians, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Sequence)] pub enum Delim { Paren, @@ -159,60 +165,105 @@ impl<'a> Lexer<'a> { true } - fn scan_number(&mut self, lo: u32, signed: bool) -> Result { - // Lexes a number: an optional sign, an integer part, an optional - // fractional part, and an optional exponent. + fn eat_str(&mut self, expected: &str) -> bool { + let pos = self.pos() as usize; + if !self.input[pos..].starts_with(expected) { + return false; + } + + for _ in expected.chars() { + let _ = self.chars.next(); + } + true + } + + fn require_digits(&mut self, error: Error) -> Result<(), Error> { + if self.eat_one_or_more_digits() { + Ok(()) + } else { + Err(error) + } + } + + /// Scans an optional "rad" suffix, which indicates that a number is in radians. + /// If the suffix is present, it must be followed by a non-alphanumeric character or the end of the input. + /// "1", "2.5", "-6" + fn scan_rad_suffix(&mut self) -> Result { + if !self.eat_str("rad") { + return Ok(false); + } + + let lo = self.pos(); + if self + .chars + .next_if(|(_, c)| c.is_alphanumeric() || *c == '_') + .is_some() + { + return Err(Error::UnrecognizedCharacter { + span: Span { lo, hi: self.pos() }, + }); + } + Ok(true) + } + + /// Scans an optional exponent: 'e'/'E', an optional sign, then one or more digits. + /// "1", "2.5", "6" + /// A bare "1e" or "1e-" (no exponent digits) is an error. + fn scan_exponent(&mut self, lo: u32) -> Result { + if self + .chars + .next_if(|(_, c)| matches!(c, 'e' | 'E')) + .is_none() + { + return Ok(false); + } + + self.chars.next_if(|(_, c)| matches!(c, '+' | '-')); + let span = Span { lo, hi: self.pos() }; + self.require_digits(Error::MissingExponentDigits { span })?; + Ok(true) + } - let mut is_double = false; + /// Scans an optional fractional part: a '.' followed by one or more digits. + /// "3<.14>", "0<.5>" + /// A '.' with no digits after it ("3.") is an error. + fn scan_fraction(&mut self, lo: u32) -> Result { + if self.chars.next_if(|(_, c)| *c == '.').is_none() { + return Ok(false); + } + + let span = Span { lo, hi: self.pos() }; + self.require_digits(Error::MissingFractionalDigits { span })?; + Ok(true) + } + + /// Scans the integer part of a number, which may be signed or unsigned. + fn scan_integer_part(&mut self, lo: u32, signed: bool) -> Result<(), Error> { if signed { // The leading sign was already consumed by the caller: // "<+>1", "<->42", "<+>3.5e-2" // This block consumes the integer digits: "+<1>", "-<42>" - if !self.eat_one_or_more_digits() { - return Err(Error::MissingDigitsAfterSign { - span: Span { lo, hi: self.pos() }, - }); - } - is_double = true; // A signed number is always a double. + let span = Span { lo, hi: self.pos() }; + self.require_digits(Error::MissingDigitsAfterSign { span }) } else { // The first digit was already consumed by the caller: // "<4>2", "<3>.14" // This block consumes the remaining integer digits: "4<2>" self.eat_while(|c| c.is_ascii_digit()); + Ok(()) } + } - if self.chars.next_if(|(_, c)| *c == '.').is_some() { - // Optional fractional part: a '.' followed by one or more digits. - // "3<.14>", "0<.5>" - // A '.' with no digits after it ("3.") is an error. - if !self.eat_one_or_more_digits() { - return Err(Error::MissingFractionalDigits { - span: Span { lo, hi: self.pos() }, - }); - } - is_double = true; - } - if self - .chars - .next_if(|(_, c)| *c == 'e' || *c == 'E') - .is_some() - { - // Optional exponent: 'e'/'E', an optional sign, then one or more digits. - // "1", "2.5", "6" - // A bare "1e" or "1e-" (no exponent digits) is an error. - self.chars.next_if(|(_, c)| *c == '+' || *c == '-'); - if !self.eat_one_or_more_digits() { - return Err(Error::MissingExponentDigits { - span: Span { lo, hi: self.pos() }, - }); - } - is_double = true; - } + fn scan_number(&mut self, lo: u32, signed: bool) -> Result { + self.scan_integer_part(lo, signed)?; + let has_fraction = self.scan_fraction(lo)?; + let has_exponent = self.scan_exponent(lo)?; + let has_rad_suffix = self.scan_rad_suffix()?; - // No '.' and no exponent => an unsigned integer ("42" => Uint); - // a sign, '.', or exponent makes it a Double ("-42", "3.14", "1e9"). - Ok(if is_double { - TokenKind::Double + Ok(if has_rad_suffix { + TokenKind::Double(DoubleUnit::Radians) + } else if signed || has_fraction || has_exponent { + TokenKind::Double(DoubleUnit::Default) } else { TokenKind::Uint }) diff --git a/source/compiler/stim_compiler/src/lex/tests/number.rs b/source/compiler/stim_compiler/src/lex/tests/number.rs index be1be612a8f..7ed3f8c3c0a 100644 --- a/source/compiler/stim_compiler/src/lex/tests/number.rs +++ b/source/compiler/stim_compiler/src/lex/tests/number.rs @@ -320,3 +320,89 @@ fn double_sign_recovers_to_a_double() { double(+1) [1-3]"#]], ); } + +#[test] +fn unsigned_integer_with_rad_suffix_lexes_as_double() { + check("1rad", &expect!["double(1rad) [0-4]"]); + check("12rad", &expect!["double(12rad) [0-5]"]); + check("123rad", &expect!["double(123rad) [0-6]"]); +} + +#[test] +fn signed_integer_with_rad_suffix_lexes_as_double() { + check("+1rad", &expect!["double(+1rad) [0-5]"]); + check("-1rad", &expect!["double(-1rad) [0-5]"]); +} + +#[test] +fn double_radians() { + check("1.0rad", &expect!["double(1.0rad) [0-6]"]); + check("3.14rad", &expect!["double(3.14rad) [0-7]"]); + check("2e-5rad", &expect!["double(2e-5rad) [0-7]"]); + check("-0.01rad", &expect!["double(-0.01rad) [0-8]"]); +} + +#[test] +fn rad_suffix_preserves_delimiters() { + check( + "1rad,2rad)", + &expect![[r#" + double(1rad) [0-4] + comma(,) [4-5] + double(2rad) [5-9] + close(paren)()) [9-10]"#]], + ); +} + +#[test] +fn rad_suffix_followed_by_invalid_character_yields_error() { + check( + "1radX", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1radX + : ^ + `---- + "#]], + ); + check( + "1radx", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1radx + : ^ + `---- + "#]], + ); + check( + "1rad0", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1rad0 + : ^ + `---- + "#]], + ); + check( + "1rad_foo", + &expect![[r#" + Qdk.Stim.Lex.UnrecognizedCharacter + + x unrecognized character + ,---- + 1 | 1rad_foo + : ^ + `---- + + instruction_name(foo) [5-8]"#]], + ); +} diff --git a/source/compiler/stim_compiler/src/parser.rs b/source/compiler/stim_compiler/src/parser.rs index f309a1908e8..f4fbfd6d485 100644 --- a/source/compiler/stim_compiler/src/parser.rs +++ b/source/compiler/stim_compiler/src/parser.rs @@ -7,7 +7,7 @@ mod tests; use crate::lex::{ self, Delim::{Brace, Paren}, - Lexer, Token, + DoubleUnit, Lexer, Token, TokenKind::{self}, }; use miette::Diagnostic; @@ -80,7 +80,7 @@ pub struct Instruction { pub span: Span, pub name: String, pub tag: Option, - pub args: Vec, + pub args: Vec, pub targets: Vec, } @@ -94,6 +94,33 @@ impl Display for Instruction { } } +#[derive(Debug, Clone, Copy)] +pub struct Arg { + pub span: Span, + pub value: ArgValue, +} + +impl Display for Arg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Arg {}: {}", self.span, self.value) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ArgValue { + Default(f64), + Radians(f64), +} + +impl Display for ArgValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArgValue::Default(value) => write!(f, "{value}"), + ArgValue::Radians(value) => write!(f, "{value} rad"), + } + } +} + #[derive(Debug)] pub struct Target { pub span: Span, @@ -250,6 +277,12 @@ pub enum Error { #[label] span: Span, }, + #[error("floating-point literal is too large to fit in a 64-bit float")] + #[diagnostic(code("Qdk.Stim.Parser.FloatTooLarge"))] + FloatTooLarge { + #[label] + span: Span, + }, #[error("measurement record offset cannot be zero; the most recent measurement is rec[-1]")] #[diagnostic(code("Qdk.Stim.Parser.ZeroMeasurementRecord"))] ZeroMeasurementRecord { @@ -369,26 +402,6 @@ impl<'a> Parser<'a> { } } - fn expect_number(&mut self) -> Option { - match self.next() { - Some(token) if token.kind == TokenKind::Uint || token.kind == TokenKind::Double => { - Some(token) - } - Some(token) => { - self.emit_error(Error::Expected { - expected: "number", - found: token.kind, - span: token.span, - }); - None - } - None => { - self.emit_eof_error(); - None - } - } - } - fn expect_line_end(&mut self) -> Option<()> { match self.peek() { None => Some(()), // End of file @@ -493,17 +506,14 @@ impl<'a> Parser<'a> { fn parse_instruction(&mut self) -> Option { let name_token = self.expect_token(TokenKind::InstructionName)?; let lo = name_token.span.lo; - let name = self.extract_string(name_token, None); + let name = self.extract_string(name_token.span); let tag_token = self.next_if(|t| t.kind == TokenKind::Tag); let tag: Option = tag_token.map(|tag_token| { - self.extract_string( - tag_token, - Some(Span { - lo: tag_token.span.lo + 1, - hi: tag_token.span.hi - 1, - }), - ) + self.extract_string(Span { + lo: tag_token.span.lo + 1, + hi: tag_token.span.hi - 1, + }) }); let mut args = Vec::new(); @@ -520,8 +530,7 @@ impl<'a> Parser<'a> { .peek() .is_some_and(|t| t.kind != TokenKind::Close(Paren)) { - let arg = self.expect_number()?; - args.push(self.extract_double(arg, None)); + args.push(self.parse_arg()?); } // Each subsequent arg must be preceded by a comma while self @@ -529,8 +538,7 @@ impl<'a> Parser<'a> { .is_some_and(|t| t.kind != TokenKind::Close(Paren)) { self.expect_token(TokenKind::Comma)?; - let arg = self.expect_number()?; - args.push(self.extract_double(arg, None)); + args.push(self.parse_arg()?); } paren_hi = Some(self.expect_token(TokenKind::Close(Paren))?.span.hi); } @@ -560,6 +568,36 @@ impl<'a> Parser<'a> { }) } + fn parse_arg(&mut self) -> Option { + let token = self.expect_any()?; + + let value = match token.kind { + TokenKind::Uint | TokenKind::Double(DoubleUnit::Default) => { + ArgValue::Default(self.extract_double(token.span)?) + } + TokenKind::Double(DoubleUnit::Radians) => { + let value_span = Span { + lo: token.span.lo, + hi: token.span.hi - 3, // strip "rad" suffix + }; + ArgValue::Radians(self.extract_double(value_span)?) + } + found => { + self.emit_error(Error::Expected { + expected: "number", + found, + span: token.span, + }); + return None; + } + }; + + Some(Arg { + span: token.span, + value, + }) + } + fn parse_target(&mut self) -> Option { let negated_token = self.next_if(|t| t.kind == TokenKind::Bang); let negated = negated_token.is_some(); @@ -722,13 +760,20 @@ impl<'a> Parser<'a> { } } - fn extract_double(&self, token: Token, span: Option) -> f64 { - self.extract_string(token, span) + fn extract_double(&mut self, value_span: Span) -> Option { + let value = self + .slice_input(value_span) .parse::() - .unwrap_or_else(|_| unreachable!("lexer guarantees a valid double literal")) + .unwrap_or_else(|_| unreachable!("lexer guarantees a valid double literal")); + + if !value.is_finite() { + self.emit_error(Error::FloatTooLarge { span: value_span }); + return None; + } + Some(value) } - fn extract_string(&self, token: Token, span: Option) -> String { - self.slice_input(span.unwrap_or(token.span)).to_string() + fn extract_string(&self, source_span: Span) -> String { + self.slice_input(source_span).to_string() } } diff --git a/source/compiler/stim_compiler/src/parser/tests/arguments.rs b/source/compiler/stim_compiler/src/parser/tests/arguments.rs index 6fb5ecb99d3..6b149c2dc8e 100644 --- a/source/compiler/stim_compiler/src/parser/tests/arguments.rs +++ b/source/compiler/stim_compiler/src/parser/tests/arguments.rs @@ -9,16 +9,16 @@ fn single_arg() { check( "DEPOLARIZE1(0.001) 0", &expect![[r#" - Circuit [0-20]: - items: - Instruction [0-20]: - name: DEPOLARIZE1 - tag: - args: - 0.001 - targets: - Target [19-20]: - kind: Qubit(0)"#]], + Circuit [0-20]: + items: + Instruction [0-20]: + name: DEPOLARIZE1 + tag: + args: + Arg [12-17]: 0.001 + targets: + Target [19-20]: + kind: Qubit(0)"#]], ); } @@ -27,18 +27,18 @@ fn multiple_comma_separated_args() { check( "PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", &expect![[r#" - Circuit [0-35]: - items: - Instruction [0-35]: - name: PAULI_CHANNEL_1 - tag: - args: - 0.01 - 0.02 - 0.03 - targets: - Target [34-35]: - kind: Qubit(0)"#]], + Circuit [0-35]: + items: + Instruction [0-35]: + name: PAULI_CHANNEL_1 + tag: + args: + Arg [16-20]: 0.01 + Arg [22-26]: 0.02 + Arg [28-32]: 0.03 + targets: + Target [34-35]: + kind: Qubit(0)"#]], ); } @@ -47,16 +47,146 @@ fn scientific_notation_arg() { check( "X_ERROR(1e-3) 0", &expect![[r#" - Circuit [0-15]: - items: - Instruction [0-15]: - name: X_ERROR - tag: - args: - 0.001 - targets: - Target [14-15]: - kind: Qubit(0)"#]], + Circuit [0-15]: + items: + Instruction [0-15]: + name: X_ERROR + tag: + args: + Arg [8-12]: 0.001 + targets: + Target [14-15]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn radians_args() { + check( + "R_X(1rad) 0", + &expect![[r#" + Circuit [0-11]: + items: + Instruction [0-11]: + name: R_X + tag: + args: + Arg [4-8]: 1 rad + targets: + Target [10-11]: + kind: Qubit(0)"#]], + ); + check( + "R_Y(-0.5rad) 0", + &expect![[r#" + Circuit [0-14]: + items: + Instruction [0-14]: + name: R_Y + tag: + args: + Arg [4-11]: -0.5 rad + targets: + Target [13-14]: + kind: Qubit(0)"#]], + ); + check( + "R_Z(+2.5e-3rad) 0", + &expect![[r#" + Circuit [0-17]: + items: + Instruction [0-17]: + name: R_Z + tag: + args: + Arg [4-14]: 0.0025 rad + targets: + Target [16-17]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn mixed_unit_args() { + check( + "U3(0.1, -0.2rad, 3e-1rad) 0", + &expect![[r#" + Circuit [0-27]: + items: + Instruction [0-27]: + name: U3 + tag: + args: + Arg [3-6]: 0.1 + Arg [8-15]: -0.2 rad + Arg [17-24]: 0.3 rad + targets: + Target [26-27]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn unitless_float_too_large_is_error() { + check( + "X_ERROR(1e999) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | X_ERROR(1e999) 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn negative_unitless_float_too_large_is_error() { + check( + "X_ERROR(-1e999) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | X_ERROR(-1e999) 0 + : ^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn radians_float_too_large_is_error() { + check( + "R_X(1e999rad) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | R_X(1e999rad) 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn negative_radians_float_too_large_is_error() { + check( + "R_X(-1e999rad) 0", + &expect![[r#" + Qdk.Stim.Parser.FloatTooLarge + + x floating-point literal is too large to fit in a 64-bit float + ,---- + 1 | R_X(-1e999rad) 0 + : ^^^^^^ + `---- + "#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs index 4ccaa14ca11..de44d61a986 100644 --- a/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs +++ b/source/compiler/stim_compiler/src/parser/tests/instruction_shapes.rs @@ -52,7 +52,7 @@ fn args_no_targets() { name: X_ERROR tag: args: - 0.1 + Arg [8-11]: 0.1 targets: "#]], ); } @@ -62,18 +62,18 @@ fn args_with_targets() { check( "X_ERROR(0.1) 0 1", &expect![[r#" - Circuit [0-16]: - items: - Instruction [0-16]: - name: X_ERROR - tag: - args: - 0.1 - targets: - Target [13-14]: - kind: Qubit(0) - Target [15-16]: - kind: Qubit(1)"#]], + Circuit [0-16]: + items: + Instruction [0-16]: + name: X_ERROR + tag: + args: + Arg [8-11]: 0.1 + targets: + Target [13-14]: + kind: Qubit(0) + Target [15-16]: + kind: Qubit(1)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/spans.rs b/source/compiler/stim_compiler/src/parser/tests/spans.rs index 853185aeecb..253d85007e1 100644 --- a/source/compiler/stim_compiler/src/parser/tests/spans.rs +++ b/source/compiler/stim_compiler/src/parser/tests/spans.rs @@ -99,11 +99,49 @@ fn span_includes_args_when_no_targets() { name: X_ERROR tag: args: - 0.1 + Arg [8-11]: 0.1 targets: "#]], ); } +#[test] +fn each_arg_gets_its_own_span() { + check( + "PAULI_CHANNEL_1(0.01, 0.02, 0.03) 0", + &expect![[r#" + Circuit [0-35]: + items: + Instruction [0-35]: + name: PAULI_CHANNEL_1 + tag: + args: + Arg [16-20]: 0.01 + Arg [22-26]: 0.02 + Arg [28-32]: 0.03 + targets: + Target [34-35]: + kind: Qubit(0)"#]], + ); +} + +#[test] +fn radians_arg_span_includes_the_suffix() { + check( + "R_X(-0.5rad) 0", + &expect![[r#" + Circuit [0-14]: + items: + Instruction [0-14]: + name: R_X + tag: + args: + Arg [4-11]: -0.5 rad + targets: + Target [13-14]: + kind: Qubit(0)"#]], + ); +} + #[test] fn span_includes_tag_when_no_targets() { // A tag extends the instruction span even when there are no targets. @@ -127,16 +165,16 @@ fn span_extends_past_tag_and_args_to_target() { check( "X_ERROR[t](0.1) 5\n", &expect![[r#" - Circuit [0-18]: - items: - Instruction [0-17]: - name: X_ERROR - tag: t - args: - 0.1 - targets: - Target [16-17]: - kind: Qubit(5)"#]], + Circuit [0-18]: + items: + Instruction [0-17]: + name: X_ERROR + tag: t + args: + Arg [11-14]: 0.1 + targets: + Target [16-17]: + kind: Qubit(5)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/tags.rs b/source/compiler/stim_compiler/src/parser/tests/tags.rs index 013de216fac..0f6cc8d68da 100644 --- a/source/compiler/stim_compiler/src/parser/tests/tags.rs +++ b/source/compiler/stim_compiler/src/parser/tests/tags.rs @@ -59,16 +59,16 @@ fn tag_with_args_and_targets() { check( "X_ERROR[t](0.1) 0", &expect![[r#" - Circuit [0-17]: - items: - Instruction [0-17]: - name: X_ERROR - tag: t - args: - 0.1 - targets: - Target [16-17]: - kind: Qubit(0)"#]], + Circuit [0-17]: + items: + Instruction [0-17]: + name: X_ERROR + tag: t + args: + Arg [11-14]: 0.1 + targets: + Target [16-17]: + kind: Qubit(0)"#]], ); } diff --git a/source/compiler/stim_compiler/src/parser/tests/targets.rs b/source/compiler/stim_compiler/src/parser/tests/targets.rs index 87a661abb69..6a1a5596de5 100644 --- a/source/compiler/stim_compiler/src/parser/tests/targets.rs +++ b/source/compiler/stim_compiler/src/parser/tests/targets.rs @@ -439,16 +439,16 @@ fn loss_target() { check( "E(0.01) L0", &expect![[r#" - Circuit [0-10]: - items: - Instruction [0-10]: - name: E - tag: - args: - 0.01 - targets: - Target [8-10]: - kind: Loss(0)"#]], + Circuit [0-10]: + items: + Instruction [0-10]: + name: E + tag: + args: + Arg [2-6]: 0.01 + targets: + Target [8-10]: + kind: Loss(0)"#]], ); } diff --git a/source/compiler/stim_compiler/src/qir.rs b/source/compiler/stim_compiler/src/qir.rs index 5df52ee74d1..5cef0aab557 100644 --- a/source/compiler/stim_compiler/src/qir.rs +++ b/source/compiler/stim_compiler/src/qir.rs @@ -22,9 +22,7 @@ type StimQubitId = u32; type QubitId = u32; type ResultId = u32; -// Angle units -type HalfTurns = f64; // used in qdk-stim -type Radians = f64; // used in QIR +type Radians = f64; struct QirWriter { output: String, @@ -386,6 +384,13 @@ pub enum Error { #[label] span: Span, }, + #[error("argument for {instruction} cannot be specified in radians")] + #[diagnostic(code("Qdk.Stim.Compiler.UnexpectedRadians"))] + UnexpectedRadians { + instruction: String, + #[label] + span: Span, + }, #[error("missing argument in instruction: {instruction}")] #[diagnostic(code("Qdk.Stim.Compiler.MissingArg"))] MissingArg { @@ -393,38 +398,34 @@ pub enum Error { #[label] span: Span, }, - #[error("instruction {instruction} requires {expected} arguments, but found {found}")] - #[diagnostic(code("Qdk.Stim.Compiler.WrongArgCount"))] - WrongArgCount { + #[error("too few arguments for instruction {instruction}; expected {expected}, found {found}")] + #[diagnostic(code("Qdk.Stim.Compiler.TooFewArgs"))] + TooFewArgs { instruction: String, expected: usize, found: usize, #[label] span: Span, }, - #[error( - "angle for {instruction} must be finite and representable in radians; found {angle} half turns" - )] - #[diagnostic(code("Qdk.Stim.Compiler.InvalidAngle"))] - InvalidAngle { + #[error("too many arguments for instruction {instruction}; expected {expected}, found {found}")] + #[diagnostic(code("Qdk.Stim.Compiler.TooManyArgs"))] + TooManyArgs { instruction: String, - angle: HalfTurns, + expected: usize, + found: usize, #[label] span: Span, }, - #[error("too many arguments for instruction {instruction}; expected at most {expected}")] - #[diagnostic(code("Qdk.Stim.Compiler.TooManyArgs"))] - TooManyArgs { + #[error("angle for {instruction} must be finite and representable in radians")] + #[diagnostic(code("Qdk.Stim.Compiler.InvalidAngle"))] + InvalidAngle { instruction: String, - expected: usize, #[label] span: Span, }, - #[error( - "readout noise probability for {instruction} must be between 0 and 1; found {probability}" - )] - #[diagnostic(code("Qdk.Stim.Compiler.InvalidReadoutNoiseProbability"))] - InvalidReadoutNoiseProbability { + #[error("probability for {instruction} must be between 0 and 1; found {probability}")] + #[diagnostic(code("Qdk.Stim.Compiler.InvalidProbability"))] + InvalidProbability { instruction: String, probability: f64, #[label] @@ -464,6 +465,13 @@ pub enum Error { #[label] span: Span, }, + #[error("instruction {instruction} requires a multiple of three targets")] + #[diagnostic(code("Qdk.Stim.Compiler.TargetCountNotMultipleOfThree"))] + TargetCountNotMultipleOfThree { + instruction: String, + #[label] + span: Span, + }, #[error("measurement record target in an unsupported position in instruction: {instruction}")] #[diagnostic(code("Qdk.Stim.Compiler.MisplacedMeasurementRecord"))] MisplacedMeasurementRecord { @@ -1243,7 +1251,7 @@ impl<'noise> Compiler<'noise> { } "I_ERROR" => (), "PAULI_CHANNEL_1" => { - let Some(probabilities) = self.expect_args(instruction, 3) else { + let Some(probabilities) = self.expect_probabilities(instruction, 3) else { return; }; let Some(table) = self.build_noise_table( @@ -1259,7 +1267,7 @@ impl<'noise> Compiler<'noise> { }); } "PAULI_CHANNEL_2" => { - let Some(probabilities) = self.expect_args(instruction, 15) else { + let Some(probabilities) = self.expect_probabilities(instruction, 15) else { return; }; @@ -1403,7 +1411,7 @@ impl<'noise> Compiler<'noise> { // Miscellaneous "PEEK_LOSS" => { // similar to broadcast_measure, but doesn't allow negated qubits - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; self.for_each_qubit(instruction, |s, q| { @@ -1419,10 +1427,60 @@ impl<'noise> Compiler<'noise> { // Non-Clifford Gates "T" => self.broadcast(instruction, |s, q| s.op("t", q)), "T_DAG" => self.broadcast(instruction, |s, q| s.op_adj("t", q)), + "TPP" | "TPP_DAG" => self.broadcast_pauli_product(instruction, |s, q, negated| { + let invert = (instruction.name == "TPP_DAG") ^ negated; + if invert { + s.op_adj("t", q); + } else { + s.op("t", q); + } + }), + "CH" => self.broadcast_pair(instruction, |s, q0, q1| { + // Clifft decomposition: R_Y(0.25 pi) 1; CX 0 1; R_Y(-0.25 pi) 1 + s.op_rotation("ry", 0.25 * PI, q1); + s.op_2("cx", q0, q1); + s.op_rotation("ry", -0.25 * PI, q1); + }), + "CCZ" => self.broadcast_triple(instruction, |s, q0, q1, q2| { + // Clifft decomposition: H 2; CCX 0 1 2; H 2 + s.op("h", q2); + s.op_3("ccx", q0, q1, q2); + s.op("h", q2); + }), + "CCX" => self.broadcast_triple(instruction, |s, q0, q1, q2| { + s.op_3("ccx", q0, q1, q2); + }), "R_X" | "R_Y" | "R_Z" => self.broadcast_rotation(instruction, |s, angle, q| { s.op_rotation(&instruction.name.to_lowercase().replace("_", ""), angle, q); }), - + "U3" | "U" => { + let Some(angles) = self.expect_angles(instruction, 3) else { + return; + }; + self.for_each_qubit(instruction, |s, q| { + s.op_rotation("rz", angles[2], q); + s.op_rotation("ry", angles[0], q); + s.op_rotation("rz", angles[1], q); + }); + } + "R_XX" | "R_YY" | "R_ZZ" => { + self.broadcast_pair_rotation(instruction, |s, angle, q0, q1| { + s.op_rotation_2( + &instruction.name.to_lowercase().replace("_", ""), + angle, + q0, + q1, + ); + }) + } + "R_PAULI" => { + let Some(angle) = self.expect_angle(instruction) else { + return; + }; + self.for_each_pauli_product(instruction, |s, q, negated| { + s.op_rotation("rz", if negated { -angle } else { angle }, q); + }); + } _ => self.unknown(instruction), } } @@ -1474,187 +1532,186 @@ impl<'noise> Compiler<'noise> { } } - fn broadcast( + fn for_each_pair( &mut self, instruction: &Instruction, - operation: impl FnMut(&mut Self, StimQubitId), + mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId), ) { - self.unsupported_args(instruction); - self.for_each_qubit(instruction, operation); + let Some(pairs) = self.expect_target_pairs(instruction) else { + return; + }; + for pair in pairs { + let Some((q0, _)) = self.expect_qubit(instruction, &pair[0], false) else { + continue; + }; + let Some((q1, _)) = self.expect_qubit(instruction, &pair[1], false) else { + continue; + }; + operation(self, q0, q1); + } } - fn broadcast_measure( + fn for_each_negatable_pair( &mut self, instruction: &Instruction, - mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, + mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool), ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(pairs) = self.expect_target_pairs(instruction) else { return; }; - self.for_each_negatable_qubit(instruction, |s, q, negated| { - let result_id = measure(s, q, negated); - s.op_readout_noise(readout_noise, result_id); - }); + for pair in pairs { + let Some((q0, neg0)) = self.expect_qubit(instruction, &pair[0], true) else { + continue; + }; + let Some((q1, neg1)) = self.expect_qubit(instruction, &pair[1], true) else { + continue; + }; + operation(self, q0, q1, neg0 ^ neg1); + } } - fn broadcast_noise( + fn for_each_triple( &mut self, instruction: &Instruction, - mut noise: impl FnMut(&mut Self, StimQubitId, f64), + mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, StimQubitId), ) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(triples) = self.expect_target_triples(instruction) else { return; }; - self.for_each_qubit(instruction, |s, q| noise(s, q, probability)); + for triple in triples { + let Some((q0, _)) = self.expect_qubit(instruction, &triple[0], false) else { + continue; + }; + let Some((q1, _)) = self.expect_qubit(instruction, &triple[1], false) else { + continue; + }; + let Some((q2, _)) = self.expect_qubit(instruction, &triple[2], false) else { + continue; + }; + operation(self, q0, q1, q2); + } } - fn broadcast_pauli_product( + fn broadcast( &mut self, instruction: &Instruction, - operation: impl FnMut(&mut Self, StimQubitId, bool), + operation: impl FnMut(&mut Self, StimQubitId), ) { self.unsupported_args(instruction); - self.for_each_pauli_product(instruction, operation); + self.for_each_qubit(instruction, operation); } - fn broadcast_pauli_product_measure( + fn broadcast_pair( + &mut self, + instruction: &Instruction, + operation: impl FnMut(&mut Self, StimQubitId, StimQubitId), + ) { + self.unsupported_args(instruction); + self.for_each_pair(instruction, operation); + } + + fn broadcast_triple( + &mut self, + instruction: &Instruction, + operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, StimQubitId), + ) { + self.unsupported_args(instruction); + self.for_each_triple(instruction, operation); + } + + fn broadcast_measure( &mut self, instruction: &Instruction, mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; - self.for_each_pauli_product(instruction, |s, q, negated| { + self.for_each_negatable_qubit(instruction, |s, q, negated| { let result_id = measure(s, q, negated); s.op_readout_noise(readout_noise, result_id); }); } - fn broadcast_rotation( + fn broadcast_pair_measure( &mut self, instruction: &Instruction, - mut operation: impl FnMut(&mut Self, Radians, StimQubitId), + mut measure: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool) -> ResultId, ) { - let Some(angle) = self.expect_angle(instruction) else { - return; - }; - self.for_each_qubit(instruction, |s, q| operation(s, angle, q)); - } - - fn accumulate_correlated_noise(&mut self, instruction: &Instruction) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; - let mut terms = Vec::with_capacity(instruction.targets.len()); - - for target in &instruction.targets { - let Some((fault, qubit)) = self.expect_fault_char(instruction, target) else { - continue; - }; - - terms.push((fault, qubit)); - } - - let row = CorrelatedRow { - probability, - terms, - span: instruction.span, - }; - - self.noise_accumulator.push_correlated_row(row); - } - - fn continue_correlated_noise(&mut self, instruction: &Instruction) { - if self.noise_accumulator.current_correlated_group.is_none() { - self.push_error(Error::OrphanedElseCorrelatedError { - span: instruction.span, - }); - return; - } - self.accumulate_correlated_noise(instruction); - } - - fn finish_correlated_noise(&mut self) { - if self.noise_accumulator.current_correlated_group.is_none() { - return; - } - match self.noise_accumulator.flush_correlated_group() { - Ok((noise_table, qubits)) => self.op_noise(noise_table, &qubits), - Err(error) => self.push_error(error), - } + self.for_each_negatable_pair(instruction, |s, q0, q1, negated| { + let result_id = measure(s, q0, q1, negated); + s.op_readout_noise(readout_noise, result_id); + }); } - fn for_each_pair( + fn broadcast_noise( &mut self, instruction: &Instruction, - mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId), + mut noise: impl FnMut(&mut Self, StimQubitId, f64), ) { - let Some(pairs) = self.expect_target_pairs(instruction) else { + let Some(probability) = self.expect_probability(instruction) else { return; }; - for pair in pairs { - let Some((q0, _)) = self.expect_qubit(instruction, &pair[0], false) else { - continue; - }; - let Some((q1, _)) = self.expect_qubit(instruction, &pair[1], false) else { - continue; - }; - operation(self, q0, q1); - } + self.for_each_qubit(instruction, |s, q| noise(s, q, probability)); } - fn for_each_negatable_pair( + fn broadcast_pair_noise( &mut self, instruction: &Instruction, - mut operation: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool), + mut noise: impl FnMut(&mut Self, StimQubitId, StimQubitId, f64), ) { - let Some(pairs) = self.expect_target_pairs(instruction) else { + let Some(probability) = self.expect_probability(instruction) else { return; }; - for pair in pairs { - let Some((q0, neg0)) = self.expect_qubit(instruction, &pair[0], true) else { - continue; - }; - let Some((q1, neg1)) = self.expect_qubit(instruction, &pair[1], true) else { - continue; - }; - operation(self, q0, q1, neg0 ^ neg1); - } + self.for_each_pair(instruction, |s, q0, q1| noise(s, q0, q1, probability)); } - fn broadcast_pair( + fn broadcast_pauli_product( &mut self, instruction: &Instruction, - operation: impl FnMut(&mut Self, StimQubitId, StimQubitId), + operation: impl FnMut(&mut Self, StimQubitId, bool), ) { self.unsupported_args(instruction); - self.for_each_pair(instruction, operation); + self.for_each_pauli_product(instruction, operation); } - fn broadcast_pair_measure( + fn broadcast_pauli_product_measure( &mut self, instruction: &Instruction, - mut measure: impl FnMut(&mut Self, StimQubitId, StimQubitId, bool) -> ResultId, + mut measure: impl FnMut(&mut Self, StimQubitId, bool) -> ResultId, ) { - let Some(readout_noise) = self.expect_readout_noise(instruction) else { + let Some(readout_noise) = self.expect_probability_or_zero(instruction) else { return; }; - self.for_each_negatable_pair(instruction, |s, q0, q1, negated| { - let result_id = measure(s, q0, q1, negated); + self.for_each_pauli_product(instruction, |s, q, negated| { + let result_id = measure(s, q, negated); s.op_readout_noise(readout_noise, result_id); }); } - fn broadcast_pair_noise( + fn broadcast_rotation( &mut self, instruction: &Instruction, - mut noise: impl FnMut(&mut Self, StimQubitId, StimQubitId, f64), + mut operation: impl FnMut(&mut Self, Radians, StimQubitId), ) { - let Some(probability) = self.expect_arg(instruction) else { + let Some(angle) = self.expect_angle(instruction) else { return; }; - self.for_each_pair(instruction, |s, q0, q1| noise(s, q0, q1, probability)); + self.for_each_qubit(instruction, |s, q| operation(s, angle, q)); + } + + fn broadcast_pair_rotation( + &mut self, + instruction: &Instruction, + mut operation: impl FnMut(&mut Self, Radians, StimQubitId, StimQubitId), + ) { + let Some(angle) = self.expect_angle(instruction) else { + return; + }; + self.for_each_pair(instruction, |s, q0, q1| operation(s, angle, q0, q1)); } fn broadcast_controlled( @@ -1747,6 +1804,49 @@ impl<'noise> Compiler<'noise> { self.writer.write_classical_control(pauli, result_id, qubit); } + fn accumulate_correlated_noise(&mut self, instruction: &Instruction) { + let Some(probability) = self.expect_probability(instruction) else { + return; + }; + let mut terms = Vec::with_capacity(instruction.targets.len()); + + for target in &instruction.targets { + let Some((fault, qubit)) = self.expect_fault_char(instruction, target) else { + continue; + }; + + terms.push((fault, qubit)); + } + + let row = CorrelatedRow { + probability, + terms, + span: instruction.span, + }; + + self.noise_accumulator.push_correlated_row(row); + } + + fn continue_correlated_noise(&mut self, instruction: &Instruction) { + if self.noise_accumulator.current_correlated_group.is_none() { + self.push_error(Error::OrphanedElseCorrelatedError { + span: instruction.span, + }); + return; + } + self.accumulate_correlated_noise(instruction); + } + + fn finish_correlated_noise(&mut self) { + if self.noise_accumulator.current_correlated_group.is_none() { + return; + } + match self.noise_accumulator.flush_correlated_group() { + Ok((noise_table, qubits)) => self.op_noise(noise_table, &qubits), + Err(error) => self.push_error(error), + } + } + /// Converts a Pauli product to a canonical form: one factor per qubit, sorted by /// qubit index, with identity factors removed. Rejects anti-Hermitian products and /// represents an overall phase of -1 as a negation. @@ -1861,6 +1961,30 @@ impl<'noise> Compiler<'noise> { self.writer.write_qis_call(intrinsic, &[q]); } + fn op_2(&mut self, intrinsic: &str, q0: StimQubitId, q1: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + self.writer.write_qis_call(intrinsic, &[q0, q1]); + } + + fn op_3(&mut self, intrinsic: &str, q0: StimQubitId, q1: StimQubitId, q2: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + let q2 = self.id_map.allocate_qubit(q2); + self.writer.write_qis_call(intrinsic, &[q0, q1, q2]); + } + + fn op_rotation(&mut self, intrinsic: &str, angle: Radians, qubit: StimQubitId) { + let qubit = self.id_map.allocate_qubit(qubit); + self.writer.write_rotation_call(intrinsic, angle, &[qubit]); + } + + fn op_rotation_2(&mut self, intrinsic: &str, angle: Radians, q0: StimQubitId, q1: StimQubitId) { + let q0 = self.id_map.allocate_qubit(q0); + let q1 = self.id_map.allocate_qubit(q1); + self.writer.write_rotation_call(intrinsic, angle, &[q0, q1]); + } + fn op_adj(&mut self, intrinsic: &str, qubit: StimQubitId) { let q = self.id_map.allocate_qubit(qubit); self.writer.write_qis_adj_call(intrinsic, &[q]); @@ -1897,12 +2021,6 @@ impl<'noise> Compiler<'noise> { r } - fn op_2(&mut self, intrinsic: &str, q0: StimQubitId, q1: StimQubitId) { - let q0 = self.id_map.allocate_qubit(q0); - let q1 = self.id_map.allocate_qubit(q1); - self.writer.write_qis_call(intrinsic, &[q0, q1]); - } - fn op_noise(&mut self, table: NoiseTable, qubits: &[StimQubitId]) { let ids: Vec = qubits .iter() @@ -1918,11 +2036,6 @@ impl<'noise> Compiler<'noise> { } } - fn op_rotation(&mut self, intrinsic: &str, angle: Radians, qubit: StimQubitId) { - let qubit = self.id_map.allocate_qubit(qubit); - self.writer.write_rotation_call(intrinsic, angle, &[qubit]); - } - fn build_noise_table( &mut self, num_qubits: u32, @@ -2193,80 +2306,150 @@ impl<'noise> Compiler<'noise> { } } - fn expect_angle(&mut self, instruction: &Instruction) -> Option { - let angle: HalfTurns = self.expect_arg(instruction)?; - let radians = angle * PI; - if !radians.is_finite() { - self.push_error(Error::InvalidAngle { + fn expect_target_pairs<'a>( + &mut self, + instruction: &'a Instruction, + ) -> Option> { + if !instruction.targets.len().is_multiple_of(2) { + self.push_error(Error::OddTargetCount { instruction: instruction.name.clone(), - angle, span: instruction.span, }); return None; } - Some(radians) - } - - fn expect_arg(&mut self, instruction: &Instruction) -> Option { - self.expect_args(instruction, 1).map(|args| args[0]) + Some(instruction.targets.chunks(2)) } - fn expect_args(&mut self, instruction: &Instruction, expected: usize) -> Option> { - if instruction.args.is_empty() { - self.push_error(Error::MissingArg { + fn expect_target_triples<'a>( + &mut self, + instruction: &'a Instruction, + ) -> Option> { + if !instruction.targets.len().is_multiple_of(3) { + self.push_error(Error::TargetCountNotMultipleOfThree { instruction: instruction.name.clone(), span: instruction.span, }); return None; } - if instruction.args.len() != expected { - self.push_error(Error::WrongArgCount { - instruction: instruction.name.clone(), - expected, - found: instruction.args.len(), - span: instruction.span, - }); - return None; + Some(instruction.targets.chunks(3)) + } + + fn expect_angle(&mut self, instruction: &Instruction) -> Option { + self.expect_angles(instruction, 1)?.pop() + } + + fn expect_angles( + &mut self, + instruction: &Instruction, + expected: usize, + ) -> Option> { + let args = self.expect_args(instruction, expected)?; + let mut radians = Vec::with_capacity(args.len()); + let mut has_invalid_angle = false; + + for arg in args { + let angle_in_radians = match arg.value { + ArgValue::Default(half_turns) => half_turns * PI, + ArgValue::Radians(radians) => radians, + }; + + if angle_in_radians.is_finite() { + radians.push(angle_in_radians); + } else { + self.push_error(Error::InvalidAngle { + instruction: instruction.name.clone(), + span: arg.span, + }); + has_invalid_angle = true; + } + } + + if !has_invalid_angle { + Some(radians) + } else { + None } - Some(instruction.args.clone()) } - fn expect_target_pairs<'a>( + fn expect_probability_or_zero(&mut self, instruction: &Instruction) -> Option { + if instruction.args.is_empty() { + return Some(0.0); + } + self.expect_probability(instruction) + } + + fn expect_probability(&mut self, instruction: &Instruction) -> Option { + self.expect_probabilities(instruction, 1)?.pop() + } + + fn expect_probabilities( &mut self, - instruction: &'a Instruction, - ) -> Option> { - if !instruction.targets.len().is_multiple_of(2) { - self.push_error(Error::OddTargetCount { + instruction: &Instruction, + expected: usize, + ) -> Option> { + let args = self.expect_args(instruction, expected)?; + + let mut probabilities = Vec::with_capacity(args.len()); + let mut has_invalid_probability = false; + for arg in args { + let value = match arg.value { + ArgValue::Default(value) => value, + ArgValue::Radians(value) => { + self.push_error(Error::UnexpectedRadians { + instruction: instruction.name.clone(), + span: arg.span, + }); + has_invalid_probability = true; + value + } + }; + + if (0.0..=1.0).contains(&value) { + probabilities.push(value); + } else { + self.push_error(Error::InvalidProbability { + instruction: instruction.name.clone(), + probability: value, + span: instruction.span, + }); + has_invalid_probability = true; + } + } + if !has_invalid_probability { + Some(probabilities) + } else { + None + } + } + + fn expect_args(&mut self, instruction: &Instruction, expected: usize) -> Option> { + let args = &instruction.args; + if args.is_empty() { + self.push_error(Error::MissingArg { instruction: instruction.name.clone(), span: instruction.span, }); return None; } - Some(instruction.targets.chunks(2)) - } - fn expect_readout_noise(&mut self, instruction: &Instruction) -> Option { - if instruction.args.len() > 1 { + if args.len() > expected { self.push_error(Error::TooManyArgs { instruction: instruction.name.clone(), - expected: 1, + expected, + found: args.len(), span: instruction.span, }); return None; - } - if instruction.args.is_empty() { - return Some(0.0); - } - let arg = instruction.args[0]; - if !(0.0..=1.0).contains(&arg) { - self.push_error(Error::InvalidReadoutNoiseProbability { + } else if args.len() < expected { + self.push_error(Error::TooFewArgs { instruction: instruction.name.clone(), - probability: arg, + expected, + found: args.len(), span: instruction.span, }); return None; } - Some(arg) + Some(args.clone()) } fn unsupported(&mut self, instruction: &Instruction) { diff --git a/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs b/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs index ce2e8a430c6..463120820f8 100644 --- a/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/collapsing_gates.rs @@ -166,33 +166,62 @@ fn m_gate_with_invalid_readout_noise_yields_error() { check( "M(1.1) 0", &expect![[r#" - Qdk.Stim.Compiler.InvalidReadoutNoiseProbability + Qdk.Stim.Compiler.InvalidProbability + + x probability for M must be between 0 and 1; found 1.1 + ,---- + 1 | M(1.1) 0 + : ^^^^^^^^ + `---- + "#]], + ); + + check( + "M(-0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability - x readout noise probability for M must be between 0 and 1; found 1.1 + x probability for M must be between 0 and 1; found -0.1 ,---- - 1 | M(1.1) 0 - : ^^^^^^^^ + 1 | M(-0.1) 0 + : ^^^^^^^^^ `---- "#]], ); } #[test] -fn m_gate_with_two_args_yields_error() { +fn m_gate_with_readout_noise_in_radians_yields_error() { check( - "M(0.1, 0.2) 0", + "M(0.1rad) 0", &expect![[r#" - Qdk.Stim.Compiler.TooManyArgs + Qdk.Stim.Compiler.UnexpectedRadians - x too many arguments for instruction M; expected at most 1 + x argument for M cannot be specified in radians ,---- - 1 | M(0.1, 0.2) 0 - : ^^^^^^^^^^^^^ + 1 | M(0.1rad) 0 + : ^^^^^^ `---- "#]], ); } +#[test] +fn m_gate_with_two_args_yields_error() { + check( + "M(0.1, 0.2) 0", + &expect![[r#" + Qdk.Stim.Compiler.TooManyArgs + + x too many arguments for instruction M; expected 1, found 2 + ,---- + 1 | M(0.1, 0.2) 0 + : ^^^^^^^^^^^^^ + `---- + "#]], + ); +} + #[test] fn mr_gate_yields_expected_qir() { let source = "MR 0"; diff --git a/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs b/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs index 5c82fff6bf5..b511daaa35f 100644 --- a/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/generalized_pauli_product_gates.rs @@ -919,6 +919,50 @@ fn mpp_with_readout_noise_yields_expected_qir() { ); } +#[test] +fn mpp_with_invalid_readout_noise_yields_error() { + check( + "MPP(1.1) Z1*Z2", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MPP must be between 0 and 1; found 1.1 + ,---- + 1 | MPP(1.1) Z1*Z2 + : ^^^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "MPP(-0.1) Z1*Z2", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MPP must be between 0 and 1; found -0.1 + ,---- + 1 | MPP(-0.1) Z1*Z2 + : ^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn mpp_with_readout_noise_in_radians_yields_error() { + check( + "MPP(0.01rad) Z1*Z2", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for MPP cannot be specified in radians + ,---- + 1 | MPP(0.01rad) Z1*Z2 + : ^^^^^^^ + `---- + "#]], + ); +} + #[test] fn spp_single_z_yields_expected_qir() { check( diff --git a/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs b/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs index 7fdb3802c09..7609bdd7321 100644 --- a/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs +++ b/source/compiler/stim_compiler/src/qir/tests/noise_channels.rs @@ -115,17 +115,45 @@ fn correlated_error_without_probability_yields_error() { } #[test] -fn correlated_error_with_probability_exceeding_one_yields_error() { +fn correlated_error_with_invalid_probability_yields_error() { let source = "CORRELATED_ERROR(1.5) X0"; check( source, &expect![[r#" - Qdk.Stim.Compiler.NoiseProbabilitiesExceedOne + Qdk.Stim.Compiler.InvalidProbability - x noise probabilities must sum to at most 1.0, but they sum to 1.5 + x probability for CORRELATED_ERROR must be between 0 and 1; found 1.5 + ,---- + 1 | CORRELATED_ERROR(1.5) X0 + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "CORRELATED_ERROR(-0.1) X0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for CORRELATED_ERROR must be between 0 and 1; found -0.1 + ,---- + 1 | CORRELATED_ERROR(-0.1) X0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn correlated_error_with_probability_in_radians_yields_error() { + check( + "CORRELATED_ERROR(0.1rad) X0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for CORRELATED_ERROR cannot be specified in radians ,---- - 1 | CORRELATED_ERROR(1.5) X0 - : ^^^^^^^^^^^^^^^^^^^^^^^^ + 1 | CORRELATED_ERROR(0.1rad) X0 + : ^^^^^^ `---- "#]], ); @@ -177,32 +205,6 @@ fn correlated_error_with_probability_of_exactly_one_is_valid() { ); } -#[test] -fn correlated_error_chain_probability_error_spans_whole_group() { - let source = indoc! {" - TICK - CORRELATED_ERROR(0.5) X0 - ELSE_CORRELATED_ERROR(1.5) Z0 - ELSE_CORRELATED_ERROR(1.5) Z0 - TICK - "}; - check( - source, - &expect![[r#" - Qdk.Stim.Compiler.NegativeNoiseProbability - - x noise probabilities must be non-negative, but found -0.375 - ,-[2:1] - 1 | TICK - 2 | ,-> CORRELATED_ERROR(0.5) X0 - 3 | | ELSE_CORRELATED_ERROR(1.5) Z0 - 4 | `-> ELSE_CORRELATED_ERROR(1.5) Z0 - 5 | TICK - `---- - "#]], - ); -} - #[test] fn else_correlated_error_with_preceding_correlated_error_yields_expected_qir() { let source = indoc! {" @@ -540,17 +542,46 @@ fn depolarize1_without_probability_yields_error() { } #[test] -fn depolarize1_with_probabilities_exceeding_one_yields_error() { +fn depolarize1_with_invalid_probability_yields_error() { let source = "DEPOLARIZE1(1.5) 0"; check( source, &expect![[r#" - Qdk.Stim.Compiler.NoiseProbabilitiesExceedOne + Qdk.Stim.Compiler.InvalidProbability - x noise probabilities must sum to at most 1.0, but they sum to 1.5 + x probability for DEPOLARIZE1 must be between 0 and 1; found 1.5 + ,---- + 1 | DEPOLARIZE1(1.5) 0 + : ^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + + check( + "DEPOLARIZE1(-0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for DEPOLARIZE1 must be between 0 and 1; found -0.1 ,---- - 1 | DEPOLARIZE1(1.5) 0 - : ^^^^^^^^^^^^^^^^^^ + 1 | DEPOLARIZE1(-0.1) 0 + : ^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn depolarize1_with_probability_in_radians_yields_error() { + check( + "DEPOLARIZE1(0.1rad) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for DEPOLARIZE1 cannot be specified in radians + ,---- + 1 | DEPOLARIZE1(0.1rad) 0 + : ^^^^^^ `---- "#]], ); @@ -829,14 +860,14 @@ fn pauli_channel_1_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_1 requires 3 arguments, but found 2 - ,---- - 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_1; expected 3, found 2 + ,---- + 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } @@ -906,22 +937,75 @@ fn pauli_channel_1_with_probabilities_summing_to_exactly_one_is_valid() { } #[test] -fn pauli_channel_1_with_negative_probability_yields_error() { +fn pauli_channel_1_with_invalid_probability_yields_error() { let source = "PAULI_CHANNEL_1(-0.1, 0.2, 0.3) 0"; check( source, &expect![[r#" - Qdk.Stim.Compiler.NegativeNoiseProbability + Qdk.Stim.Compiler.InvalidProbability + + x probability for PAULI_CHANNEL_1 must be between 0 and 1; found -0.1 + ,---- + 1 | PAULI_CHANNEL_1(-0.1, 0.2, 0.3) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + + check( + "PAULI_CHANNEL_1(1.5, 0.0, 0.0) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for PAULI_CHANNEL_1 must be between 0 and 1; found 1.5 + ,---- + 1 | PAULI_CHANNEL_1(1.5, 0.0, 0.0) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn pauli_channel_1_with_probability_in_radians_yields_error() { + check( + "PAULI_CHANNEL_1(0.1rad, 0.2, 0.3) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians - x noise probabilities must be non-negative, but found -0.1 + x argument for PAULI_CHANNEL_1 cannot be specified in radians ,---- - 1 | PAULI_CHANNEL_1(-0.1, 0.2, 0.3) 0 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 1 | PAULI_CHANNEL_1(0.1rad, 0.2, 0.3) 0 + : ^^^^^^ `---- "#]], ); } +#[test] +fn pauli_channel_1_with_multiple_probabilities_in_radians_yields_errors() { + check( + "PAULI_CHANNEL_1(0.1rad, 0.2rad, 0.3) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PAULI_CHANNEL_1 cannot be specified in radians + ,---- + 1 | PAULI_CHANNEL_1(0.1rad, 0.2rad, 0.3) 0 + : ^^^^^^ + `---- + + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PAULI_CHANNEL_1 cannot be specified in radians + ,---- + 1 | PAULI_CHANNEL_1(0.1rad, 0.2rad, 0.3) 0 + : ^^^^^^ + `---- + "#]], + ); +} + #[test] fn pauli_channel_2_yields_expected_qir() { let source = "PAULI_CHANNEL_2(0,0,0, 0,0.1,0,0, 0,0,0,0.2, 0,0,0,0) 0 1"; @@ -1005,14 +1089,14 @@ fn pauli_channel_2_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_2 requires 15 arguments, but found 1 - ,---- - 1 | PAULI_CHANNEL_2(0.1) 0 1 - : ^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_2; expected 15, found 1 + ,---- + 1 | PAULI_CHANNEL_2(0.1) 0 1 + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } @@ -1085,14 +1169,14 @@ fn x_error_with_probability_exceeding_one_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.NoiseProbabilitiesExceedOne + Qdk.Stim.Compiler.InvalidProbability - x noise probabilities must sum to at most 1.0, but they sum to 1.5 - ,---- - 1 | X_ERROR(1.5) 0 - : ^^^^^^^^^^^^^^ - `---- - "#]], + x probability for X_ERROR must be between 0 and 1; found 1.5 + ,---- + 1 | X_ERROR(1.5) 0 + : ^^^^^^^^^^^^^^ + `---- + "#]], ); } diff --git a/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs b/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs index ddc5309dce7..7496fb76f6e 100644 --- a/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs +++ b/source/compiler/stim_compiler/src/qir/tests/noise_channels_broadcasting.rs @@ -346,14 +346,14 @@ fn pauli_channel_1_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_1 requires 3 arguments, but found 2 - ,---- - 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 1 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_1; expected 3, found 2 + ,---- + 1 | PAULI_CHANNEL_1(0.1, 0.2) 0 1 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } @@ -441,14 +441,14 @@ fn pauli_channel_2_with_wrong_number_of_args_yields_error() { check( source, &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.TooFewArgs - x instruction PAULI_CHANNEL_2 requires 15 arguments, but found 1 - ,---- - 1 | PAULI_CHANNEL_2(0.1) 0 1 2 3 - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `---- - "#]], + x too few arguments for instruction PAULI_CHANNEL_2; expected 15, found 1 + ,---- + 1 | PAULI_CHANNEL_2(0.1) 0 1 2 3 + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], ); } diff --git a/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs b/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs index f9674edf220..ac2a9e7f866 100644 --- a/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs +++ b/source/compiler/stim_compiler/src/qir/tests/non_clifford_gates.rs @@ -147,20 +147,21 @@ fn t_gate_with_pauli_target_yields_error() { } #[test] -fn r_x_yields_expected_qir() { +fn tpp_single_z_yields_expected_qir() { + // same as T 0 check( - "R_X(0.25) 0", + "TPP Z0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__rx__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__qis__rx__body(double, ptr) declare void @__quantum__rt__result_record_output(ptr, ptr) declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) declare void @__quantum__rt__initialize(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } @@ -183,21 +184,24 @@ fn r_x_yields_expected_qir() { } #[test] -fn r_y_with_negative_angle_yields_expected_qir() { +fn tpp_single_x_yields_expected_qir() { check( - "R_Y(-0.25) 0", + "TPP X0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__ry__body(double -0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__qis__ry__body(double, ptr) declare void @__quantum__rt__result_record_output(ptr, ptr) declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } attributes #1 = { "irreversible" } @@ -219,21 +223,28 @@ fn r_y_with_negative_angle_yields_expected_qir() { } #[test] -fn r_z_with_large_angle_yields_expected_qir() { +fn tpp_single_y_yields_expected_qir() { check( - "R_Z(123.432) 0", + "TPP Y0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__rz__body(double 387.77306441789534, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__s__body(ptr) declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__t__body(ptr) declare void @__quantum__rt__initialize(ptr) - declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__qis__h__body(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } attributes #1 = { "irreversible" } @@ -255,23 +266,72 @@ fn r_z_with_large_angle_yields_expected_qir() { } #[test] -fn r_x_broadcasts_over_targets() { +fn tpp_dag_single_z_yields_expected_qir() { + // same as T_DAG 0 check( - "R_X(0.125) 0 1 2", + "TPP_DAG Z0", &expect![[r#" define i64 @ENTRYPOINT__main() #0 { call void @__quantum__rt__initialize(ptr null) - call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 0 to ptr)) - call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 1 to ptr)) - call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 2 to ptr)) + call void @__quantum__qis__t__adj(ptr inttoptr (i64 0 to ptr)) call void @__quantum__rt__array_record_output(i64 0, ptr null) ret i64 0 } - declare void @__quantum__qis__rx__body(double, ptr) declare void @__quantum__rt__result_record_output(ptr, ptr) declare void @__quantum__rt__array_record_output(i64, ptr) declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__t__adj(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_three_factor_product_yields_expected_qir() { + check( + "TPP X0*Y1*Z2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__s__body(ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__h__body(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__t__body(ptr) + declare void @__quantum__rt__initialize(ptr) attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } attributes #1 = { "irreversible" } @@ -293,69 +353,1364 @@ fn r_x_broadcasts_over_targets() { } #[test] -fn r_x_without_argument_yields_error() { +fn tpp_negated_product_applies_inverse() { check( - "R_X 0", + "TPP !Z0", &expect![[r#" - Qdk.Stim.Compiler.MissingArg + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__t__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } - x missing argument in instruction: R_X + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__t__adj(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_dag_negated_product_applies_inverse() { + check( + "TPP_DAG !Z0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_negation_on_later_factor_negates_whole_product() { + check( + "TPP X0*!Z1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__qis__t__adj(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_double_negation_cancels() { + check( + "TPP !X0*!Z1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__t__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__t__body(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_identity_products_are_noops() { + let source = indoc! {" + TPP X0*X0 !Y1*Y1 + TPP_DAG Z2*Z2 !X3*X3 + "}; + check( + source, + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn tpp_anti_hermitian_product_yields_error() { + check( + "TPP X0*Y0", + &expect![[r#" + Qdk.Stim.Compiler.AntiHermitianPauliProduct + + x Pauli product must be Hermitian ,---- - 1 | R_X 0 - : ^^^^^ + 1 | TPP X0*Y0 + : ^^^^^ `---- "#]], ); } #[test] -fn r_x_with_two_arguments_yields_error() { +fn tpp_with_argument_yields_error() { check( - "R_X(0.25, 0.5) 0", + "TPP(0.5) Z0", &expect![[r#" - Qdk.Stim.Compiler.WrongArgCount + Qdk.Stim.Compiler.UnsupportedArgument - x instruction R_X requires 1 arguments, but found 2 + x unsupported argument in instruction: TPP ,---- - 1 | R_X(0.25, 0.5) 0 - : ^^^^^^^^^^^^^^^^ + 1 | TPP(0.5) Z0 + : ^^^^^^^^^^^ `---- "#]], ); } #[test] -fn r_x_with_negated_target_yields_error() { +fn tpp_with_qubit_target_yields_error() { check( - "R_X(0.25) !0", + "TPP 0", &expect![[r#" - Qdk.Stim.Compiler.NegatedTarget + Qdk.Stim.Compiler.UnsupportedTarget - x target cannot be negated in instruction: R_X + x unsupported target in instruction: TPP ,---- - 1 | R_X(0.25) !0 - : ^^ + 1 | TPP 0 + : ^ `---- "#]], ); } #[test] -fn r_x_with_angle_that_overflows_radians_yields_error() { +fn ch_gate_yields_expected_qir() { check( - "R_X(1e308) 0", + "CH 0 1", &expect![[r#" - Qdk.Stim.Compiler.InvalidAngle + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ry__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double -0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccz_gate_yields_expected_qir() { + check( + "CCZ 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ccx__body(ptr, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccx_gate_yields_expected_qir() { + check( + "CCX 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 2 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ccx__body(ptr, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccz_gate_broadcasts_over_triples() { + check( + "CCZ 0 1 2 3 4 5", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 3 to ptr)) + call void @__quantum__qis__ccx__body(ptr inttoptr (i64 4 to ptr), ptr inttoptr (i64 5 to ptr), ptr inttoptr (i64 3 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 3 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ccx__body(ptr, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="6" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn ccx_gate_with_one_target_yields_error() { + check( + "CCX 0", + &expect![[r#" + Qdk.Stim.Compiler.TargetCountNotMultipleOfThree + + x instruction CCX requires a multiple of three targets + ,---- + 1 | CCX 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_two_targets_yields_error() { + check( + "CCX 0 1", + &expect![[r#" + Qdk.Stim.Compiler.TargetCountNotMultipleOfThree + + x instruction CCX requires a multiple of three targets + ,---- + 1 | CCX 0 1 + : ^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_four_targets_yields_error() { + check( + "CCX 0 1 2 3", + &expect![[r#" + Qdk.Stim.Compiler.TargetCountNotMultipleOfThree + + x instruction CCX requires a multiple of three targets + ,---- + 1 | CCX 0 1 2 3 + : ^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_argument_yields_error() { + check( + "CCX(0.5) 0 1 2", + &expect![[r#" + Qdk.Stim.Compiler.UnsupportedArgument + + x unsupported argument in instruction: CCX + ,---- + 1 | CCX(0.5) 0 1 2 + : ^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn ccx_gate_with_negated_target_yields_error() { + check( + "CCX 0 1 !2", + &expect![[r#" + Qdk.Stim.Compiler.NegatedTarget + + x target cannot be negated in instruction: CCX + ,---- + 1 | CCX 0 1 !2 + : ^^ + `---- + "#]], + ); +} + +#[test] +fn ccz_gate_with_measurement_record_target_yields_error() { + let source = indoc! {" + M 0 + CCZ rec[-1] 1 2 + "}; + check( + source, + &expect![[r#" + Qdk.Stim.Compiler.UnsupportedTarget + + x unsupported target in instruction: CCZ + ,-[2:5] + 1 | M 0 + 2 | CCZ rec[-1] 1 2 + : ^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_x_yields_expected_qir() { + check( + "R_X(0.25) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rx__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rx__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_x_with_angle_in_radians_yields_expected_qir() { + check( + "R_X(1rad) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rx__body(double 1.0, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rx__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_y_yields_expected_qir() { + check( + "R_Y(-0.375) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ry__body(double -1.1780972450961724, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_z_yields_expected_qir() { + check( + "R_Z(123.432) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 387.77306441789534, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_x_broadcasts_over_targets() { + check( + "R_X(0.125) 0 1 2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__rx__body(double 0.39269908169872414, ptr inttoptr (i64 2 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rx__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_x_without_argument_yields_error() { + check( + "R_X 0", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: R_X + ,---- + 1 | R_X 0 + : ^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_x_with_two_arguments_yields_error() { + check( + "R_X(0.25, 0.5) 0", + &expect![[r#" + Qdk.Stim.Compiler.TooManyArgs + + x too many arguments for instruction R_X; expected 1, found 2 + ,---- + 1 | R_X(0.25, 0.5) 0 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_x_with_negated_target_yields_error() { + check( + "R_X(0.25) !0", + &expect![[r#" + Qdk.Stim.Compiler.NegatedTarget + + x target cannot be negated in instruction: R_X + ,---- + 1 | R_X(0.25) !0 + : ^^ + `---- + "#]], + ); +} + +#[test] +fn u3_yields_expected_qir() { + check( + "U3(0.1, 0.2, 0.3) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.9424777960769379, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double 0.3141592653589793, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.6283185307179586, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn u_alias_yields_expected_qir() { + check( + "U(0.1, 0.2, 0.3) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.9424777960769379, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double 0.3141592653589793, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.6283185307179586, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn u3_with_mixed_angle_units_yields_expected_qir() { + check( + "U3(0.1, -0.2rad, 3e-1rad) 0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.3, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__ry__body(double 0.3141592653589793, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double -0.2, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__ry__body(double, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn u3_without_arguments_yields_error() { + check( + "U3 0", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: U3 + ,---- + 1 | U3 0 + : ^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_one_argument_yields_error() { + check( + "U3(0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.TooFewArgs + + x too few arguments for instruction U3; expected 3, found 1 + ,---- + 1 | U3(0.1) 0 + : ^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_two_arguments_yields_error() { + check( + "U3(0.1, 0.2) 0", + &expect![[r#" + Qdk.Stim.Compiler.TooFewArgs + + x too few arguments for instruction U3; expected 3, found 2 + ,---- + 1 | U3(0.1, 0.2) 0 + : ^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_four_arguments_yields_error() { + check( + "U3(0.1, 0.2, 0.3, 0.4) 0", + &expect![[r#" + Qdk.Stim.Compiler.TooManyArgs + + x too many arguments for instruction U3; expected 3, found 4 + ,---- + 1 | U3(0.1, 0.2, 0.3, 0.4) 0 + : ^^^^^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn u3_with_multiple_angles_that_overflow_radians_yields_errors() { + check( + "U3(1e308, 0.25, -1e308) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidAngle + + x angle for U3 must be finite and representable in radians + ,---- + 1 | U3(1e308, 0.25, -1e308) 0 + : ^^^^^ + `---- + + Qdk.Stim.Compiler.InvalidAngle + + x angle for U3 must be finite and representable in radians + ,---- + 1 | U3(1e308, 0.25, -1e308) 0 + : ^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_xx_yields_expected_qir() { + check( + "R_XX(0.25) 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rxx__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rxx__body(double, ptr, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_yy_yields_expected_qir() { + check( + "R_YY(-0.6) 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__ryy__body(double -1.8849555921538759, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__ryy__body(double, ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_zz_yields_expected_qir() { + check( + "R_ZZ(0.25) 0 1", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rzz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rzz__body(double, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_zz_broadcasts_over_pairs() { + check( + "R_ZZ(0.25) 0 1 2 3", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rzz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__rzz__body(double 0.7853981633974483, ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 3 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rzz__body(double, ptr, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="4" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_xx_with_odd_target_count_yields_error() { + check( + "R_XX(0.25) 0 1 2", + &expect![[r#" + Qdk.Stim.Compiler.OddTargetCount + + x instruction R_XX requires an even number of targets + ,---- + 1 | R_XX(0.25) 0 1 2 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_xx_without_argument_yields_error() { + check( + "R_XX 0 1", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: R_XX + ,---- + 1 | R_XX 0 1 + : ^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_xx_with_two_arguments_yields_error() { + check( + "R_XX(0.25, 0.5) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.TooManyArgs + + x too many arguments for instruction R_XX; expected 1, found 2 + ,---- + 1 | R_XX(0.25, 0.5) 0 1 + : ^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_pauli_single_z_yields_expected_qir() { + // same as R_Z(0.25) 0 + check( + "R_PAULI(0.25) Z0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_single_x_yields_expected_qir() { + check( + "R_PAULI(0.25) X0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__h__body(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_single_y_yields_expected_qir() { + check( + "R_PAULI(0.25) Y0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__s__body(ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__h__body(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_mixed_basis_product_yields_expected_qir() { + check( + "R_PAULI(0.25) X0*Y1*Z2", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__s__adj(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__rz__body(double 0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 2 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__cx__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 0 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__s__body(ptr inttoptr (i64 1 to ptr)) + call void @__quantum__qis__h__body(ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__qis__cx__body(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__qis__s__adj(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + declare void @__quantum__qis__s__body(ptr) + declare void @__quantum__qis__h__body(ptr) + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__initialize(ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="3" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_negated_product_negates_angle() { + check( + "R_PAULI(0.25) !Z0", + &expect![[r#" + define i64 @ENTRYPOINT__main() #0 { + call void @__quantum__rt__initialize(ptr null) + call void @__quantum__qis__rz__body(double -0.7853981633974483, ptr inttoptr (i64 0 to ptr)) + call void @__quantum__rt__array_record_output(i64 0, ptr null) + ret i64 0 + } + + declare void @__quantum__rt__result_record_output(ptr, ptr) + declare void @__quantum__rt__array_record_output(i64, ptr) + declare void @__quantum__rt__initialize(ptr) + declare void @__quantum__qis__rz__body(double, ptr) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7} + + !0 = !{i32 1, !"qir_major_version", i32 2} + !1 = !{i32 7, !"qir_minor_version", i32 1} + !2 = !{i32 1, !"dynamic_qubit_management", i1 false} + !3 = !{i32 1, !"dynamic_result_management", i1 false} + !4 = !{i32 5, !"int_computations", !{!"i64"}} + !5 = !{i32 5, !"float_computations", !{!"double"}} + !6 = !{i32 7, !"backwards_branching", i2 3} + !7 = !{i32 1, !"arrays", i1 true} + "#]], + ); +} + +#[test] +fn r_pauli_without_argument_yields_error() { + check( + "R_PAULI X0", + &expect![[r#" + Qdk.Stim.Compiler.MissingArg + + x missing argument in instruction: R_PAULI + ,---- + 1 | R_PAULI X0 + : ^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn r_pauli_with_two_arguments_yields_error() { + check( + "R_PAULI(0.25, 0.5) X0", + &expect![[r#" + Qdk.Stim.Compiler.TooManyArgs - x angle for R_X must be finite and representable in radians; found - | 10000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 00000000000000000000000000000000000000000000000000000000000000000000000000 - | 0000000000000 half turns + x too many arguments for instruction R_PAULI; expected 1, found 2 ,---- - 1 | R_X(1e308) 0 - : ^^^^^^^^^^^^ + 1 | R_PAULI(0.25, 0.5) X0 + : ^^^^^^^^^^^^^^^^^^^^^ `---- "#]], ); diff --git a/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs b/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs index 0faf661d0de..a4ccc5d63ce 100644 --- a/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs +++ b/source/compiler/stim_compiler/src/qir/tests/pair_measurements.rs @@ -142,6 +142,50 @@ fn mxx_with_readout_noise_yields_correct_qir() { ); } +#[test] +fn mxx_with_invalid_readout_noise_yields_error() { + check( + "MXX(1.1) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MXX must be between 0 and 1; found 1.1 + ,---- + 1 | MXX(1.1) 0 1 + : ^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "MXX(-0.1) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for MXX must be between 0 and 1; found -0.1 + ,---- + 1 | MXX(-0.1) 0 1 + : ^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn mxx_with_readout_noise_in_radians_yields_error() { + check( + "MXX(0.1rad) 0 1", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for MXX cannot be specified in radians + ,---- + 1 | MXX(0.1rad) 0 1 + : ^^^^^^ + `---- + "#]], + ); +} + #[test] fn myy_measurement_yields_correct_qir() { let source = "MYY 0 1"; diff --git a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs index 2c36a214d84..bbc1b6f598c 100644 --- a/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs +++ b/source/compiler/stim_compiler/src/qir/tests/peek_loss.rs @@ -124,6 +124,74 @@ fn peek_loss_with_readout_noise_yields_expected_qir() { ); } +#[test] +fn peek_loss_with_invalid_readout_noise_yields_error() { + check( + "PEEK_LOSS(1.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for PEEK_LOSS must be between 0 and 1; found 1.1 + ,---- + 1 | PEEK_LOSS(1.1) 0 + : ^^^^^^^^^^^^^^^^ + `---- + "#]], + ); + check( + "PEEK_LOSS(-0.1) 0", + &expect![[r#" + Qdk.Stim.Compiler.InvalidProbability + + x probability for PEEK_LOSS must be between 0 and 1; found -0.1 + ,---- + 1 | PEEK_LOSS(-0.1) 0 + : ^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn peek_loss_with_readout_noise_in_radians_yields_error() { + check( + "PEEK_LOSS(0.1rad) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PEEK_LOSS cannot be specified in radians + ,---- + 1 | PEEK_LOSS(0.1rad) 0 + : ^^^^^^ + `---- + "#]], + ); +} + +#[test] +fn peek_loss_with_negative_readout_noise_in_radians_yields_errors() { + check( + "PEEK_LOSS(-0.1rad) 0", + &expect![[r#" + Qdk.Stim.Compiler.UnexpectedRadians + + x argument for PEEK_LOSS cannot be specified in radians + ,---- + 1 | PEEK_LOSS(-0.1rad) 0 + : ^^^^^^^ + `---- + + Qdk.Stim.Compiler.InvalidProbability + + x probability for PEEK_LOSS must be between 0 and 1; found -0.1 + ,---- + 1 | PEEK_LOSS(-0.1rad) 0 + : ^^^^^^^^^^^^^^^^^^^^ + `---- + "#]], + ); +} + #[test] fn peek_loss_with_negated_target_yields_error() { check( diff --git a/source/qdk_package/qdk/simulation/_simulation.py b/source/qdk_package/qdk/simulation/_simulation.py index bfc92a23aec..88f1637d252 100644 --- a/source/qdk_package/qdk/simulation/_simulation.py +++ b/source/qdk_package/qdk/simulation/_simulation.py @@ -641,6 +641,7 @@ def run_qir_clifford( seed: Optional[int] = None, ) -> List: mod, shots, noise, seed = preprocess_simulation_input(input, shots, noise, seed) + DecomposeCcxPass().run(mod) if is_adaptive(mod): program = AdaptiveProfilePass(Bytecode.Bit64).run(mod, noise) return run_adaptive(run_clifford_adaptive, mod, program, shots, noise, seed) diff --git a/source/vscode/syntaxes/stim.tmLanguage.json b/source/vscode/syntaxes/stim.tmLanguage.json index b1850a80786..3e87e0b6e06 100644 --- a/source/vscode/syntaxes/stim.tmLanguage.json +++ b/source/vscode/syntaxes/stim.tmLanguage.json @@ -68,7 +68,7 @@ "name": "keyword.other.measurement.stim" }, "gate": { - "match": "\\b(C_NXYZ|C_NZYX|C_XNYZ|C_XYNZ|C_XYZ|C_ZNYX|C_ZYNX|C_ZYX|CXSWAP|CX|CNOT|ZCX|CY|ZCY|CZSWAP|CZ|ZCZ|SWAPCZ|SWAPCX|SWAP|H_XZ|H_NXY|H_NXZ|H_NYZ|H_XY|H_YZ|H|SQRT_X_DAG|SQRT_X|SQRT_Y_DAG|SQRT_Y|SQRT_Z_DAG|SQRT_Z|SQRT_XX_DAG|SQRT_XX|SQRT_YY_DAG|SQRT_YY|SQRT_ZZ_DAG|SQRT_ZZ|S_DAG|S|ISWAP_DAG|ISWAP|II|XCX|XCY|XCZ|YCX|YCY|YCZ|MPP|SPP_DAG|SPP|I|X|Y|Z)\\b", + "match": "\\b(C_NXYZ|C_NZYX|C_XNYZ|C_XYNZ|C_XYZ|C_ZNYX|C_ZYNX|C_ZYX|CXSWAP|CX|CNOT|ZCX|CY|ZCY|CZSWAP|CZ|ZCZ|SWAPCZ|SWAPCX|SWAP|H_XZ|H_NXY|H_NXZ|H_NYZ|H_XY|H_YZ|H|SQRT_X_DAG|SQRT_X|SQRT_Y_DAG|SQRT_Y|SQRT_Z_DAG|SQRT_Z|SQRT_XX_DAG|SQRT_XX|SQRT_YY_DAG|SQRT_YY|SQRT_ZZ_DAG|SQRT_ZZ|S_DAG|S|ISWAP_DAG|ISWAP|II|XCX|XCY|XCZ|YCX|YCY|YCZ|MPP|SPP_DAG|SPP|T_DAG|T|TPP_DAG|TPP|CH|CCX|CCZ|R_X|R_Y|R_Z|U3|U|R_XX|R_YY|R_ZZ|R_PAULI|I|X|Y|Z)\\b", "name": "keyword.other.gate.stim" }, "tag": { @@ -129,7 +129,7 @@ "name": "keyword.operator.combiner.stim" }, "number": { - "match": "[+-]?\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?\\b", + "match": "[+-]?\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?(rad)?\\b", "name": "constant.numeric.stim" }, "bracket": {