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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
12 changes: 12 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,17 @@ 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[list[PyQrackQubit]] = frame.get(stmt.qubits)
for qubit_group in qubits:
if interp.rng_state.uniform(0.0, 1.0) <= p:
for qbit in qubit_group:
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
6 changes: 6 additions & 0 deletions src/bloqade/squin/noise/_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ 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[ilist.IList[Qubit, N], Any]
) -> None: ...
14 changes: 13 additions & 1 deletion src/bloqade/squin/noise/stmts.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,16 @@ 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)
qubits: ir.SSAValue = info.argument(ilist.IListType[QubitType, types.Any])


@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[ilist.IListType[QubitType, N], types.Any]
)
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,
)
26 changes: 26 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,32 @@ 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[ilist.IList[Qubit, N], Any]
) -> None:
"""
Apply correlated qubit loss channels to groups of qubits.

For each group of qubits, applies a correlated loss channel where all qubits
within the group are lost together with probability `p`. Loss events are independent
between different groups.

Args:
p (float): Loss probability for each group.
qubits (IList[IList[Qubit, N], Any]): List of qubit groups. Each sublist
represents a group of qubits to which a correlated loss channel is applied.

Example:
>>> q1 = squin.qubit.new(3) # First group: qubits 0, 1, 2
>>> q2 = squin.qubit.new(3) # Second group: qubits 3, 4, 5
>>> squin.broadcast.correlated_qubit_loss(0.5, [q1, q2])
# Each group has 50% chance: either all qubits lost or none lost.
# Group 1 and Group 2 outcomes are independent.
"""
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, ilist.IList([qubits]))


# 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(
Copy link
Author

Choose a reason for hiding this comment

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

I think the correlated error previously used to exist in the code base. I'm wondering if the previous approach also assigned a random number to nonce or if there is a better solution.

Copy link
Contributor

Choose a reason for hiding this comment

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

The nonce solution was mine originally. If you can find a stim instruction other than I_ERROR that satisfies the following, then please use it.

  • The instruction as emitted from kirin must have no effect when simulated in stim
  • The instruction must maintain groups of qubits, either within a single instruction or by the stim library not merging multiple instructions in sequence.

Copy link
Author

Choose a reason for hiding this comment

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

I agree that I_ERROR is the only choice.

I'm just wondering if a random 32 bit integer for the tag is the best solution or if, e.g., a global counter would be better.

Copy link
Contributor

Choose a reason for hiding this comment

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

the tag need to be different in this case? how does it work

Copy link
Contributor

Choose a reason for hiding this comment

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

@kaihsin When you import the stim IR into the stim package (stim.Circuit(...)) it optimizes the instructions by merging adjacent commands if they are the same instruction, have the same tag, and same argument values. They merge by concatenating the list of qubits targeted. E.g. CX 1 2 3 4; CX 2 4->CX 1 2 3 4 2 4.

Copy link
Contributor

Choose a reason for hiding this comment

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

@rafaelha A counter would also work as long as you can ensure no collisions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Alternative solution would be to put dummy I statements between each correlated error to block merging.

E.g.

I_ERROR[correlated_error](0.01) 1 2 3 4 5
I
I_ERROR[correlated_error](0.01) 6 7 8
I
I_ERROR[correlated_error](0.01) 1 3 7

Copy link
Author

Choose a reason for hiding this comment

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

I think I still prefer the random nonce. The I works, but seems a bit dangerous if some parser were to discard them.

Copy link
Contributor

Choose a reason for hiding this comment

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

I_ERROR[correlated_error](0.01) 1 2 3 4 5
I_ERROR[correlated_error_barrier]
I_ERROR[correlated_error](0.01) 6 7 8
I_ERROR[correlated_error_barrier]
I_ERROR[correlated_error](0.01) 1 3 7

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
59 changes: 59 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,62 @@ 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()


@pytest.mark.parametrize(
"seed, expected_loss_triggered",
[(0, (False, True)), (1, (False, False)), (2, (True, True)), (8, (True, False))],
)
def test_correlated_loss_broadcast(seed, expected_loss_triggered):

@squin.kernel
def main():
q = squin.qubit.new(6)
q1 = q[:3]
q2 = q[3:]
squin.broadcast.correlated_qubit_loss(0.5, [q1, q2])
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[:3]:
assert not q.is_active() if expected_loss_triggered[0] else q.is_active()

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


def test_bit_flip():

@squin.kernel
Expand Down
Loading