Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9352266
Add support for correlated loss
rafaelha Oct 6, 2025
6a07247
Add tests for correlated qubit loss in noise simulations
rafaelha Oct 7, 2025
2c90cac
Refactor CorrelatedQubitLoss to inherit from NoiseChannel instead of …
rafaelha Oct 7, 2025
cd69a37
Run precommit
rafaelha Oct 7, 2025
d8c5318
Merge branch 'main' of https://github.com/QuEraComputing/bloqade-circ…
rafaelha Oct 7, 2025
646a478
Add filterwarnings for cirq FutureWarning in pyproject.toml
rafaelha Oct 7, 2025
2bba610
Make type of squin.noise.CorrelatedQubitLoss.qubits list[list[Qubit]]…
rafaelha Oct 10, 2025
bcb3e76
Adjust typing to ensure correlated errors are only broadcast over rec…
rafaelha Oct 10, 2025
4e71bee
Merge branch 'main' into bloqade-qec-58/correlated-loss-support
kaihsin Oct 14, 2025
51d7c3f
Merge branch 'main' of https://github.com/QuEraComputing/bloqade-circ…
rafaelha Oct 14, 2025
fdafe6d
Add CorrelatedQubitNoise to SquinNoiseToStim rewrite
rafaelha Oct 14, 2025
4c3f852
Add unit tests
rafaelha Oct 14, 2025
86a499f
Remove MultiQubitNoiseChannel
rafaelha Oct 15, 2025
23fb34d
Handle None values of insert_qubit_idx_from_address
rafaelha Oct 15, 2025
03a3358
Remove dynamic import
rafaelha Oct 15, 2025
ca3c198
Merge branch 'main' of https://github.com/QuEraComputing/bloqade-circ…
rafaelha Oct 15, 2025
d130835
Make NonStimCorrelatedError.nonce=0 instead of random value
rafaelha Oct 15, 2025
20fe041
Revert "Make NonStimCorrelatedError.nonce=0 instead of random value"
rafaelha Oct 15, 2025
73ea0b4
Merge branch 'main' of https://github.com/QuEraComputing/bloqade-circ…
rafaelha Oct 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,6 @@ include = ["src/bloqade/*"]

[tool.pytest.ini_options]
testpaths = "test/"
filterwarnings = [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The python 3.10 build is producing a lot of warnings (this currently also happens on main)

test/cirq_utils/test_parallelize.py: 9170 warnings
test/qasm2/test_native.py: 1873 warnings
  /Users/rafaelhaenel/Documents/quera/kirin-workspace/.venv/lib/python3.10/site-packages/cirq/circuits/circuit_operation.py:173: FutureWarning: In cirq 1.6 the default value of `use_repetition_ids` will change to
  `use_repetition_ids=False`. To make this warning go away, please pass
  explicit `use_repetition_ids`, e.g., to preserve current behavior, use
  
    CircuitOperations(..., use_repetition_ids=True)
    warnings.warn(msg, FutureWarning)

These warnings are coming internally from cirq 1.5.0. The solution is to simply upgrade to cirq 1.6 which requires Python 3.11 (which is why only the 3.10 build has these issues).

I've gone ahead and filtered these warnings.

"ignore:In cirq 1.6 the default value of `use_repetition_ids`:FutureWarning:cirq.circuits.circuit_operation",
]
2 changes: 1 addition & 1 deletion src/bloqade/pyqrack/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class QuantumState(NamedTuple):
A representation of a quantum state as a density matrix, where the density matrix is
rho = sum_i eigenvalues[i] |eigenvectors[:,i]><eigenvectors[:,i]|.

This reprsentation is efficient for low-rank density matrices by only storing
This representation is efficient for low-rank density matrices by only storing
the non-zero eigenvalues and corresponding eigenvectors of the density matrix.
For example, a pure state has only one non-zero eigenvalue equal to 1.0.

Expand Down
11 changes: 11 additions & 0 deletions src/bloqade/pyqrack/squin/noise/native.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
QubitLoss,
Depolarize,
Depolarize2,
CorrelatedQubitLoss,
TwoQubitPauliChannel,
SingleQubitPauliChannel,
)
Expand Down Expand Up @@ -88,6 +89,16 @@ def qubit_loss(
if interp.rng_state.uniform(0.0, 1.0) <= p:
qbit.drop()

@interp.impl(CorrelatedQubitLoss)
def correlated_qubit_loss(
self, interp: PyQrackInterpreter, frame: interp.Frame, stmt: CorrelatedQubitLoss
):
p = frame.get(stmt.p)
qubits: list[PyQrackQubit] = frame.get(stmt.qubits)
if interp.rng_state.uniform(0.0, 1.0) <= p:
for qbit in qubits:
qbit.drop()

def apply_single_qubit_pauli_error(
self,
interp: PyQrackInterpreter,
Expand Down
1 change: 1 addition & 0 deletions src/bloqade/squin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
sqrt_y_adj as sqrt_y_adj,
sqrt_z_adj as sqrt_z_adj,
depolarize2 as depolarize2,
correlated_qubit_loss as correlated_qubit_loss,
two_qubit_pauli_channel as two_qubit_pauli_channel,
single_qubit_pauli_channel as single_qubit_pauli_channel,
)
Expand Down
4 changes: 4 additions & 0 deletions src/bloqade/squin/noise/_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ def two_qubit_pauli_channel(

@wraps(stmts.QubitLoss)
def qubit_loss(p: float, qubits: ilist.IList[Qubit, Any]) -> None: ...


@wraps(stmts.CorrelatedQubitLoss)
def correlated_qubit_loss(p: float, qubits: ilist.IList[Qubit, Any]) -> None: ...
10 changes: 10 additions & 0 deletions src/bloqade/squin/noise/stmts.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,13 @@ class QubitLoss(SingleQubitNoiseChannel):
# NOTE: qubit loss error (not supported by Stim)
p: ir.SSAValue = info.argument(types.Float)
qubits: ir.SSAValue = info.argument(ilist.IListType)


@statement(dialect=dialect)
class CorrelatedQubitLoss(NoiseChannel):
"""
Apply a correlated atom loss channel.
"""

p: ir.SSAValue = info.argument(types.Float)
qubits: ir.SSAValue = info.argument(ilist.IListType)
1 change: 1 addition & 0 deletions src/bloqade/squin/stdlib/broadcast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
depolarize as depolarize,
qubit_loss as qubit_loss,
depolarize2 as depolarize2,
correlated_qubit_loss as correlated_qubit_loss,
two_qubit_pauli_channel as two_qubit_pauli_channel,
single_qubit_pauli_channel as single_qubit_pauli_channel,
)
14 changes: 14 additions & 0 deletions src/bloqade/squin/stdlib/broadcast/noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ def qubit_loss(p: float, qubits: ilist.IList[Qubit, Any]) -> None:
noise.qubit_loss(p, qubits)


@kernel
def correlated_qubit_loss(p: float, qubits: ilist.IList[Qubit, Any]) -> None:
"""
Apply a correlated qubit loss channel to the given qubits.

All qubits are lost together with a probability `p`.

Args:
p (float): Probability of the qubits being lost.
qubits (IList[Qubit, Any]): The list of qubits to which the correlated noise channel is applied.
"""
noise.correlated_qubit_loss(p, qubits)


# NOTE: actual stdlib that doesn't wrap statements starts here


Expand Down
1 change: 1 addition & 0 deletions src/bloqade/squin/stdlib/simple/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
depolarize as depolarize,
qubit_loss as qubit_loss,
depolarize2 as depolarize2,
correlated_qubit_loss as correlated_qubit_loss,
two_qubit_pauli_channel as two_qubit_pauli_channel,
single_qubit_pauli_channel as single_qubit_pauli_channel,
)
16 changes: 15 additions & 1 deletion src/bloqade/squin/stdlib/simple/noise.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, TypeVar
from typing import Any, Literal, TypeVar

from kirin.dialects import ilist

Expand Down Expand Up @@ -97,6 +97,20 @@ def qubit_loss(p: float, qubit: Qubit) -> None:
broadcast.qubit_loss(p, ilist.IList([qubit]))


@kernel
def correlated_qubit_loss(p: float, qubits: ilist.IList[Qubit, Any]) -> None:
"""
Apply a correlated qubit loss channel to the given qubits.

All qubits are lost together with a probability `p`.

Args:
p (float): Probability of the qubits being lost.
qubits (IList[Qubit, Any]): The list of qubits to which the correlated noise channel is applied.
"""
broadcast.correlated_qubit_loss(p, qubits)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat hacky. correlated_qubit_loss is an n-qubit operator. As far as I know all other operators act on either one or two qubits.

Stim does not support n-qubit operators, which is why the nonce workaround is introduced.

I'm wondering if broadcast should be disallowed for this type of operator? Instead only use apply? I am a bit confused how that works after the refactor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mismatch in semantics is a concern. There are a couple stim gates that act on more qubits: MPP, SPP, and SPP_DAG

# Perform a SQRT_YY gate between qubit 1 and 2, and a SQRT_ZZ_DAG between qubit 3 and 4.
SPP Y1*Y2 !Z1*Z2

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the requirements here? Do we really need to support correlated loss for more than two qubits at a time? If so, it's probably best to make this statement apply to an IList[IList[Qubit]] or similar. All statements in the gate dialect should "broadcast", meaning that it applies to each entry in the list of arguments.

@cduck are you saying we need to support statements for these gates? Or are you just pointing out that these would be points where this noise process can be injected and so we have more than two qubits here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the example @cduck sketched out for me I think it should be an arbitrary number of atoms.

From Casey himself:

What I mean is
I_ERROR[CORRELATED_LOSS:<nonce>](p) 1 2 3
Is there is a chance p that a single event that looses all atoms 1, 2, and 3 occurs.

Briefly chatted with @david-pl , IList[IList[Qubit]] makes sense here.

In the apply case you just accept a single list of qubits (if one atom in the list goes, all the other ones get lost too)

In the broadcast case you accept list of lists, if any atom in one of those sublists goes, then the other atoms should go too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes in the case of SPP is why we have discussion whether we need PauliString Type or something to express a pauli-string. iirc the conclusion is to just use string "XYXXYZ" to represent SPP in stim dialect, and no support for it in squin?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to have an equivalent of the SPP command in kirin-stim at all. This is a really niche gate that we don't need. I was just using that as an example of a more complex stim instruction than I_ERROR.


@cduck are you saying we need to support statements for these gates? Or are you just pointing out that these would be points where this noise process can be injected and so we have more than two qubits here?

@david-pl, I'm throwing out ideas for stim gates that can be repurposed to represent correlated atom loss. SPP isn't a great option but shows how stim can group qubits.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cduck ok so thats align with what we discuss earlier with @ChenZhao44 that we don't really have need of SPP. For the tag. If thats the behavior of stim, then, is the quest here that you want to also preserve the command order in stim circuit python instance (i.e. don't want stim to auto merge? so we need separate tags?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

broadcast.correlated_qubit_noise has now been updated to accept qubits: list[list[Qubit]]

image



# NOTE: actual stdlib that doesn't wrap statements starts here


Expand Down
1 change: 1 addition & 0 deletions src/bloqade/stim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@
pauli_channel1 as pauli_channel1,
pauli_channel2 as pauli_channel2,
observable_include as observable_include,
correlated_qubit_loss as correlated_qubit_loss,
)
6 changes: 6 additions & 0 deletions src/bloqade/stim/_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,9 @@ def z_error(p: float, targets: tuple[int, ...]) -> None: ...

@wraps(noise.QubitLoss)
def qubit_loss(probs: tuple[float, ...], targets: tuple[int, ...]) -> None: ...


@wraps(noise.CorrelatedQubitLoss)
def correlated_qubit_loss(
probs: tuple[float, ...], targets: tuple[int, ...]
) -> None: ...
1 change: 1 addition & 0 deletions src/bloqade/stim/dialects/noise/emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def non_stim_error(
return ()

@impl(stmts.TrivialCorrelatedError)
@impl(stmts.CorrelatedQubitLoss)
def non_stim_corr_error(
self,
emit: EmitStimMain,
Expand Down
9 changes: 7 additions & 2 deletions src/bloqade/stim/dialects/noise/stmts.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ class NonStimError(ir.Statement):
class NonStimCorrelatedError(ir.Statement):
name = "NonStimCorrelatedError"
traits = frozenset({lowering.FromPythonCall()})
nonce: int = (
info.attribute()
nonce: int = info.attribute(
default_factory=lambda: __import__("random").getrandbits(32)
) # Must be a unique value, otherwise stim might merge two correlated errors with equal probabilities
probs: tuple[ir.SSAValue, ...] = info.argument(types.Float)
targets: tuple[ir.SSAValue, ...] = info.argument(types.Int)
Expand All @@ -109,3 +109,8 @@ class TrivialError(NonStimError):
@statement(dialect=dialect)
class QubitLoss(NonStimError):
name = "loss"


@statement(dialect=dialect)
class CorrelatedQubitLoss(NonStimCorrelatedError):
name = "correlated_loss"
4 changes: 4 additions & 0 deletions src/bloqade/stim/rewrite/qubit_to_stim.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
SQUIN_STIM_OP_MAPPING,
rewrite_Control,
rewrite_QubitLoss,
rewrite_CorrelatedQubitLoss,
insert_qubit_idx_from_address,
)

Expand Down Expand Up @@ -38,6 +39,9 @@ def rewrite_Apply_and_Broadcast(
if isinstance(applied_op, noise.stmts.QubitLoss):
return rewrite_QubitLoss(stmt)

if isinstance(applied_op, noise.stmts.CorrelatedQubitLoss):
return rewrite_CorrelatedQubitLoss(stmt)

assert isinstance(applied_op, op.stmts.Operator)

if isinstance(applied_op, op.stmts.Control):
Expand Down
6 changes: 4 additions & 2 deletions src/bloqade/stim/rewrite/squin_noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ def rewrite_Apply_and_Broadcast(

# this is an SSAValue, need it to be the actual operator
applied_op = stmt.operator.owner

if isinstance(applied_op, squin_noise.stmts.QubitLoss):
if isinstance(
applied_op,
(squin_noise.stmts.QubitLoss, squin_noise.stmts.CorrelatedQubitLoss),
):
return RewriteResult()

if isinstance(applied_op, squin_noise.stmts.NoiseChannel):
Expand Down
28 changes: 28 additions & 0 deletions src/bloqade/stim/rewrite/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
op.stmts.Identity: gate.Identity,
op.stmts.Reset: collapse.RZ,
squin_noise.stmts.QubitLoss: stim_noise.QubitLoss,
squin_noise.stmts.CorrelatedQubitLoss: stim_noise.CorrelatedQubitLoss,
}

# Squin allows creation of control gates where the gate can be any operator,
Expand Down Expand Up @@ -201,6 +202,33 @@ def rewrite_QubitLoss(
return RewriteResult(has_done_something=True)


def rewrite_CorrelatedQubitLoss(
stmt: qubit.Apply | qubit.Broadcast | wire.Broadcast | wire.Apply,
) -> RewriteResult:
"""
Rewrite CorrelatedQubitLoss statements to Stim's TrivialCorrelatedError.
"""

squin_loss_op = stmt.operator.owner
assert isinstance(squin_loss_op, squin_noise.stmts.CorrelatedQubitLoss)

qubit_idx_ssas = insert_qubit_idx_after_apply(stmt=stmt)
if qubit_idx_ssas is None:
return RewriteResult()

stim_loss_stmt = stim_noise.CorrelatedQubitLoss(
targets=qubit_idx_ssas,
probs=(squin_loss_op.p,),
)

if isinstance(stmt, (wire.Apply, wire.Broadcast)):
create_wire_passthrough(stmt)

stmt.replace_by(stim_loss_stmt)

return RewriteResult(has_done_something=True)


def create_wire_passthrough(stmt: wire.Apply | wire.Broadcast) -> None:

for input_wire, output_wire in zip(stmt.inputs, stmt.results):
Expand Down
4 changes: 4 additions & 0 deletions src/bloqade/stim/rewrite/wire_to_stim.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
SQUIN_STIM_OP_MAPPING,
rewrite_Control,
rewrite_QubitLoss,
rewrite_CorrelatedQubitLoss,
insert_qubit_idx_from_wire_ssa,
)

Expand All @@ -29,6 +30,9 @@ def rewrite_Apply_and_Broadcast(
if isinstance(applied_op, noise.stmts.QubitLoss):
return rewrite_QubitLoss(stmt)

if isinstance(applied_op, noise.stmts.CorrelatedQubitLoss):
return rewrite_CorrelatedQubitLoss(stmt)

assert isinstance(applied_op, op.stmts.Operator)

if isinstance(applied_op, op.stmts.Control):
Expand Down
16 changes: 16 additions & 0 deletions test/pyqrack/squin/test_noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ def main():
assert not qubit.is_active()


def test_correlated_loss():
@squin.kernel
def main():
q = squin.qubit.new(5)
squin.correlated_qubit_loss(0.5, q[0:4])
return q

target = PyQrack(5)
for _ in range(10):
qubits = target.run(main)
qubits_active = [q.is_active() for q in qubits[:4]]
assert all(qubits_active) or not any(qubits_active)

assert qubits[4].is_active()


def test_pauli_channel():
@squin.kernel
def single_qubit():
Expand Down
31 changes: 31 additions & 0 deletions test/squin/noise/test_stdlib_noise.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import numpy as np
import pytest

from bloqade import squin
from bloqade.pyqrack import PyQrackQubit, StackMemorySimulator

Expand All @@ -19,6 +22,34 @@ def main():
assert not qubit.is_active()


@pytest.mark.parametrize(
"seed, expected_loss_triggered",
[
(0, False), # Seed 0: no loss
(2, True), # Seed 2: qubits 0-3 are lost
],
)
def test_correlated_loss(seed, expected_loss_triggered):

@squin.kernel
def main():
q = squin.qubit.new(5)
squin.correlated_qubit_loss(0.5, q[0:4])
return q

rng = np.random.default_rng(seed=seed)
sim = StackMemorySimulator(min_qubits=5, rng_state=rng)
qubits = sim.run(main)

for q in qubits:
assert isinstance(q, PyQrackQubit)

for q in qubits[:4]:
assert not q.is_active() if expected_loss_triggered else q.is_active()

assert qubits[4].is_active()


def test_bit_flip():

@squin.kernel
Expand Down
15 changes: 15 additions & 0 deletions test/stim/dialects/stim/test_stim_circuits.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from bloqade import stim
from bloqade.stim.emit import EmitStimMain

Expand Down Expand Up @@ -94,6 +96,19 @@ def test_qubit_loss():
assert interp.get_output() == "\nI_ERROR[loss](0.10000000, 0.20000000) 0 1 2"


def test_correlated_qubit_loss():

@stim.main
def test_correlated_qubit_loss():
stim.correlated_qubit_loss(probs=(0.1,), targets=(0, 3, 1))

interp.run(test_correlated_qubit_loss, args=())

assert re.match(
r"\nI_ERROR\[correlated_loss:\d+\]\(0\.10000000\) 0 3 1", interp.get_output()
)


def test_collapse():
@stim.main
def test_measure():
Expand Down