Source code for blueqat.circuit

# Copyright 2019-2026 The Blueqat Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module defines Circuit and the setting for circuit.
Modernized for PyTorch Tensor Network backend integration in 2026.
"""

import warnings
from functools import partial, update_wrapper
import typing
from typing import cast, Any, Callable, Dict, Optional, Tuple, Type

import torch

from . import gate
from .gateset import get_op_type, register_operation, unregister_operation
from .typing import CircuitOperation

if typing.TYPE_CHECKING:
    from .gate import Operation
    from .backends.backendbase import Backend
    BackendUnion = typing.Union[None, str, Backend]

GLOBAL_MACROS = {}


[docs] class Circuit: """Store the gate operations and call the backends.""" def __init__(self, n_qubits: int = 0, ops: Optional[list] = None): self.ops = ops or [] self._backends: Dict[str, 'Backend'] = {} self.n_qubits = n_qubits def __repr__(self): return f'Circuit({self.n_qubits}).' + '.'.join( str(op) for op in self.ops) def __get_backend(self, backend_name): from blueqat.backends import BACKENDS from blueqat.backends.backendbase import _BACKEND_REGISTRY, get_backend try: return self._backends[backend_name] except KeyError: backend = BACKENDS.get(backend_name) if backend is not None: # インスタンス化してキャッシュ(型でもファクトリlambdaでも呼び出しは同じ) self._backends[backend_name] = backend() return self._backends[backend_name] if backend_name in _BACKEND_REGISTRY: # register_backend()経由で登録されたプラグインバックエンド self._backends[backend_name] = get_backend(backend_name) return self._backends[backend_name] raise ValueError(f"Backend {backend_name} doesn't exist.") def __backend_runner_wrapper(self, backend_name: str) -> Callable: backend = self.__get_backend(backend_name) def runner(*args, **kwargs): return backend.run(self.ops, self.n_qubits, *args, **kwargs) return runner def __getattr__(self, name: str) -> CircuitOperation[Any]: op_type = get_op_type(name) if op_type: return _GateWrapper(self, op_type) if name in GLOBAL_MACROS: macro = update_wrapper(partial(GLOBAL_MACROS[name], self), GLOBAL_MACROS[name]) return cast(CircuitOperation[Any], macro) if name.startswith("run_with_"): # メソッド内部で遅延インポート from blueqat.backends import BACKENDS from blueqat.backends.backendbase import _BACKEND_REGISTRY backend_name = name[9:] if backend_name in BACKENDS or backend_name in _BACKEND_REGISTRY: return self.__backend_runner_wrapper(backend_name) raise AttributeError(f"Backend '{backend_name}' does not exist.") raise AttributeError( f"'Circuit' object has no attribute or gate '{name}'") def __add__(self, other: 'Circuit') -> 'Circuit': if not isinstance(other, Circuit): return NotImplemented c = self.copy() c += other return c def __iadd__(self, other: 'Circuit') -> 'Circuit': if not isinstance(other, Circuit): return NotImplemented self.ops += other.ops self.n_qubits = max(self.n_qubits, other.n_qubits) return self
[docs] def copy(self, copy_backends: bool = True) -> 'Circuit': """Copy the circuit.""" copied = Circuit(self.n_qubits, self.ops.copy()) if copy_backends: copied._backends = {k: v.copy() for k, v in self._backends.items()} return copied
[docs] def dagger(self, ignore_measurement: bool = False) -> 'Circuit': """Make Hermitian conjugate of the circuit. If the circuit contains measurement or reset (which have no Hermitian conjugate), ValueError is raised, unless `ignore_measurement` is True, in which case those operations are simply dropped.""" ops = [] for g in reversed(self.ops): if not hasattr(g, 'dagger'): if ignore_measurement: continue raise ValueError( 'Cannot make the Hermitian conjugate of this circuit because ' f'the circuit contains a non-invertible operation `{g.lowername}`.') ops.append(g.dagger()) copied = Circuit(self.n_qubits, ops) return copied
[docs] def run(self, backend: Optional[str] = None, *args, **kwargs) -> Any: """Run the circuit. Passes parameters to the PyTorch-based backend.""" from blueqat.backends import BACKENDS, DEFAULT_BACKEND_NAME if backend is None: backend = self.__get_backend(DEFAULT_BACKEND_NAME) elif isinstance(backend, str): backend = self.__get_backend(backend) return backend.run(self.ops, self.n_qubits, *args, **kwargs)
[docs] def to_qasm(self, output_prologue: bool = True) -> str: """Convert this circuit into an OpenQASM 2.0 program string.""" from blueqat.backends.qasm_output_backend import QasmOutputBackend return QasmOutputBackend().run(self.ops, self.n_qubits, output_prologue=output_prologue)
[docs] def statevector(self, backend: 'BackendUnion' = None, **kwargs) -> torch.Tensor: """Run the circuit and get a statevector as a PyTorch Tensor to keep gradients intact.""" from blueqat.backends import DEFAULT_BACKEND_NAME if kwargs.get('returns'): raise ValueError('Circuit.statevector has no argument `returns`.') if backend is None: backend = self.__get_backend(DEFAULT_BACKEND_NAME) elif isinstance(backend, str): backend = self.__get_backend(backend) if hasattr(backend, 'statevector'): return backend.statevector(self.ops, self.n_qubits, **kwargs) return backend.run(self.ops, self.n_qubits, returns='statevector', **kwargs)
[docs] def shots(self, shots: int, backend: 'BackendUnion' = None, **kwargs) -> typing.Counter[str]: """Run the circuit and get shot counts as a result.""" from blueqat.backends import DEFAULT_BACKEND_NAME if kwargs.get('returns'): raise ValueError('Circuit.shots has no argument `returns`.') if backend is None: backend = self.__get_backend(DEFAULT_BACKEND_NAME) elif isinstance(backend, str): backend = self.__get_backend(backend) if hasattr(backend, 'shots'): return backend.shots(self.ops, self.n_qubits, shots=shots, **kwargs) return backend.run(self.ops, self.n_qubits, shots=shots, returns='shots', **kwargs)
[docs] def oneshot(self, backend: 'BackendUnion' = None, **kwargs) -> Tuple[torch.Tensor, str]: """Run the circuit once and return the post-measurement statevector together with the single measured bitstring.""" from blueqat.backends import DEFAULT_BACKEND_NAME if kwargs.get('returns'): raise ValueError('Circuit.oneshot has no argument `returns`.') if backend is None: backend = self.__get_backend(DEFAULT_BACKEND_NAME) elif isinstance(backend, str): backend = self.__get_backend(backend) vec, cnt = backend.run(self.ops, self.n_qubits, shots=1, returns='statevector_and_shots', **kwargs) return vec, next(iter(cnt))
def _expanded_applications(self, ops: Optional[list] = None): """Yield (lowername, qubit-tuple) for each atomic gate application, expanding slices/multi-targets (and recursing into named blocks) the same way the backends do.""" from .gate import (Barrier, Gate, GateBlock, Measurement, OneQubitGate, Reset, TwoQubitGate) n_qubits = self.n_qubits for op in (self.ops if ops is None else ops): if isinstance(op, GateBlock): yield from self._expanded_applications(op.ops) elif isinstance(op, Barrier): yield op.lowername, tuple(op.target_iter(n_qubits)) elif isinstance(op, (OneQubitGate, Measurement, Reset)): for t in op.target_iter(n_qubits): yield op.lowername, (t, ) elif isinstance(op, TwoQubitGate): for c, t in op.control_target_iter(n_qubits): yield op.lowername, (c, t) elif isinstance(op, Gate): yield op.lowername, tuple(op.targets) else: yield op.lowername, tuple(op.target_iter(n_qubits))
[docs] def depth(self) -> int: """Circuit depth: length of the longest gate sequence on any qubit path, counting each expanded gate application (as in Qiskit). Barriers don't add depth.""" depths = [0] * self.n_qubits for name, qubits in self._expanded_applications(): if name == 'barrier' or not qubits: continue d = max(depths[q] for q in qubits) + 1 for q in qubits: depths[q] = d return max(depths, default=0)
[docs] def count_ops(self) -> typing.Counter[str]: """Count expanded gate applications by name (as in Qiskit's count_ops).""" import collections return collections.Counter(name for name, _ in self._expanded_applications())
[docs] def probs(self, qubits: Optional[typing.Sequence[int]] = None, backend: 'BackendUnion' = None, **kwargs) -> torch.Tensor: """Measurement probabilities of the circuit's final state, optionally marginalized onto `qubits` (as in PennyLane's `qml.probs`). Returns a tensor of length 2**len(qubits) where index bit j is the outcome of `qubits[j]` (the first listed qubit is the least-significant bit, matching the SDK-wide convention). Differentiable.""" state = self.statevector(backend, **kwargs) p = torch.abs(state) ** 2 if qubits is None: return p keep = list(qubits) if len(set(keep)) != len(keep): raise ValueError('qubits must not contain duplicates.') n = self.n_qubits if any(not 0 <= q < n for q in keep): raise ValueError(f'qubits must be in range(0, {n}).') # After reshape, axis k corresponds to qubit n-1-k (the statevector # index has qubit 0 as its least-significant bit). t = p.reshape((2, ) * n) keep_set = set(keep) sum_axes = [n - 1 - q for q in range(n) if q not in keep_set] if sum_axes: t = t.sum(dim=sum_axes) remaining = [q for q in reversed(range(n)) if q in keep_set] # reshape(-1) makes the first axis most significant, so order axes as # [last listed qubit, ..., first listed qubit]. t = t.permute([remaining.index(q) for q in reversed(keep)]) return t.reshape(-1)
[docs] def expect(self, hamiltonian: Any, backend: 'BackendUnion' = None, **kwargs) -> torch.Tensor: """Expectation value <psi|H|psi> of a Pauli-expression Hamiltonian on the circuit's final state. Differentiable.""" if hasattr(hamiltonian, 'to_expr'): hamiltonian = hamiltonian.to_expr().simplify() return self.run(backend, hamiltonian=hamiltonian, **kwargs)
[docs] def block(self, name: str) -> '_BlockContext': """Group the operations appended inside the `with` body into a named, nestable block (as in the sub-circuits of Shor's algorithm): c = Circuit(4) with c.block("QFT"): c.h[0].cphase(math.pi / 2)[0, 1] ... Blocks change nothing about execution -- every backend transparently sees the inner gates -- but the structure is kept in `repr()`, `Circuit.tree()`, and survives `dagger()` (as a mirrored block named `name + '†'`).""" return _BlockContext(self, name)
[docs] def append_block(self, name: str, subcircuit: 'Circuit', offset: int = 0) -> 'Circuit': """Append an existing circuit as a named block. `offset` shifts every qubit index of `subcircuit`, so a library circuit built on qubits 0..k can be placed anywhere. Shifting resolves slice targets against `subcircuit.n_qubits` and preserves any nested block structure inside `subcircuit`.""" from .circuit_funcs.flatten import flatten from .gate import GateBlock if offset < 0: raise ValueError('offset must not be negative.') n_sub = subcircuit.n_qubits def _shift_ops(ops: list) -> list: out = [] for op in ops: if isinstance(op, GateBlock): out.append(GateBlock(op.name, _shift_ops(op.ops))) continue # flatten a single op to resolve slices into explicit targets for atom in flatten(Circuit(n_sub, [op])).ops: targets = atom.targets if isinstance(targets, int): shifted: Any = targets + offset else: shifted = tuple(t + offset for t in targets) options = None if getattr(atom, 'key', None) is not None: options = {'key': atom.key} if atom.duplicated is not None: options['duplicated'] = atom.duplicated out.append(atom.create(shifted, atom.params, options)) return out if offset == 0: ops = [op for op in subcircuit.ops] else: ops = _shift_ops(subcircuit.ops) width = n_sub + offset self.ops.append(GateBlock(name, ops)) self.n_qubits = max(self.n_qubits, width) return self
[docs] def tree(self) -> str: """A text rendering of the circuit's nested block structure: Circuit(4) ├─ h[0] └─ QFT ├─ cphase(1.5708)[0, 1] └─ ... """ from .gate import GateBlock def _lines(ops, prefix: str): out = [] for i, op in enumerate(ops): last = i == len(ops) - 1 branch = '└─ ' if last else '├─ ' cont = ' ' if last else '│ ' if isinstance(op, GateBlock): out.append(f'{prefix}{branch}{op.name}') out.extend(_lines(op.ops, prefix + cont)) else: out.append(f'{prefix}{branch}{op}') return out return '\n'.join([f'Circuit({self.n_qubits})'] + _lines(self.ops, ''))
[docs] def ancilla(self, n: int = 1, pos: Optional[int] = None, stop: Optional[int] = None, reset: bool = True) -> '_AncillaContext': """Context manager allocating temporary ancilla qubit(s) for use inside the `with` block. By default, appends `n` fresh qubits past the circuit's current width: with c.ancilla() as a: c.cx[0, a[0]] `pos`/`stop` instead pin the ancilla range to specific qubit indices (`range(pos, stop)`; `stop` defaults to `pos + n`): with c.ancilla(pos=4, stop=6, reset=True) as a: c.cx[3, a[0]] If `reset` is true (the default), a `reset` gate is appended for each ancilla qubit on exiting the block, so they're back at ``|0>`` and safe to reuse elsewhere in the circuit. """ if pos is not None: indices = list(range(pos, stop if stop is not None else pos + n)) self.n_qubits = max(self.n_qubits, (max(indices) + 1) if indices else 0) else: indices = list(range(self.n_qubits, self.n_qubits + n)) self.n_qubits += n return _AncillaContext(self, indices, reset)
class _BlockContext: """Context manager returned by `Circuit.block()`. On exit, the operations appended inside the body are wrapped into a single named GateBlock (supports nesting: an inner block closes before its enclosing one).""" def __init__(self, circuit: Circuit, name: str) -> None: self.circuit = circuit self.name = name self._start = 0 def __enter__(self) -> '_BlockContext': self._start = len(self.circuit.ops) return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: if exc_type is not None: return from .gate import GateBlock inner = self.circuit.ops[self._start:] del self.circuit.ops[self._start:] self.circuit.ops.append(GateBlock(self.name, inner)) class _AncillaContext: """Context manager returned by `Circuit.ancilla()`. See that method's docstring.""" def __init__(self, circuit: Circuit, indices: list, reset: bool) -> None: self.circuit = circuit self.indices = indices self.reset = reset def __getitem__(self, i: int) -> int: return self.indices[i] def __len__(self) -> int: return len(self.indices) def __iter__(self): return iter(self.indices) def __enter__(self) -> '_AncillaContext': return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: if self.reset and exc_type is None: for idx in self.indices: self.circuit.reset[idx] class _GateWrapper(CircuitOperation[Circuit]): def __init__(self, circuit: Circuit, op_type: Type['Operation']): self.circuit = circuit self.op_type = op_type self.params = () self.options = None def __call__(self, *args, **kwargs) -> '_GateWrapper': self.params = args if kwargs: self.options = kwargs return self def __getitem__(self, targets) -> 'Circuit': self.circuit.ops.append( self.op_type.create(targets, self.params, self.options)) self.circuit.n_qubits = max( gate.get_maximum_index(targets) + 1, self.circuit.n_qubits) return self.circuit def __str__(self) -> str: args_str = str(self.params) if self.params else "" if self.options: args_str += str(self.options) return self.op_type.lowername + args_str
[docs] class BlueqatGlobalSetting: """Setting for Blueqat."""
[docs] @staticmethod def register_macro(name: str, func: Callable, allow_overwrite: bool = False) -> None: """Register new macro to Circuit.""" if hasattr(Circuit, name): if allow_overwrite: warnings.warn(f"Circuit has attribute `{name}`.") else: raise ValueError(f"Circuit has attribute `{name}`.") if name.startswith("run_with_"): if allow_overwrite: warnings.warn(f"Gate name `{name}` may conflict with run of backend.") else: raise ValueError(f"Gate name `{name}` shall not start with 'run_with_'.") if not allow_overwrite: if get_op_type(name) is not None: raise ValueError(f"Gate '{name}' already exists in gate set.") if name in GLOBAL_MACROS: raise ValueError(f"Macro '{name}' already exists.") GLOBAL_MACROS[name] = func
[docs] @staticmethod def unregister_macro(name: str) -> None: """Unregister a macro.""" if name not in GLOBAL_MACROS: raise ValueError(f"Macro '{name}' is not registered.") del GLOBAL_MACROS[name]
[docs] @staticmethod def register_gate(name: str, gateclass: Type['Operation'], allow_overwrite: bool = False) -> None: """Register new gate to gate set.""" if hasattr(Circuit, name): if allow_overwrite: warnings.warn(f"Circuit has attribute `{name}`.") else: raise ValueError(f"Circuit has attribute `{name}`.") if name.startswith("run_with_"): if allow_overwrite: warnings.warn(f"Gate name `{name}` may conflict with run of backend.") else: raise ValueError(f"Gate name `{name}` shall not start with 'run_with_'.") if not allow_overwrite: if get_op_type(name) is not None: raise ValueError(f"Gate '{name}' already exists in gate set.") if name in GLOBAL_MACROS: raise ValueError(f"Macro '{name}' already exists.") register_operation(name, gateclass)
[docs] @staticmethod def unregister_gate(name: str) -> None: """Unregister a gate from gate set.""" if get_op_type(name) is None: raise ValueError(f"Gate '{name}' is not registered.") unregister_operation(name)
[docs] @staticmethod def register_backend(name: str, backend: Type['Backend'], allow_overwrite: bool = False) -> None: """Register new backend.""" from blueqat.backends import BACKENDS if hasattr(Circuit, "run_with_" + name): if allow_overwrite: warnings.warn(f"Circuit has attribute `run_with_{name}`.") else: raise ValueError(f"Circuit has attribute `run_with_{name}`.") if not allow_overwrite and name in BACKENDS: raise ValueError(f"Backend '{name}' is already registered.") BACKENDS[name] = backend
[docs] @staticmethod def unregister_backend(name: str) -> None: """Unregister a backend.""" from blueqat.backends import BACKENDS if name not in BACKENDS: raise ValueError(f"Backend '{name}' is not registered.") del BACKENDS[name]
[docs] @staticmethod def set_default_backend(name: str) -> None: """Set the default backend to be used by `Circuit`.""" from blueqat.backends import BACKENDS if name not in BACKENDS: raise ValueError(f"Backend '{name}' is not registered.") # モジュール参照経由でグローバル変数を書き換える import blueqat.backends blueqat.backends.DEFAULT_BACKEND_NAME = name
[docs] @staticmethod def get_default_backend_name() -> str: """Get the default backend name.""" from blueqat.backends import DEFAULT_BACKEND_NAME return DEFAULT_BACKEND_NAME