Repository: TeamGraphix/graphix

Issue:
Use `pydantic` effectively
**Is your feature request related to a problem? Please describe.**

Currently `pydantic` validations are sorely used to validate types, which can be elided assuming type annotations are respected.

**Describe the feature you'd like**

- Use `pydantic` to check additional constraints not manifested in type annotations, otherwise remove it
- Remove `BaseModel` inheritance to avoid unnecessary automatic validations
- Define simple models and use them manually

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix) Report
Attention: Patch coverage is `98.83041%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 76.72%. Comparing base [(`124e3ab`)](https://app.codecov.io/gh/TeamGraphix/graphix/commit/124e3ab9e0f95808f2192e6170e147996b7a4bbc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix) to head [(`261acbd`)](https://app.codecov.io/gh/TeamGraphix/graphix/commit/261acbde7802861285076e8b489ee7f44ba4c231?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix).

| [Files](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix) | Patch % | Lines |
|---|---|---|
| [graphix/instruction.py](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?src=pr&el=tree&filepath=graphix%2Finstruction.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix#diff-Z3JhcGhpeC9pbnN0cnVjdGlvbi5weQ==) | 99.00% | [1 Missing :warning: ](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix) |
| [graphix/sim/statevec.py](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?src=pr&el=tree&filepath=graphix%2Fsim%2Fstatevec.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix#diff-Z3JhcGhpeC9zaW0vc3RhdGV2ZWMucHk=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #205      +/-   ##
==========================================
+ Coverage   76.30%   76.72%   +0.42%     
==========================================
  Files          37       37              
  Lines        5768     5865      +97     
==========================================
+ Hits         4401     4500      +99     
+ Misses       1367     1365       -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/TeamGraphix/graphix/pull/205?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TeamGraphix).

shinich1: just out of interest, is 'kind' the right (idiomatic and/or normal) word for it? 'type' might be more straightforward? or perhaps just confusing?
EarlMilktea: I personally prefer `kind` because…

- `type` is a special keyword in Python
- this field is not exactly the same as the corresponding type name

Also let us emphasize that this field is added for faster `isinstance` check: users are unlikely to use it directly, and it is, by design, not reassigned.
thierry-martinez: `kind` is a common field name for tagged unions; you can find examples on [Wikipedia](https://en.wikipedia.org/wiki/Tagged_union). In Python specifically, `tag` is also a common name (see the [MyPy documentation on tagged unions](https://mypy.readthedocs.io/en/stable/literal_types.html#tagged-unions) for instance). In the specific case of `Command` and `Instruction`, `name` might be even more descriptive.
EarlMilktea: @thierry-martinez

I updated the design.
(Note that `command.py` and `instruction.py` are also now monitored by `mypy`)
Could you review again?
shinich1: @thierry-martinez could you also take a final look?
EarlMilktea: > This looks good to me but we need to update:
> 
> * tutorial
> * changelog

@shinich1 To my understanding, this PR does not introduce visible changes to user.

As for the docstring, let me update it when I submit another PR for refactoring `Pattern`.

Patch:
diff --git a/graphix/command.py b/graphix/command.py
index 555175d..6bd30f5 100644
--- a/graphix/command.py
+++ b/graphix/command.py
@@ -2,12 +2,15 @@
 
 from __future__ import annotations
 
-import abc
+import dataclasses
 import enum
+import sys
+from enum import Enum
+from typing import ClassVar, Literal, Union
 
 import numpy as np
-from pydantic import BaseModel, ConfigDict
 
+from graphix import type_utils
 from graphix.clifford import Clifford, Domains
 from graphix.pauli import Pauli, Plane, Sign
 from graphix.states import BasicStates, State
@@ -15,56 +18,46 @@ from graphix.states import BasicStates, State
 Node = int
 
 
-class CommandKind(enum.Enum):
+class CommandKind(Enum):
     """Tag for command kind."""
 
-    N = "N"
-    M = "M"
-    E = "E"
-    C = "C"
-    X = "X"
-    Z = "Z"
-    T = "T"
-    S = "S"
+    N = enum.auto()
+    M = enum.auto()
+    E = enum.auto()
+    C = enum.auto()
+    X = enum.auto()
+    Z = enum.auto()
+    S = enum.auto()
+    T = enum.auto()
 
 
-class Command(BaseModel, abc.ABC):
-    """Base command class."""
+class _KindChecker:
+    """Enforce tag field declaration."""
 
-    kind: CommandKind = None
+    def __init_subclass__(cls) -> None:
+        super().__init_subclass__()
+        type_utils.check_kind(cls, {"CommandKind": CommandKind, "Clifford": Clifford})
 
 
-class N(Command):
+@dataclasses.dataclass
+class N(_KindChecker):
     """Preparation command."""
 
-    kind: CommandKind = CommandKind.N
     node: Node
-    state: State = BasicStates.PLUS
+    state: State = dataclasses.field(default_factory=lambda: BasicStates.PLUS)
+    kind: ClassVar[Literal[CommandKind.N]] = dataclasses.field(default=CommandKind.N, init=False)
 
 
-class BaseM(Command):
-    """Base measurement command.
+@dataclasses.dataclass
+class M(_KindChecker):
+    """Measurement command. By default the plane is set to 'XY', the angle to 0, empty domains and identity vop."""
 
-    Represent a measurement of a node. In MBQC, a measure is an instance of `M`,
-    with given plane, angles, and domains. In the context of blind computations,
-    the server only knows which node is measured, and the parameters are given
-    by the :class:`graphix.simulator.MeasureMethod` provided by the client.
-    """
-
-    kind: CommandKind = CommandKind.M
     node: Node
-
-
-class M(BaseM):
-    """Measurement command.
-
-    By default the plane is set to 'XY', the angle to 0, empty domains.
-    """
-
     plane: Plane = Plane.XY
     angle: float = 0.0
-    s_domain: set[Node] = set()
-    t_domain: set[Node] = set()
+    s_domain: set[Node] = dataclasses.field(default_factory=set)
+    t_domain: set[Node] = dataclasses.field(default_factory=set)
+    kind: ClassVar[Literal[CommandKind.M]] = dataclasses.field(default=CommandKind.M, init=False)
 
     def clifford(self, clifford_gate: Clifford) -> M:
         """Apply a Clifford gate to the measure command.
@@ -74,65 +67,79 @@ class M(BaseM):
         domains = clifford_gate.commute_domains(Domains(self.s_domain, self.t_domain))
         update = MeasureUpdate.compute(self.plane, False, False, clifford_gate)
         return M(
-            node=self.node,
-            plane=update.new_plane,
-            angle=self.angle * update.coeff + update.add_term / np.pi,
-            s_domain=domains.s_domain,
-            t_domain=domains.t_domain,
+            self.node,
+            update.new_plane,
+            self.angle * update.coeff + update.add_term / np.pi,
+            domains.s_domain,
+            domains.t_domain,
         )
 
 
-class E(Command):
+@dataclasses.dataclass
+class E(_KindChecker):
     """Entanglement command."""
 
-    kind: CommandKind = CommandKind.E
     nodes: tuple[Node, Node]
+    kind: ClassVar[Literal[CommandKind.E]] = dataclasses.field(default=CommandKind.E, init=False)
 
 
-class C(Command):
+@dataclasses.dataclass
+class C(_KindChecker):
     """Clifford command."""
 
-    model_config = ConfigDict(arbitrary_types_allowed=True)  # for the `clifford` field
-
-    kind: CommandKind = CommandKind.C
     node: Node
     clifford: Clifford
+    kind: ClassVar[Literal[CommandKind.C]] = dataclasses.field(default=CommandKind.C, init=False)
 
 
-class Correction(Command):
-    """Base class for correction command (either X or Z)."""
-
-    node: Node
-    domain: set[Node] = set()
-
-
-class X(Correction):
+@dataclasses.dataclass
+class X(_KindChecker):
     """X correction command."""
 
-    kind: CommandKind = CommandKind.X
+    node: Node
+    domain: set[Node] = dataclasses.field(default_factory=set)
+    kind: ClassVar[Literal[CommandKind.X]] = dataclasses.field(default=CommandKind.X, init=False)
 
 
-class Z(Correction):
+@dataclasses.dataclass
+class Z(_KindChecker):
     """Z correction command."""
 
-    kind: CommandKind = CommandKind.Z
+    node: Node
+    domain: set[Node] = dataclasses.field(default_factory=set)
+    kind: ClassVar[Literal[CommandKind.Z]] = dataclasses.field(default=CommandKind.Z, init=False)
 
 
-class S(Command):
+@dataclasses.dataclass
+class S(_KindChecker):
     """S command."""
 
-    kind: CommandKind = CommandKind.S
     node: Node
-    domain: set[Node] = set()
+    domain: set[Node] = dataclasses.field(default_factory=set)
+    kind: ClassVar[Literal[CommandKind.S]] = dataclasses.field(default=CommandKind.S, init=False)
 
 
-class T(Command):
+@dataclasses.dataclass
+class T(_KindChecker):
     """T command."""
 
-    kind: CommandKind = CommandKind.T
+    node: Node
+    domain: set[Node] = dataclasses.field(default_factory=set)
+    kind: ClassVar[Literal[CommandKind.T]] = dataclasses.field(default=CommandKind.T, init=False)
+
+
+if sys.version_info >= (3, 10):
+    Command = N | M | E | C | X | Z | S | T
+    Correction = X | Z
+else:
+    Command = Union[N, M, E, C, X, Z, S, T]
+    Correction = Union[X, Z]
+
+BaseM = M
 
 
-class MeasureUpdate(BaseModel):
+@dataclasses.dataclass
+class MeasureUpdate:
     """Describe how a measure is changed by the signals and/or a vertex operator."""
 
     new_plane: Plane
@@ -161,4 +168,4 @@ class MeasureUpdate(BaseModel):
             add_term += np.pi
         if exchange:
             add_term = np.pi / 2 - add_term
-        return MeasureUpdate(new_plane=new_plane, coeff=coeff, add_term=add_term)
+        return MeasureUpdate(new_plane, coeff, add_term)
diff --git a/graphix/instruction.py b/graphix/instruction.py
index b816b11..2759f92 100644
--- a/graphix/instruction.py
+++ b/graphix/instruction.py
@@ -2,168 +2,192 @@
 
 from __future__ import annotations
 
-import abc
+import dataclasses
 import enum
+import sys
+from enum import Enum
+from typing import ClassVar, Literal, Union
 
-from pydantic import BaseModel
+from graphix import type_utils
+from graphix.pauli import Plane
 
-# MEMO: Cannot use TYPE_CHECKING here for pydantic
-from graphix.pauli import Plane  # noqa: TCH001
 
-
-class InstructionKind(enum.Enum):
+class InstructionKind(Enum):
     """Tag for instruction kind."""
 
-    CCX = "CCX"
-    RZZ = "RZZ"
-    CNOT = "CNOT"
-    SWAP = "SWAP"
-    H = "H"
-    S = "S"
-    X = "X"
-    Y = "Y"
-    Z = "Z"
-    I = "I"
-    M = "M"
-    RX = "RX"
-    RY = "RY"
-    RZ = "RZ"
+    CCX = enum.auto()
+    RZZ = enum.auto()
+    CNOT = enum.auto()
+    SWAP = enum.auto()
+    H = enum.auto()
+    S = enum.auto()
+    X = enum.auto()
+    Y = enum.auto()
+    Z = enum.auto()
+    I = enum.auto()
+    M = enum.auto()
+    RX = enum.auto()
+    RY = enum.auto()
+    RZ = enum.auto()
     # The two following instructions are used internally by the transpiler
-    XC = "XC"
-    ZC = "ZC"
+    _XC = enum.auto()
+    _ZC = enum.auto()
 
 
-class Instruction(BaseModel, abc.ABC):
-    """Circuit instruction base class model."""
+class _KindChecker:
+    """Enforce tag field declaration."""
 
-    kind: InstructionKind = None
-    meas_index: int = None
+    def __init_subclass__(cls) -> None:
+        super().__init_subclass__()
+        type_utils.check_kind(cls, {"InstructionKind": InstructionKind, "Plane": Plane})
 
 
-class OneQubitInstruction(Instruction):
-    """One qubit circuit instruction base class model."""
+@dataclasses.dataclass
+class CCX(_KindChecker):
+    """Toffoli circuit instruction."""
 
     target: int
-
-
-class CorrectionInstruction(OneQubitInstruction):
-    """Correction instruction base class model."""
-
-    domain: set[int]
-
-
-class RotationInstruction(OneQubitInstruction):
-    """Rotation instruction base class model."""
-
-    angle: float
-
-
-class OneControlInstruction(OneQubitInstruction):
-    """One control instruction base class model."""
-
-    control: int
-
-
-class TwoControlsInstruction(OneQubitInstruction):
-    """Two controls instruction base class model."""
-
     controls: tuple[int, int]
+    kind: ClassVar[Literal[InstructionKind.CCX]] = dataclasses.field(default=InstructionKind.CCX, init=False)
 
 
-class XC(CorrectionInstruction):
-    """X correction circuit instruction. Used internally by the transpiler."""
-
-    kind: InstructionKind = InstructionKind.XC
-
-
-class ZC(CorrectionInstruction):
-    """Z correction circuit instruction. Used internally by the transpiler."""
-
-    kind: InstructionKind = InstructionKind.ZC
-
-
-class CCX(TwoControlsInstruction):
-    """Toffoli circuit instruction."""
-
-    kind: InstructionKind = InstructionKind.CCX
-
-
-class RZZ(OneControlInstruction, RotationInstruction):
+@dataclasses.dataclass
+class RZZ(_KindChecker):
     """RZZ circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.RZZ
+    target: int
+    control: int
+    angle: float
+    # FIXME: Remove `| None` from `meas_index`
+    # - `None` makes codes messy/type-unsafe
+    meas_index: int | None = None
+    kind: ClassVar[Literal[InstructionKind.RZZ]] = dataclasses.field(default=InstructionKind.RZZ, init=False)
 
 
-class CNOT(OneControlInstruction):
+@dataclasses.dataclass
+class CNOT(_KindChecker):
     """CNOT circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.CNOT
+    target: int
+    control: int
+    kind: ClassVar[Literal[InstructionKind.CNOT]] = dataclasses.field(default=InstructionKind.CNOT, init=False)
 
 
-class SWAP(Instruction):
+@dataclasses.dataclass
+class SWAP(_KindChecker):
     """SWAP circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.SWAP
     targets: tuple[int, int]
+    kind: ClassVar[Literal[InstructionKind.SWAP]] = dataclasses.field(default=InstructionKind.SWAP, init=False)
 
 
-class H(OneQubitInstruction):
+@dataclasses.dataclass
+class H(_KindChecker):
     """H circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.H
+    target: int
+    kind: ClassVar[Literal[InstructionKind.H]] = dataclasses.field(default=InstructionKind.H, init=False)
 
 
-class S(OneQubitInstruction):
+@dataclasses.dataclass
+class S(_KindChecker):
     """S circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.S
+    target: int
+    kind: ClassVar[Literal[InstructionKind.S]] = dataclasses.field(default=InstructionKind.S, init=False)
 
 
-class X(OneQubitInstruction):
+@dataclasses.dataclass
+class X(_KindChecker):
     """X circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.X
+    target: int
+    kind: ClassVar[Literal[InstructionKind.X]] = dataclasses.field(default=InstructionKind.X, init=False)
 
 
-class Y(OneQubitInstruction):
+@dataclasses.dataclass
+class Y(_KindChecker):
     """Y circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.Y
+    target: int
+    kind: ClassVar[Literal[InstructionKind.Y]] = dataclasses.field(default=InstructionKind.Y, init=False)
 
 
-class Z(OneQubitInstruction):
+@dataclasses.dataclass
+class Z(_KindChecker):
     """Z circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.Z
+    target: int
+    kind: ClassVar[Literal[InstructionKind.Z]] = dataclasses.field(default=InstructionKind.Z, init=False)
 
 
-class I(OneQubitInstruction):
+@dataclasses.dataclass
+class I(_KindChecker):
     """I circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.I
+    target: int
+    kind: ClassVar[Literal[InstructionKind.I]] = dataclasses.field(default=InstructionKind.I, init=False)
 
 
-class M(OneQubitInstruction):
+@dataclasses.dataclass
+class M(_KindChecker):
     """M circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.M
+    target: int
     plane: Plane
     angle: float
+    kind: ClassVar[Literal[InstructionKind.M]] = dataclasses.field(default=InstructionKind.M, init=False)
 
 
-class RX(RotationInstruction):
+@dataclasses.dataclass
+class RX(_KindChecker):
     """X rotation circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.RX
+    target: int
+    angle: float
+    meas_index: int | None = None
+    kind: ClassVar[Literal[InstructionKind.RX]] = dataclasses.field(default=InstructionKind.RX, init=False)
 
 
-class RY(RotationInstruction):
+@dataclasses.dataclass
+class RY(_KindChecker):
     """Y rotation circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.RY
+    target: int
+    angle: float
+    meas_index: int | None = None
+    kind: ClassVar[Literal[InstructionKind.RY]] = dataclasses.field(default=InstructionKind.RY, init=False)
 
 
-class RZ(RotationInstruction):
+@dataclasses.dataclass
+class RZ(_KindChecker):
     """Z rotation circuit instruction."""
 
-    kind: InstructionKind = InstructionKind.RZ
+    target: int
+    angle: float
+    meas_index: int | None = None
+    kind: ClassVar[Literal[InstructionKind.RZ]] = dataclasses.field(default=InstructionKind.RZ, init=False)
+
+
+@dataclasses.dataclass
+class _XC(_KindChecker):
+    """X correction circuit instruction. Used internally by the transpiler."""
+
+    target: int
+    domain: set[int]
+    kind: ClassVar[Literal[InstructionKind._XC]] = dataclasses.field(default=InstructionKind._XC, init=False)
+
+
+@dataclasses.dataclass
+class _ZC(_KindChecker):
+    """Z correction circuit instruction. Used internally by the transpiler."""
+
+    target: int
+    domain: set[int]
+    kind: ClassVar[Literal[InstructionKind._ZC]] = dataclasses.field(default=InstructionKind._ZC, init=False)
+
+
+if sys.version_info >= (3, 10):
+    Instruction = CCX | RZZ | CNOT | SWAP | H | S | X | Y | Z | I | M | RX | RY | RZ | _XC | _ZC
+else:
+    Instruction = Union[CCX, RZZ, CNOT, SWAP, H, S, X, Y, Z, I, M, RX, RY, RZ, _XC, _ZC]
diff --git a/graphix/pattern.py b/graphix/pattern.py
index d68d885..022e2c4 100644
--- a/graphix/pattern.py
+++ b/graphix/pattern.py
@@ -5,6 +5,7 @@ ref: V. Danos, E. Kashefi and P. Panangaden. J. ACM 54.2 8 (2007)
 
 from __future__ import annotations
 
+import dataclasses
 import warnings
 from copy import deepcopy
 from dataclasses import dataclass
@@ -381,7 +382,7 @@ class Pattern:
                 if clifford_gate := c_dict.pop(cmd.node, None):
                     new_cmd = cmd.clifford(clifford_gate)
                 else:
-                    new_cmd = cmd.model_copy()
+                    new_cmd = deepcopy(cmd)
                 if t_domain := z_dict.pop(cmd.node, None):
                     new_cmd.t_domain ^= t_domain
                 if s_domain := x_dict.pop(cmd.node, None):
@@ -538,12 +539,12 @@ class Pattern:
                         signal_dict[cmd.node] = s_domain
                         s_domain = set()
                 if s_domain != cmd.s_domain or t_domain != cmd.t_domain:
-                    self.__seq[i] = cmd.model_copy(update={"s_domain": s_domain, "t_domain": t_domain})
+                    self.__seq[i] = dataclasses.replace(cmd, s_domain=s_domain, t_domain=t_domain)
             elif cmd.kind == CommandKind.X or cmd.kind == CommandKind.Z:
                 domain = set(cmd.domain)
                 expand_domain(domain)
                 if domain != cmd.domain:
-                    self.__seq[i] = cmd.model_copy(update={"domain": domain})
+                    self.__seq[i] = dataclasses.replace(cmd, domain=domain)
         return signal_dict
 
     def _find_op_to_be_moved(self, op: CommandKind, rev=False, skipnum=0):
@@ -1540,7 +1541,7 @@ class Pattern:
     def copy(self) -> Pattern:
         """Return a copy of the pattern."""
         result = self.__new__(self.__class__)
-        result.__seq = [cmd.model_copy(deep=True) for cmd in self.__seq]
+        result.__seq = [deepcopy(cmd) for cmd in self.__seq]
         result.__input_nodes = self.__input_nodes.copy()
         result.__output_nodes = self.__output_nodes.copy()
         result.__n_node = self.__n_node
diff --git a/graphix/sim/density_matrix.py b/graphix/sim/density_matrix.py
index 5e224a4..0979956 100644
--- a/graphix/sim/density_matrix.py
+++ b/graphix/sim/density_matrix.py
@@ -17,7 +17,6 @@ import sys
 import numpy as np
 
 import graphix.states
-import graphix.types
 from graphix import linalg_validations as lv
 from graphix.channels import KrausChannel
 from graphix.sim.base_backend import Backend, State
@@ -30,7 +29,7 @@ class DensityMatrix(State):
     def __init__(
         self,
         data: Data = graphix.states.BasicStates.PLUS,
-        nqubit: graphix.types.PositiveOrNullInt | None = None,
+        nqubit: int | None = None,
     ):
         """Initialize density matrix objects.
 
@@ -54,7 +53,8 @@ class DensityMatrix(State):
         :param nqubit: number of qubits to prepare, defaults to `None`
         :type nqubit: int, optional
         """
-        assert nqubit is None or isinstance(nqubit, numbers.Integral) and nqubit >= 0
+        if nqubit is not None and nqubit < 0:
+            raise ValueError("nqubit must be a non-negative integer.")
 
         def check_size_consistency(mat):
             if nqubit is not None and mat.shape != (2**nqubit, 2**nqubit):
diff --git a/graphix/sim/statevec.py b/graphix/sim/statevec.py
index 1a15650..33a99d6 100644
--- a/graphix/sim/statevec.py
+++ b/graphix/sim/statevec.py
@@ -11,17 +11,16 @@ from typing import TYPE_CHECKING
 import numpy as np
 import numpy.typing as npt
 
-if TYPE_CHECKING:
-    from numpy.random import Generator
-
 import graphix.pauli
 import graphix.states
-import graphix.types
+from graphix import type_utils
 from graphix.sim.base_backend import Backend, State
 
 if TYPE_CHECKING:
     import collections
 
+    from numpy.random import Generator
+
 
 class StatevectorBackend(Backend):
     """MBQC simulator with statevector method."""
@@ -64,7 +63,7 @@ class Statevec(State):
     def __init__(
         self,
         data: Data = graphix.states.BasicStates.PLUS,
-        nqubit: graphix.types.PositiveOrNullInt | None = None,
+        nqubit: int | None = None,
     ):
         """Initialize statevector objects.
 
@@ -86,7 +85,8 @@ class Statevec(State):
         :param nqubit: number of qubits to prepare, defaults to None
         :type nqubit: int, optional
         """
-        assert nqubit is None or isinstance(nqubit, numbers.Integral) and nqubit >= 0
+        if nqubit is not None and nqubit < 0:
+            raise ValueError("nqubit must be a non-negative integer.")
 
         if isinstance(data, Statevec):
             # assert nqubit is None or len(state.flatten()) == 2**nqubit
@@ -114,7 +114,7 @@ class Statevec(State):
 
         else:
             if isinstance(input_list[0], graphix.states.State):
-                graphix.types.check_list_elements(input_list, graphix.states.State)
+                type_utils.check_list_elements(input_list, graphix.states.State)
                 if nqubit is None:
                     nqubit = len(input_list)
                 elif nqubit != len(input_list):
@@ -124,7 +124,7 @@ class Statevec(State):
                 # reshape
                 self.psi = tmp_psi.reshape((2,) * nqubit)
             elif isinstance(input_list[0], numbers.Number):
-                graphix.types.check_list_elements(input_list, numbers.Number)
+                type_utils.check_list_elements(input_list, numbers.Number)
                 if nqubit is None:
                     length = len(input_list)
                     if length & (length - 1):
diff --git a/graphix/sim/tensornet.py b/graphix/sim/tensornet.py
index b5e53a7..ef81b1a 100644
--- a/graphix/sim/tensornet.py
+++ b/graphix/sim/tensornet.py
@@ -169,7 +169,7 @@ class TensorNetworkBackend(Backend):
             result = self.__rng.choice([0, 1])
             self.results[node] = result
             buffer = 2**0.5
-        vec = PlanarState(plane=measurement_description.plane, angle=measurement_description.angle).get_statevector()
+        vec = PlanarState(measurement_description.plane, measurement_description.angle).get_statevector()
         if result:
             vec = measurement_description.plane.orth.op @ vec
         proj_vec = vec * buffer
diff --git a/graphix/states.py b/graphix/states.py
index 492812b..e612c42 100644
--- a/graphix/states.py
+++ b/graphix/states.py
@@ -3,20 +3,19 @@
 from __future__ import annotations
 
 import abc
-import typing
+from abc import ABC
 from typing import ClassVar
 
 import numpy as np
 import numpy.typing as npt
-import pydantic
-import pydantic_core
+import pydantic.dataclasses
 import typing_extensions
 
 from graphix.pauli import Plane
 
 
 # generic class State for all States
-class State(abc.ABC):
+class State(ABC):
     """Abstract base class for single qubit states objects.
 
     Only requirement for concrete classes is to have
@@ -25,30 +24,17 @@ class State(abc.ABC):
     """
 
     @abc.abstractmethod
-    def get_statevector(self) -> npt.NDArray:
+    def get_statevector(self) -> npt.NDArray[np.complex128]:
         """Return the state vector."""
-        ...
 
-    def get_densitymatrix(self) -> npt.NDArray:
+    def get_densitymatrix(self) -> npt.NDArray[np.complex128]:
         """Return the density matrix."""
         # return DM in 2**n x 2**n dim (2x2 here)
         return np.outer(self.get_statevector(), self.get_statevector().conj())
 
-    @classmethod
-    def __get_pydantic_core_schema__(
-        cls, source_type: typing.Any, handler: pydantic.GetCoreSchemaHandler
-    ) -> pydantic_core.CoreSchema:
-        """Validate state."""
 
-        def check_state(obj) -> State:
-            if not isinstance(obj, State):
-                raise ValueError("State expected")
-            return obj
-
-        return pydantic_core.core_schema.no_info_plain_validator_function(function=check_state)
-
-
-class PlanarState(pydantic.BaseModel, State):
+@pydantic.dataclasses.dataclass
+class PlanarState(State):
     """Light object used to instantiate backends.
 
     doesn't cover all possible states but this is
@@ -69,22 +55,22 @@ class PlanarState(pydantic.BaseModel, State):
 
     def __repr__(self) -> str:
         """Return a string representation of the planar state."""
-        return f"graphix.states.PlanarState(plane={self.plane}, angle={self.angle})"
+        return f"graphix.states.PlanarState({self.plane}, {self.angle})"
 
     def __str__(self) -> str:
         """Return a string description of the planar state."""
         return f"PlanarState object defined in plane {self.plane} with angle {self.angle}."
 
-    def get_statevector(self) -> npt.NDArray:
+    def get_statevector(self) -> npt.NDArray[np.complex128]:
         """Return the state vector."""
         if self.plane == Plane.XY:
-            return np.array([1, np.exp(1j * self.angle)]) / np.sqrt(2)
+            return np.asarray([1 / np.sqrt(2), np.exp(1j * self.angle) / np.sqrt(2)], dtype=np.complex128)
 
         if self.plane == Plane.YZ:
-            return np.array([np.cos(self.angle / 2), 1j * np.sin(self.angle / 2)])
+            return np.asarray([np.cos(self.angle / 2), 1j * np.sin(self.angle / 2)], dtype=np.complex128)
 
         if self.plane == Plane.XZ:
-            return np.array([np.cos(self.angle / 2), np.sin(self.angle / 2)])
+            return np.asarray([np.cos(self.angle / 2), np.sin(self.angle / 2)], dtype=np.complex128)
         # other case never happens since exhaustive
         typing_extensions.assert_never(self.plane)
 
@@ -93,12 +79,12 @@ class PlanarState(pydantic.BaseModel, State):
 class BasicStates:
     """Basic states."""
 
-    ZERO = PlanarState(plane=Plane.XZ, angle=0)
-    ONE = PlanarState(plane=Plane.XZ, angle=np.pi)
-    PLUS = PlanarState(plane=Plane.XY, angle=0)
-    MINUS = PlanarState(plane=Plane.XY, angle=np.pi)
-    PLUS_I = PlanarState(plane=Plane.XY, angle=np.pi / 2)
-    MINUS_I = PlanarState(plane=Plane.XY, angle=-np.pi / 2)
+    ZERO: ClassVar = PlanarState(Plane.XZ, 0)
+    ONE: ClassVar = PlanarState(Plane.XZ, np.pi)
+    PLUS: ClassVar = PlanarState(Plane.XY, 0)
+    MINUS: ClassVar = PlanarState(Plane.XY, np.pi)
+    PLUS_I: ClassVar = PlanarState(Plane.XY, np.pi / 2)
+    MINUS_I: ClassVar = PlanarState(Plane.XY, -np.pi / 2)
     # remove that in the end
     # need in TN backend
     VEC: ClassVar = [PLUS, MINUS, ZERO, ONE, PLUS_I, MINUS_I]
diff --git a/graphix/transpiler.py b/graphix/transpiler.py
index 533c084..5f61920 100644
--- a/graphix/transpiler.py
+++ b/graphix/transpiler.py
@@ -455,19 +455,19 @@ class Circuit:
                 n_node += 2
                 self._instr.append(instr)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[7].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[8].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.control,
                         domain=seq[9].domain,
                     )
@@ -488,7 +488,7 @@ class Circuit:
                 self._m.append(seq[2])
                 self._instr.append(instr)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[3].domain,
                     )
@@ -502,13 +502,13 @@ class Circuit:
                 self._m.extend(seq[4:6])
                 self._instr.append(instr)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[6].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[7].domain,
                     )
@@ -522,13 +522,13 @@ class Circuit:
                 self._m.extend(seq[4:6])
                 self._instr.append(instr)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[6].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[7].domain,
                     )
@@ -542,13 +542,13 @@ class Circuit:
                 self._m.extend(seq[8:12])
                 self._instr.append(instr)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[12].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[13].domain,
                     )
@@ -562,13 +562,13 @@ class Circuit:
                 self._m.extend(seq[4:6])
                 self._instr.append(instr)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[6].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[7].domain,
                     )
@@ -584,13 +584,13 @@ class Circuit:
                 instr_.meas_index = len(self._m) - 1  # index of arb angle measurement command
                 self._instr.append(instr_)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[6].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[7].domain,
                     )
@@ -606,13 +606,13 @@ class Circuit:
                 instr_.meas_index = len(self._m) - 3  # index of arb angle measurement command
                 self._instr.append(instr_)
                 self._instr.append(
-                    instruction.XC(
+                    instruction._XC(
                         target=instr.target,
                         domain=seq[12].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[13].domain,
                     )
@@ -629,7 +629,7 @@ class Circuit:
                     instr_.meas_index = len(self._m) - 1  # index of arb angle measurement command
                     self._instr.append(instr_)
                     self._instr.append(
-                        instruction.ZC(
+                        instruction._ZC(
                             target=instr.target,
                             domain=seq[3].domain,
                         )
@@ -645,13 +645,13 @@ class Circuit:
                     instr_.meas_index = len(self._m) - 2  # index of arb angle measurement command
                     self._instr.append(instr_)
                     self._instr.append(
-                        instruction.XC(
+                        instruction._XC(
                             target=instr.target,
                             domain=seq[6].domain,
                         )
                     )
                     self._instr.append(
-                        instruction.ZC(
+                        instruction._ZC(
                             target=instr.target,
                             domain=seq[7].domain,
                         )
@@ -670,13 +670,13 @@ class Circuit:
                 instr_.meas_index = len(self._m) - 1  # index of arb angle measurement command
                 self._instr.append(instr_)
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.target,
                         domain=seq[4].domain,
                     )
                 )
                 self._instr.append(
-                    instruction.ZC(
+                    instruction._ZC(
                         target=instr.control,
                         domain=seq[5].domain,
                     )
@@ -696,13 +696,13 @@ class Circuit:
         x_cmds: list[command.X] = []
         for i in range(len(self._instr)):
             instr = self._instr[i]
-            if instr.kind == instruction.InstructionKind.XC:
+            if instr.kind == instruction.InstructionKind._XC:
                 if instr.target in bpx_added.keys():
                     x_cmds[bpx_added[instr.target]].domain ^= instr.domain
                 else:
                     bpx_added[instr.target] = len(x_cmds)
                     x_cmds.append(X(node=out[instr.target], domain=deepcopy(instr.domain)))
-            elif instr.kind == instruction.InstructionKind.ZC:
+            elif instr.kind == instruction.InstructionKind._ZC:
                 if instr.target in bpz_added.keys():
                     z_cmds[bpz_added[instr.target]].domain ^= instr.domain
                 else:
@@ -721,8 +721,8 @@ class Circuit:
         correction_instr = self._instr[target]
         swap_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert swap_instr.kind == instruction.InstructionKind.SWAP
         if correction_instr.target == swap_instr.targets[0]:
@@ -739,14 +739,14 @@ class Circuit:
         correction_instr = self._instr[target]
         cnot_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert cnot_instr.kind == instruction.InstructionKind.CNOT
         if (
-            correction_instr.kind == instruction.InstructionKind.XC and correction_instr.target == cnot_instr.control
+            correction_instr.kind == instruction.InstructionKind._XC and correction_instr.target == cnot_instr.control
         ):  # control
-            new_cmd = instruction.XC(
+            new_cmd = instruction._XC(
                 target=cnot_instr.target,
                 domain=correction_instr.domain,
             )
@@ -754,9 +754,9 @@ class Circuit:
             self._instr.insert(target + 1, new_cmd)
             return target + 1
         elif (
-            correction_instr.kind == instruction.InstructionKind.ZC and correction_instr.target == cnot_instr.target
+            correction_instr.kind == instruction.InstructionKind._ZC and correction_instr.target == cnot_instr.target
         ):  # target
-            new_cmd = instruction.ZC(
+            new_cmd = instruction._ZC(
                 target=cnot_instr.control,
                 domain=correction_instr.domain,
             )
@@ -771,18 +771,18 @@ class Circuit:
         correction_instr = self._instr[target]
         h_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert h_instr.kind == instruction.InstructionKind.H
         if correction_instr.target == h_instr.target:
-            if correction_instr.kind == instruction.InstructionKind.XC:
-                self._instr[target] = instruction.ZC(
+            if correction_instr.kind == instruction.InstructionKind._XC:
+                self._instr[target] = instruction._ZC(
                     target=correction_instr.target, domain=correction_instr.domain
                 )  # byproduct changes to Z
                 self._commute_with_following(target)
             else:
-                self._instr[target] = instruction.XC(
+                self._instr[target] = instruction._XC(
                     target=correction_instr.target, domain=correction_instr.domain
                 )  # byproduct changes to X
                 self._commute_with_following(target)
@@ -793,17 +793,17 @@ class Circuit:
         correction_instr = self._instr[target]
         s_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert s_instr.kind == instruction.InstructionKind.S
         if correction_instr.target == s_instr.target:
-            if correction_instr.kind == instruction.InstructionKind.XC:
+            if correction_instr.kind == instruction.InstructionKind._XC:
                 self._commute_with_following(target)
                 # changes to Y = XZ
                 self._instr.insert(
                     target + 1,
-                    instruction.ZC(
+                    instruction._ZC(
                         target=correction_instr.target,
                         domain=correction_instr.domain,
                     ),
@@ -816,12 +816,12 @@ class Circuit:
         correction_instr = self._instr[target]
         rx_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert rx_instr.kind == instruction.InstructionKind.RX
         if correction_instr.target == rx_instr.target:
-            if correction_instr.kind == instruction.InstructionKind.ZC:
+            if correction_instr.kind == instruction.InstructionKind._ZC:
                 # add to the s-domain
                 _extend_domain(self._m[rx_instr.meas_index], correction_instr.domain)
                 self._commute_with_following(target)
@@ -834,8 +834,8 @@ class Circuit:
         correction_instr = self._instr[target]
         ry_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert ry_instr.kind == instruction.InstructionKind.RY
         if correction_instr.target == ry_instr.target:
@@ -849,12 +849,12 @@ class Circuit:
         correction_instr = self._instr[target]
         rz_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert rz_instr.kind == instruction.InstructionKind.RZ
         if correction_instr.target == rz_instr.target:
-            if correction_instr.kind == instruction.InstructionKind.XC:
+            if correction_instr.kind == instruction.InstructionKind._XC:
                 # add to the s-domain
                 _extend_domain(self._m[rz_instr.meas_index], correction_instr.domain)
                 self._commute_with_following(target)
@@ -867,11 +867,11 @@ class Circuit:
         correction_instr = self._instr[target]
         rzz_instr = self._instr[target + 1]
         assert (
-            correction_instr.kind == instruction.InstructionKind.XC
-            or correction_instr.kind == instruction.InstructionKind.ZC
+            correction_instr.kind == instruction.InstructionKind._XC
+            or correction_instr.kind == instruction.InstructionKind._ZC
         )
         assert rzz_instr.kind == instruction.InstructionKind.RZZ
-        if correction_instr.kind == instruction.InstructionKind.XC:
+        if correction_instr.kind == instruction.InstructionKind._XC:
             cond = correction_instr.target == rzz_instr.control
             cond2 = correction_instr.target == rzz_instr.target
             if cond or cond2:
@@ -913,8 +913,8 @@ class Circuit:
         num_ops = 0
         while ite < len(self._instr):
             if (
-                self._instr[target].kind == instruction.InstructionKind.ZC
-                or self._instr[target].kind == instruction.InstructionKind.XC
+                self._instr[target].kind == instruction.InstructionKind._ZC
+                or self._instr[target].kind == instruction.InstructionKind._XC
             ):
                 num_ops += 1
             if num_ops == skipnum + 1:
@@ -930,8 +930,8 @@ class Circuit:
         target = self._find_byproduct_to_move(rev=True, skipnum=moved)
         while target != "end":
             if (target == len(self._instr) - 1) or (
-                self._instr[target + 1].kind == instruction.InstructionKind.XC
-                or self._instr[target + 1].kind == instruction.InstructionKind.ZC
+                self._instr[target + 1].kind == instruction.InstructionKind._XC
+                or self._instr[target + 1].kind == instruction.InstructionKind._ZC
             ):
                 moved += 1
                 target = self._find_byproduct_to_move(rev=True, skipnum=moved)
diff --git a/graphix/type_utils.py b/graphix/type_utils.py
new file mode 100644
index 0000000..e1fbc0c
--- /dev/null
+++ b/graphix/type_utils.py
@@ -0,0 +1,43 @@
+"""Type utilities."""
+
+from __future__ import annotations
+
+import sys
+import typing
+from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
+
+if TYPE_CHECKING:
+    from collections.abc import Iterable
+
+_T = TypeVar("_T")
+
+
+def check_list_elements(l: Iterable[_T], ty: type[_T]) -> None:
+    """Check that every element of the list has the given type."""
+    for index, item in enumerate(l):
+        if not isinstance(item, ty):
+            raise TypeError(f"data[{index}] has type {type(item)} whereas {ty} is expected")
+
+
+def check_kind(cls: type, scope: dict[str, Any]) -> None:
+    """Check that the class has a kind attribute."""
+    if not hasattr(cls, "kind"):
+        msg = f"{cls.__name__} must have a tag attribute named kind."
+        raise TypeError(msg)
+    if sys.version_info < (3, 10):
+        # MEMO: `inspect.get_annotations` unavailable
+        return
+
+    import inspect
+
+    ann = inspect.get_annotations(cls, eval_str=True, locals=scope).get("kind")
+    if ann is None:
+        msg = "kind must be annotated."
+        raise TypeError(msg)
+    if typing.get_origin(ann) is not ClassVar:
+        msg = "Tag attribute must be a class variable."
+        raise TypeError(msg)
+    (ann,) = typing.get_args(ann)
+    if typing.get_origin(ann) is not Literal:
+        msg = "Tag attribute must be a literal."
+        raise TypeError(msg)
diff --git a/graphix/types.py b/graphix/types.py
deleted file mode 100644
index 1138258..0000000
--- a/graphix/types.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Types."""
-
-from __future__ import annotations
-
-from typing import TypeVar
-
-import annotated_types
-import typing_extensions
-
-if typing_extensions.TYPE_CHECKING:
-    from collections.abc import Iterable
-
-PositiveOrNullInt = typing_extensions.Annotated[int, annotated_types.Ge(0)]  # includes 0
-
-_T = TypeVar("_T")
-
-
-def check_list_elements(l: Iterable[_T], ty: type[_T]) -> None:
-    """Check that every element of the list has the given type."""
-    for index, item in enumerate(l):
-        if not isinstance(item, ty):
-            raise TypeError(f"data[{index}] has type {type(item)} whereas {ty} is expected")
diff --git a/pyproject.toml b/pyproject.toml
index 40ebcc9..40eeb70 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -99,10 +99,13 @@ filterwarnings = ["ignore:Couldn't import `kahypar`"]
 files = [
   "graphix/channels.py",
   "graphix/clifford.py",
+  "graphix/command.py",
+  "graphix/instruction.py",
   "graphix/linalg_validations.py",
   "graphix/ops.py",
   "graphix/rng.py",
-  "graphix/types.py",
+  "graphix/states.py",
+  "graphix/type_utils.py",
   "graphix/_db.py",
   "tests/conftest.py",
   "tests/test_clifford.py",
@@ -120,10 +123,13 @@ strict = true
 include = [
   "graphix/channels.py",
   "graphix/clifford.py",
+  "graphix/command.py",
+  "graphix/instruction.py",
   "graphix/linalg_validations.py",
   "graphix/ops.py",
   "graphix/rng.py",
-  "graphix/types.py",
+  "graphix/states.py",
+  "graphix/type_utils.py",
   "graphix/_db.py",
   "tests/conftest.py",
   "tests/test_clifford.py",


Test patch:
diff --git a/tests/test_density_matrix.py b/tests/test_density_matrix.py
index ab12680..923ff9c 100644
--- a/tests/test_density_matrix.py
+++ b/tests/test_density_matrix.py
@@ -37,12 +37,8 @@ class TestDensityMatrix:
     """Test for DensityMatrix class."""
 
     def test_init_without_data_fail(self) -> None:
-        with pytest.raises(AssertionError):
+        with pytest.raises(ValueError):
             DensityMatrix(nqubit=-2)
-        with pytest.raises(AssertionError):
-            DensityMatrix(nqubit="hello")
-        with pytest.raises(AssertionError):
-            DensityMatrix(nqubit=[])
 
     def test_init_with_invalid_data_fail(self, fx_rng: Generator) -> None:
         with pytest.raises(TypeError):
diff --git a/tests/test_gflow.py b/tests/test_gflow.py
index a0ef22c..0a96f6f 100644
--- a/tests/test_gflow.py
+++ b/tests/test_gflow.py
@@ -601,12 +601,12 @@ class TestGflow:
             assert valid
 
     def test_corrections_from_pattern(self) -> None:
-        pattern = Pattern(input_nodes=range(5))
+        pattern = Pattern(input_nodes=list(range(5)))
         pattern.add(command.M(node=0))
         pattern.add(command.M(node=1))
-        pattern.add(command.M(node=2, s_domain=(0,), t_domain=(1,)))
-        pattern.add(command.X(node=3, domain=(2,)))
-        pattern.add(command.Z(node=4, domain=(3,)))
+        pattern.add(command.M(node=2, s_domain={0}, t_domain={1}))
+        pattern.add(command.X(node=3, domain={2}))
+        pattern.add(command.Z(node=4, domain={3}))
         xflow, zflow = get_corrections_from_pattern(pattern)
         assert xflow == {0: {2}, 2: {3}}
         assert zflow == {1: {2}, 3: {4}}
diff --git a/tests/test_pattern.py b/tests/test_pattern.py
index 4c48df5..9b069b1 100644
--- a/tests/test_pattern.py
+++ b/tests/test_pattern.py
@@ -378,11 +378,11 @@ class TestPattern:
         pattern = Pattern(input_nodes=[0])
         for i in (1, 2, 3):
             pattern.add(N(node=i))
-            pattern.add(E(nodes=[0, i]))
+            pattern.add(E(nodes=(0, i)))
         pattern.add(M(node=0, angle=0.5))
         pattern.add(M(node=1, angle=0.5))
-        pattern.add(M(node=2, angle=0.5, plane=plane, s_domain=[0], t_domain=[1]))
-        pattern.add(Z(node=3, domain=[2]))
+        pattern.add(M(node=2, angle=0.5, plane=plane, s_domain={0}, t_domain={1}))
+        pattern.add(Z(node=3, domain={2}))
         pattern_ref = copy.deepcopy(pattern)
         pattern.standardize(method="global")
         signal_dict = pattern.shift_signals(method=method)
diff --git a/tests/test_statevec.py b/tests/test_statevec.py
index 640ad3d..83c7ca3 100644
--- a/tests/test_statevec.py
+++ b/tests/test_statevec.py
@@ -66,7 +66,7 @@ class TestStatevec:
         # tensor of same state
         rand_angle = fx_rng.random() * 2 * np.pi
         rand_plane = fx_rng.choice(np.array([i for i in graphix.pauli.Plane]))
-        state = PlanarState(plane=rand_plane, angle=rand_angle)
+        state = PlanarState(rand_plane, rand_angle)
         vec = Statevec(nqubit=nqb, data=state)
         sv_list = [state.get_statevector() for _ in range(nqb)]
         sv = functools.reduce(np.kron, sv_list)
diff --git a/tests/test_statevec_backend.py b/tests/test_statevec_backend.py
index e626cc6..bf1b344 100644
--- a/tests/test_statevec_backend.py
+++ b/tests/test_statevec_backend.py
@@ -80,7 +80,7 @@ class TestStatevecNew:
         # random planar state
         rand_angle = fx_rng.random() * 2 * np.pi
         rand_plane = fx_rng.choice(np.array([i for i in graphix.pauli.Plane]))
-        state = PlanarState(plane=rand_plane, angle=rand_angle)
+        state = PlanarState(rand_plane, rand_angle)
         backend = StatevectorBackend()
         backend.add_nodes(hadamardpattern.input_nodes, data=state)
         vec = Statevec(nqubit=1, data=state)
@@ -94,8 +94,8 @@ class TestStatevecNew:
         rand_angle = fx_rng.random(2) * 2 * np.pi
         rand_plane = fx_rng.choice(np.array([i for i in graphix.pauli.Plane]), 2)
 
-        state = PlanarState(plane=rand_plane[0], angle=rand_angle[0])
-        state2 = PlanarState(plane=rand_plane[1], angle=rand_angle[1])
+        state = PlanarState(rand_plane[0], rand_angle[0])
+        state2 = PlanarState(rand_plane[1], rand_angle[1])
         with pytest.raises(ValueError):
             StatevectorBackend().add_nodes(hadamardpattern.input_nodes, data=[state, state2])