circuit/index.py
class Mode
Execution mode for the circuit: statevector (VECTOR), density-matrix (MATRIX), noisy density-matrix (NOISY), or statevector quantum trajectories (TRAJECTORY).
| name | type | default |
|---|---|---|
| MATRIX | None | 'matrix' |
| NOISY | None | 'noisy' |
| TRAJECTORY | None | 'trajectory' |
| VECTOR | None | 'vector' |
class Frame
Lightweight record of a circuit operation (gate name, target indices, local dims, and parameters).
| name | type | default |
|---|---|---|
| dim | U[int, Array] | None |
| index | List[int] | None |
| name | str | None |
| params | Dict[str, Any] | None |
init
Create an operation frame describing a gate acting on index with given local dim and params.
def __init__(self: Any, dim: U[int, Array], index: List[int], name: str, params: Optional[Dict[str, Any]]) -> AnyImplementation
def __init__(self, dim: U[int, Array], index: List[int], name: str, params: Optional[Dict[str, Any]]=None):
self.index = index
self.dim = dim
self.name = name
self.params = params or {}parse
Extract a small, human-readable parameter subset (e.g. angles/indices) for display/logging.
def parse(kwargs: Dict[str, Any]) -> Dict[str, Any] [static]Implementation
def parse(kwargs: Dict[str, Any]) -> Dict[str, Any]:
valid = ['i', 'j', 'k', 'angle', 'type']
params: Dict[str, Any] = {}
for key in valid:
if key in kwargs:
val = kwargs[key]
if isinstance(val, torch.Tensor):
val = val.item()
if isinstance(val, float):
val = round(val, 4)
params[key] = val
return paramscreate
Create a Frame from a gate-like input, inferring a display name and capturing key params.
def create(gate_in: Any, dims: List[int], index: List[int]) -> 'Frame' [static]Implementation
def create(gate_in: Any, dims: List[int], index: List[int], **kwargs: Any) -> 'Frame':
params = Frame.parse(kwargs)
name = ''
if hasattr(gate_in, 'name'):
name = gate_in.name
elif hasattr(gate_in, '__name__'):
name = gate_in.__name__
else:
name = 'U'
return Frame(dim=dims, index=index, name=name, params=params)class Circuit
A qudit circuit as an nn.Module applying a sequence of embedded unitaries. In VECTOR (default) mode it applies
whereas in MATRIX mode it applies the unitary channel,
| name | type | default |
|---|---|---|
| circuit | nn.Sequential | None |
| device | str | None |
| dim | U[int, Array] | None |
| dims_ | List[int] | None |
| flip | bool | None |
| gate_gen | Any | None |
| gates | Dict[int, Gategen] | None |
| mode | Mode | None |
| noise_model | Optional[Any] | None |
| operations | List[Frame] | None |
| width | int | None |
| wires | int | None |
init
Initialize a circuit with wires subsystems of local dimension(s) dim on device.
def __init__(self: Any, wires: int, dim: U[int, Array], device: str, mode: U[Mode, str], flip: bool, noise: Optional[Any]) -> AnyImplementation
def __init__(self, wires: int=2, dim: U[int, Array]=2, device: str='cpu', mode: U[Mode, str]=Mode.VECTOR, flip: bool=False, noise: Optional[Any]=None):
super(Circuit, self).__init__()
if isinstance(mode, str):
mode = mode.lower()
self.mode = Mode(mode)
if isinstance(dim, int):
self.dims_ = [dim] * wires
elif isinstance(dim, list):
if len(dim) != wires:
raise ValueError(f'Dim list {len(dim)} != wires {wires}.')
self.dims_ = dim
self.dim = dim
self.width = int(torch.prod(torch.tensor(self.dims_)).item())
self.wires = wires
self.device = device
self.flip = flip
self.circuit = nn.Sequential()
self.operations = []
udits = sorted(list(set(self.dims_)))
self.gates = {}
for d in udits:
self.gates[d] = Gategen(dim=d, device=device)
if isinstance(self.dim, int):
self.gate_gen = self.gates[self.dim]
if self.mode in (Mode.NOISY, Mode.TRAJECTORY):
if noise is None:
raise ValueError(f'Circuit(mode={mode!r}) requires a noise model.')
self.noise_model = noise
self._noise_pairs: List[List] = []
self._noise_module_list = nn.ModuleList()
else:
self.noise_model = Nonegate
Append a gate acting on index to the circuit with optionally params
Operator may be of type Gate, Operator, torch.Tensor, or a callable factory for an embedded Gate.
The gate is added to the circuit as an nn.Module and a Frame is recorded for display/logging.
def gate(self: Any, gate_in: Any, index: Any) -> NoneImplementation
def gate(self, gate_in: Any, index: Any, **kwargs: Any) -> None:
if 'device' not in kwargs:
kwargs['device'] = self.device
idx_list = index if isinstance(index, list) else [index]
dims = [self.dims_[i] for i in idx_list]
self.operations.append(Frame.create(gate_in, dims, idx_list, **kwargs))
Instance: Any = None
if isinstance(gate_in, (torch.Tensor, Operator)):
Instance = Gate(matrix=gate_in, index=idx_list, wires=self.wires, dim=self.dims_, device=self.device)
elif callable(gate_in):
Instance = gate_in(dim=self.dims_, wires=self.wires, index=idx_list, **kwargs)
else:
raise TypeError(f'Unsupported gate input: {type(gate_in)}')
pos = str(len(self.circuit))
self.circuit.add_module(pos, Instance)
if self.mode in (Mode.NOISY, Mode.TRAJECTORY):
noise = self.noise_model
gate_name = self.operations[-1].name
generator = getattr(noise, 'generator', None)
if isinstance(noise, CoherentNoise):
d_local = Instance.target_size
Instance.U = noise.perturb(Instance.U, d_local).to(dtype=C64)
self._noise_pairs.append([])
else:
pair = []
for wire in idx_list:
d = self.dims_[wire]
K = noise.kraus_for(gate_name, wire, d).to(dtype=C64, device=self.device)
if self.mode == Mode.NOISY:
ng = NoisyGate(K=K, index=[wire], wires=self.wires, dims=self.dims_, generator=generator)
else:
ng = TrajectoryGate(K=K, index=[wire], wires=self.wires, dims=self.dims_, generator=generator)
pair.append(ng)
self._noise_module_list.append(ng)
self._noise_pairs.append(pair)forward
Apply the circuit to a statevector or density matrix depending on self.mode.
def forward(self: Any, x: Any) -> torch.TensorImplementation
def forward(self, x: Any) -> torch.Tensor:
if isinstance(x, np.ndarray):
x = torch.from_numpy(x).to(dtype=C64, device=self.device)
elif not isinstance(x, torch.Tensor):
x = torch.tensor(x, dtype=C64, device=self.device)
else:
x = x.to(dtype=C64, device=self.device)
if self.mode == Mode.VECTOR:
return self.circuit(x)
W = self.width
if self.mode == Mode.TRAJECTORY:
psi = x.reshape(W)
for gate, noise_gates in zip(self.circuit, self._noise_pairs):
psi = gate(psi)
for ng in noise_gates:
psi = ng(psi.reshape(W))
return psi.reshape(W)
if x.dim() == 1 or (x.dim() == 2 and min(x.shape) == 1):
psi = x.reshape(W, 1)
rho = psi @ psi.conj().T
else:
rho = x
if self.mode == Mode.NOISY:
for gate, noise_gates in zip(self.circuit, self._noise_pairs):
rho = gate.forwardd(rho)
for ng in noise_gates:
rho = ng.forwardd(rho)
return rho
for module in self.circuit:
rho = module.forwardd(rho)
return rhomatrix
Materialize the full unitary matrix by acting on the computational basis vectors.
def matrix(self: Any, littleEndian: Any) -> torch.TensorImplementation
def matrix(self, littleEndian=False) -> torch.Tensor:
W = self.width
I = torch.eye(W, dtype=C64, device=self.device)
cols = []
for i in range(W):
res = self.circuit(I[i])
if littleEndian:
res = LittleEndian(res, self.dims_)
cols.append(res.reshape(-1, 1))
return torch.cat(cols, dim=1)expectation
Compute forward(state).
def expectation(self: Any, operator: Any, state: torch.Tensor) -> torch.TensorImplementation
def expectation(self, operator: Any, state: torch.Tensor) -> torch.Tensor:
out = self.forward(state)
if not isinstance(operator, torch.Tensor):
op = torch.tensor(operator, dtype=C64, device=self.device)
else:
op = operator.to(dtype=C64, device=self.device)
if self.mode == Mode.VECTOR:
psi = out.reshape(-1)
return torch.real(psi.conj() @ (op @ psi))
else:
return torch.real(torch.trace(op @ out))sample
Sample shots bitstring outcomes from dict[bitstring, count].
def sample(self: Any, state: torch.Tensor, shots: int) -> dictImplementation
def sample(self, state: torch.Tensor, shots: int=1024) -> dict:
out = self.forward(state)
if self.mode == Mode.VECTOR:
probs = out.reshape(-1).abs().pow(2).real.float()
else:
probs = torch.diag(out).real.float()
indices = torch.multinomial(probs, num_samples=shots, replacement=True)
result: dict = {}
for idx in indices.tolist():
bits = []
n = idx
for d in reversed(self.dims_):
bits.append(str(n % d))
n //= d
key = ''.join(reversed(bits))
result[key] = result.get(key, 0) + 1
return resultcut
Mark a gate position as a circuit cut using the given Coupler.
index = [c_A, c_B]: the two wires the cut gate acts on. split: wire index where partition A ends and B begins (wires 0..split-1 → A, wires split..n-1 → B). For adjacent cuts (c_B == c_A + 1) split defaults to c_B. For non-adjacent gates split must be given explicitly. The gate is NOT added to the circuit — it is replaced by the Coupler decomposition when run_cut() is called.
def cut(self: Any, coupler: Any, index: Any, split: Optional[int]) -> NoneImplementation
def cut(self, coupler: Any, index: Any, split: Optional[int]=None) -> None:
if not hasattr(self, '_cuts'):
self._cuts: list = []
idx = index if isinstance(index, list) else list(index)
c_A, c_B = idx
if split is None:
if c_B != c_A + 1:
raise ValueError(f'Cut wires {c_A} and {c_B} are not adjacent — provide split= to specify where partition A ends and B begins.')
split = c_B
if not c_A < split <= c_B:
raise ValueError(f'split={split} must satisfy c_A < split <= c_B ({c_A} < split <= {c_B}).')
self._cuts.append((coupler, idx, split, len(self.circuit)))run_cut
Execute the circuit using circuit cutting at the marked cut position.
Partitions wires at the cut: partition A = wires 0..split-1, partition B = wires split..end. Gates on each partition are split into pre- and post-cut segments; sub-circuits run as pre → cut_op → post. The joint probability distribution is reconstructed via amplitude superposition over the Coupler decomposition.
def run_cut(self: Any) -> dictImplementation
def run_cut(self) -> dict:
from ..cut.utils import matrix_of
if self.mode == Mode.TRAJECTORY:
raise NotImplementedError('Circuit cutting is not supported for TRAJECTORY mode — stochastic collapse per term makes amplitude reconstruction invalid.')
cuts = getattr(self, '_cuts', [])
if not cuts:
raise ValueError('No cuts defined — call cut() before run_cut().')
if len(cuts) > 1:
from ..cut.multi import run_multi
return run_multi(cuts, self.wires, self.dims_, self.circuit, self.device)
coupler, [c_A, c_B], split, cut_pos = cuts[0]
partA = set(range(0, split))
partB = set(range(split, self.wires))
dims_A = [self.dims_[w] for w in sorted(partA)]
dims_B = [self.dims_[w] for w in sorted(partB)]
preA, postA = ([], [])
preB, postB = ([], [])
for i, gate in enumerate(self.circuit.children()):
hasA = any((w in partA for w in gate.index))
hasB = any((w in partB for w in gate.index))
if hasA and hasB:
raise ValueError(f'Gate on wires {gate.index} spans both partitions. Only the marked cut gate may cross the partition boundary.')
if hasA:
(preA if i < cut_pos else postA).append((matrix_of(gate), gate.index))
elif hasB:
(preB if i < cut_pos else postB).append((matrix_of(gate), [w - split for w in gate.index]))
device = self.device
def subA(op):
qc = Circuit(len(partA), dim=dims_A, device=device)
for U, idx in preA:
qc.gate(U, idx)
qc.gate(op, [c_A])
for U, idx in postA:
qc.gate(U, idx)
return qc
def subB(op):
qc = Circuit(len(partB), dim=dims_B, device=device)
for U, idx in preB:
qc.gate(U, idx)
qc.gate(op, [c_B - split])
for U, idx in postB:
qc.gate(U, idx)
return qc
return coupler.run(subA, subB)draw
Render a simple textual diagram of the circuit; currently supports mode='ascii'.
def draw(self: Any, mode: str) -> AnyImplementation
def draw(self, mode: str='ascii') -> Any:
from .transform import Table
table = Table(self)
if mode == 'ascii':
return table.draw(self)
return table.draw(self)