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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:

- name: Install dependencies
run: |
uv pip install --python .venv/bin/python -e .[dev]
uv sync --all-groups

- name: Run tests
run: .venv/bin/python -m pytest
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
".DS_Store",
".env",
".venv",
'api/components.*'
"api/components.*",
]

html_theme = "furo"
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ addopts = [
[dependency-groups]
dev = [
"furo>=2025.12.19",
"hypothesis>=6.151.14",
"invoke>=2.2.1",
"prek>=0.3.3",
"pytest>=9.0.2",
"ruff>=0.15.0",
"stim>=1.15.0",
]
docs = [
"myst-parser>=5.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/cluster_sim/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .simulator import ClusterState

__all__ = ["ClusterState"]
136 changes: 111 additions & 25 deletions src/cluster_sim/simulator/cluster_state.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Self
from rustworkx.visualization import mpl_draw
import graphsim
import rustworkx as rx
Expand All @@ -6,6 +7,7 @@
import itertools
import random


class ClusterState:
"""
Adapter class containing a GraphRegister object.
Expand Down Expand Up @@ -44,6 +46,28 @@ def __str__(self):
def __len__(self):
return self.num_nodes

def __add__(self, other: Self) -> "ClusterState":
new_state = ClusterState(self.num_nodes + other.num_nodes)
new_state.simulator = self.simulator + other.simulator
return new_state

def __getitem__(self, key):
if isinstance(key, slice) or isinstance(key, int):
return {
"neighbors": self.adjacency_list[key],
"stabilizer": self.stabilizers[key],
"vop": self.vertex_operators[key],
}
else:
raise TypeError("Invalid argument type")

def __iter__(self, key):
return zip(
self.adjacency_list,
self.stabilizers,
self.vertex_operators,
)

def measure(self, qubit: int, force: int = -1, basis: str = "Z"):
"""
Measure a node in the graph state.
Expand All @@ -62,6 +86,10 @@ def MX(self, qubit: int, force: int = -1):
def MY(self, qubit: int, force: int = -1):
return self.simulator.measure(qubit, force, "Y")

def M(self, qubit: int, force: int = -1):
"""Shorthand for MZ"""
return self.MZ(qubit, force)

def MZ(self, qubit: int, force: int = -1):
return self.simulator.measure(qubit, force, "Z")

Expand Down Expand Up @@ -111,8 +139,15 @@ def edge_local_complementation(self, edge: tuple[int, int]):
"""
raise NotImplementedError


def fusion_gate(self, qubit1: int, qubit2: int, gate_control="I", gate_target="I", mode="success", force=0):
def fusion_gate(
self,
qubit1: int,
qubit2: int,
gate_control="I",
gate_target="I",
mode="success",
force=0,
):
"""Apply a fusion gate.

Type II fusion of the form XXZZ corresponds to gate_control = I and gate_target = I.
Expand All @@ -126,49 +161,62 @@ def fusion_gate(self, qubit1: int, qubit2: int, gate_control="I", gate_target="I
gate_control(str): either I or H
gate_target(str): either I or H or SH
mode (str, optional): Either "success", "failure", or "random". Defaults to "success".
force (int): whether to force the measurements to be 0. Defaults to 0.
force (int): Force the measurements according to the following mapping:
- 0 : Qubit 1 = 0, Qubit 2 = 0
- 1 : Qubit 1 = 0, Qubit 2 = 1
- 2 : Qubit 1 = 1, Qubit 2 = 0
- 3 : Qubit 1 = 1, Qubit 2 = 1
- -1 : Random measurement
"""

force_settings = {
0 : (0, 0),
1 : (0, 1),
2 : (1, 0),
3 : (1, 1),
-1 : (-1, -1)
}

qubit1_force, qubit2_force = force_settings[force]

if mode == "random":
if random.randint(0, 1) == 0:
mode = "success"
else:
mode = "failure"

if mode == "success":

# The gates applied are Rc^\dag and Rt^\dag
getattr(self, gate_control)(qubit2)
if gate_target == 'SH':
getattr(self, gate_control)(qubit1)
if gate_target == "SH":
self.S_DAG(qubit2)
self.H(qubit2)
else:
getattr(self, gate_target)(qubit2)

self.CX(qubit1, qubit2)
self.H(qubit1)
self.MZ(qubit1, force=force)
self.MZ(qubit2, force=force)
self.MZ(qubit1, force=qubit1_force)
self.MZ(qubit2, force=qubit2_force)
elif mode == "failure":
if gate_control == "I" and gate_target == "I":
self.MZ(qubit1, force=force)
self.MZ(qubit1, force=qubit1_force)
self.X(qubit2)
self.MZ(qubit2, force=force)
self.MZ(qubit2, force=qubit2_force)
elif gate_control == "H" and gate_target == "I":
self.MX(qubit1, force=force)
self.MX(qubit1, force=qubit1_force)
self.X(qubit2)
self.MZ(qubit2, force=force)
self.MZ(qubit2, force=qubit2_force)
elif gate_control == "H" and gate_target == "H":
self.MX(qubit1, force=force)
self.MX(qubit1, force=qubit1_force)
self.Z(qubit2)
self.MX(qubit2, force=force)
self.MX(qubit2, force=qubit2_force)
elif gate_control == "H" and gate_target == "SH":
self.MZ(qubit1, force=force)
self.MY(qubit2, force=force)
self.MZ(qubit1, force=qubit1_force)
self.MY(qubit2, force=qubit2_force)
else:
raise NotImplementedError


def local_complementation(self, qubit):
"""Apply a local complementation.

Expand Down Expand Up @@ -227,8 +275,35 @@ def remove_node(self, qubits: int | list[int]):
self.simulator = ClusterState.from_rustworkx(new_graph).simulator
self.num_nodes -= len(qubits)

def subgraph(self, targets: list[int] | None = None):
"""Generate a copy and return a larger object.

This does not copy any edges out of the selection.

Args:
targets (list[int]): The qubits to copy. Defaults to all qubits.
"""

if targets is None:
targets = list(range(self.num_nodes))

graph = self.to_rustworkx()
return ClusterState.from_rustworkx(graph.subgraph(targets))

def duplicate(self, targets: list[int] | None = None):
"""Generate a copy of the selection and return a larger object.

Args:
targets (list[int]): The qubits to copy. Defaults to all qubits.
"""

if targets is None:
targets = list(range(self.num_nodes))

return self + self.subgraph(targets)

@classmethod
def load_text(cls, text: str, return_log: bool = False):
def from_text(cls, text: str, return_log: bool = False):
"""
Create a cluster state from a text-based representation of operations.
"""
Expand All @@ -248,7 +323,9 @@ def load_text(cls, text: str, return_log: bool = False):
max_qubit = -1

# Regex to find commands: NAME followed optionally by [numbers]
pattern = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)(?:\[([A-Za-z0-9_]+)\])?\s*([^A-Za-z_]*)")
pattern = re.compile(
r"([A-Za-z_][A-Za-z0-9_]*)(?:\[([A-Za-z0-9_]+)\])?\s*([^A-Za-z_]*)"
)

for match in pattern.finditer(full_text):
op_name = match.group(1).upper()
Expand All @@ -273,13 +350,14 @@ def load_text(cls, text: str, return_log: bool = False):
# ---------- Second pass: apply ops ----------
parsed_log = ""


for op_name, qubits, optional_args in parsed_ops:
if optional_args:
parsed_log += f"{op_name}[{optional_args}] {' '.join(map(str, qubits))}\n"
parsed_log += (
f"{op_name}[{optional_args}] {' '.join(map(str, qubits))}\n"
)
else:
parsed_log += f"{op_name} {' '.join(map(str, qubits))}\n"

if op_name == "ADD_NODE":
for q in qubits:
if q + 1 > num_nodes:
Expand All @@ -292,7 +370,7 @@ def load_text(cls, text: str, return_log: bool = False):
new_state.remove_node(q)
num_nodes -= 1

elif op_name in {"H", "X", "Y", "Z", "S"}:
elif op_name in {"H", "X", "Y", "Z", "S", "S_DAG"}:
for q in qubits:
getattr(new_state, op_name)(q)
elif op_name in {"MZ", "MX", "MY"}:
Expand All @@ -315,7 +393,13 @@ def load_text(cls, text: str, return_log: bool = False):
method(pair[0], pair[1])
elif op_name in {"FUSION_GATE"}:
for pair in itertools.combinations(qubits, 2):
new_state.fusion_gate(*pair, gate_control=optional_args[0], gate_target=optional_args[1])
new_state.fusion_gate(
*pair,
gate_control=optional_args[0],
gate_target=optional_args[1],
)
elif op_name in {"DUPLICATE"}:
new_state += new_state.subgraph(qubits)
else:
raise ValueError(f"Unknown operation: {op_name}")

Expand Down Expand Up @@ -534,7 +618,9 @@ def from_cytoscape(cls, data):
return G

@classmethod
def random_graph(cls, num_nodes: int, p: float = 0.5, local_cliffords: bool = False):
def random_graph(
cls, num_nodes: int, p: float = 0.5, local_cliffords: bool = False
):
"""Generate a random graph.

Args:
Expand Down
3 changes: 3 additions & 0 deletions src/components/components_2d/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
preprocess_cyto_data_elements,
postprocess_cyto_data_elements,
)
from .fusion_panel import fusion_menu
from .move_log_2d import move_log
from .figure_2d import figure_2d, tab_ui_2d



__all__ = [
"figure_2d",
"tab_ui_2d",
"move_log",
"qubit_panel",
"fusion_menu",
"preprocess_cyto_data_elements",
"postprocess_cyto_data_elements",
]
Loading
Loading