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 = {}




def _reverse_index_bits(values: torch.Tensor, n_qubits: int) -> torch.Tensor:
    """Re-index so that qubit 0 is the most-significant bit of the index.

    The values are unchanged; only which index they sit at moves. On a
    symmetric distribution this is the identity, which is exactly why reading
    the convention backwards can go unnoticed.
    """
    if n_qubits <= 1:
        return values
    index = torch.arange(values.shape[0], device=values.device)
    swapped = torch.zeros_like(index)
    for bit in range(n_qubits):
        swapped |= ((index >> bit) & 1) << (n_qubits - 1 - bit)
    return values[swapped]


def _as_pauli_expression(hamiltonian: Any) -> Any:
    """A Pauli expression, or a TypeError saying what one looks like.

    Left as it is when it already is one. A string is parsed. Anything else --
    a dict of coefficients is the common guess -- gets an error naming the
    thing to pass, because the alternative is an AttributeError about
    `simplify` from three frames down, which says nothing about Hamiltonians.
    """
    if hasattr(hamiltonian, 'to_expr'):
        return hamiltonian.to_expr().simplify()
    if hasattr(hamiltonian, 'simplify'):
        return hamiltonian
    if isinstance(hamiltonian, str):
        from .utils import parse_hamiltonian
        return parse_hamiltonian(hamiltonian).simplify()
    raise TypeError(
        f"a Hamiltonian is a Pauli expression, not {type(hamiltonian).__name__}. "
        f"Build one from the operators exported at the top level -- "
        f"`from blueqat import Z, X; Z[0] + 0.5 * X[1]` -- or pass the same "
        f"thing as a string, \"Z[0] + 0.5*X[1]\".")


def _hamiltonian_width(hamiltonian: Any) -> int:
    """How many qubits a Pauli expression names, or 0 if it names none."""
    highest = -1
    for term in getattr(hamiltonian, 'terms', ()):
        for op in getattr(term, 'ops', ()):
            index = getattr(op, 'n', None)
            if index is not None:
                highest = max(highest, int(index))
    return highest + 1


[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. Beyond the backend's own arguments (``shots``, ``returns``, ``mode``, ``hamiltonian``, ``amplitude``, ``initial``, ...), two arguments shape sampled results: ``seed`` Fix every random draw of this run -- shot sampling, mid-circuit collapse and large-``n`` perfect sampling -- so that the same circuit and seed give the same counts. It drives a private ``torch.Generator``, leaving the global RNG untouched. ``bit_order`` Layout of the counts keys: ``'q0_last'`` (the default, and blueqat's long-standing order, where ``key[-1]`` is qubit 0) or ``'q0_first'``, where ``key[i]`` is qubit i, as cloud APIs report it. Keys are zero-padded to ``n_qubits`` in either order. """ return self._resolve_backend(backend, kwargs).run( self.ops, self.n_qubits, *args, **kwargs)
def _resolve_backend(self, backend: 'BackendUnion', kwargs: dict) -> 'Backend': """The backend a call should go to, given what it was asked for. Noise needs a density matrix, which the default backends do not carry, so asking for noise is also a choice of backend. Quasi-static noise counts too: its result is an average over frozen detunings, which is a mixture. Every entry point routes through here. When only `run` did, the same request written as `shots(...)` or `probs(...)` reached a backend that does not know the argument and dropped it, returning a noiseless answer with no error -- the failure this exists to prevent. """ from blueqat.backends import DEFAULT_BACKEND_NAME if backend is not None: return self.__get_backend(backend) if isinstance(backend, str) else backend noisy = (kwargs.get('noise') is not None or kwargs.get('quasi_static') is not None or kwargs.get('noise_scale') is not None) return self.__get_backend('density' if noisy else DEFAULT_BACKEND_NAME)
[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, bit_order: str = 'q0_last', **kwargs) -> torch.Tensor: """Run the circuit and get a statevector as a PyTorch Tensor to keep gradients intact. Amplitude ``v[k]`` belongs to the basis state whose qubit ``q`` is bit ``q`` of ``k`` -- qubit 0 is the *least*-significant bit of the index, the same convention as everywhere else in the SDK and as `blueqat.BIT_ORDER` reports. So for two qubits the order is |00>, |01> with qubit 0 set, |10> with qubit 1 set, |11>. Reading it the other way round is a mistake nothing catches: it gives the mirror image of the answer, and on a symmetric state -- a GHZ, a W, anything permutation-invariant -- the two agree, so it can go unnoticed through a whole set of examples and fail on the one that is not symmetric. `bit_order='q0_first'` puts qubit 0 in the most-significant bit instead, as some other toolkits do. It is the same argument name and the same values that `run(shots=...)` and `probs()` take. Having it on some of the three and not the others is itself the trap, because then "I checked with run()" stops being an answer about the other two -- and before this it was worse than absent here: the argument was accepted, validated, and then silently ignored, so asking for the other convention returned the default one with nothing said.""" if kwargs.get('returns'): raise ValueError('Circuit.statevector has no argument `returns`.') # Imported here rather than at module scope: backendbase pulls in the # backends package, which imports this module back. from .backends.backendbase import BIT_ORDERS if bit_order not in BIT_ORDERS: raise ValueError( f"bit_order must be one of {BIT_ORDERS}, got {bit_order!r}.") backend = self._resolve_backend(backend, kwargs) if hasattr(backend, 'statevector'): state = backend.statevector(self.ops, self.n_qubits, **kwargs) else: state = backend.run(self.ops, self.n_qubits, returns='statevector', **kwargs) if bit_order == 'q0_first': return _reverse_index_bits(state, self.n_qubits) return state
[docs] def shots(self, shots: int, backend: 'BackendUnion' = None, **kwargs) -> typing.Counter[str]: """Run the circuit and get shot counts as a result. Accepts the same ``seed`` and ``bit_order`` arguments as :meth:`~blueqat.circuit.Circuit.run`.""" if kwargs.get('returns'): raise ValueError('Circuit.shots has no argument `returns`.') backend = self._resolve_backend(backend, kwargs) 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.""" if kwargs.get('returns'): raise ValueError('Circuit.oneshot has no argument `returns`.') backend = self._resolve_backend(backend, kwargs) 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, bit_order: str = 'q0_last', **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). By default index bit j is the outcome of `qubits[j]`: the first listed qubit is the least-significant bit of the index, matching the SDK-wide convention and what `blueqat.BIT_ORDER` reports. Differentiable. `bit_order='q0_first'` reverses that, putting the first listed qubit in the most-significant bit, which is what some other toolkits do. The name and the values are the same ones `run(shots=...)` takes, so the two do not have to be remembered separately. Under noise there is no statevector to square; the probabilities are the density matrix's diagonal, and are read from there.""" from .backends.backendbase import BIT_ORDERS if bit_order not in BIT_ORDERS: raise ValueError( f"bit_order must be one of {BIT_ORDERS}, got {bit_order!r}.") resolved = self._resolve_backend(backend, kwargs) if getattr(resolved, 'returns_density_matrix', False): rho = resolved.run(self.ops, self.n_qubits, **kwargs) p = torch.diagonal(rho).real.clone() else: p = torch.abs(self.statevector(backend, **kwargs)) ** 2 if qubits is None: return _reverse_index_bits(p, self.n_qubits) if bit_order == 'q0_first' else 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)]) marginal = t.reshape(-1) if bit_order == 'q0_first': marginal = _reverse_index_bits(marginal, len(keep)) return marginal
[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. A Hamiltonian is a Pauli expression -- ``Z[0] + 0.5 * X[1]``, built from the operators exported at the top level -- or a string (``"Z[0] + 0.5*X[1]"``). A dict of coefficients is not one, and neither is a matrix. The circuit may be narrower than the Hamiltonian: qubits the circuit never mentions are in |0>, which is a perfectly good state to take an expectation in, and ``Circuit().expect(Z[0]) == 1`` rather than an error about a zero-qubit state. """ hamiltonian = _as_pauli_expression(hamiltonian) width = _hamiltonian_width(hamiltonian) if width > self.n_qubits: # Widen a copy rather than this circuit: expect() is a question, # and asking it should not change the thing being asked about. widened = Circuit(width, list(self.ops)) return widened.run(backend, hamiltonian=hamiltonian, **kwargs) return self.run(backend, hamiltonian=hamiltonian, **kwargs)
[docs] def exp_pauli(self, paulis: typing.Mapping[int, str], theta: Any) -> 'Circuit': """Append ``exp(-i * theta * P)``, the time evolution of a single Pauli product. `paulis` maps a qubit index to its Pauli letter, so the operator is stated without reference to any bit order or overall width:: Circuit().exp_pauli({0: 'X', 1: 'X', 2: 'Z', 3: 'Y'}, 0.3) # exp(-0.3i XXZY) Since ``P**2 == I``, this is exactly ``cos(theta) - i sin(theta) P``. The convention (no factor of 1/2) matches :meth:`~blueqat.utils.Term.get_time_evolution`; note that a single-qubit ``{q: 'Z'}`` is therefore ``rz(2 * theta)[q]``. `theta` may be a ``torch.Tensor``, in which case the gradient flows through. Letters are case-insensitive, and ``'I'`` entries are ignored. A product of nothing but identities is a global phase, which a statevector does not carry, so it appends no gates. """ ops = [] for qubit, letter in paulis.items(): if not isinstance(qubit, int) or isinstance(qubit, bool) or qubit < 0: raise ValueError(f"Qubit index must be a non-negative int, got {qubit!r}.") letter = str(letter).upper() if letter not in ('X', 'Y', 'Z', 'I'): raise ValueError(f"Pauli letter must be one of X, Y, Z, I, got {letter!r}.") if letter != 'I': ops.append((qubit, letter)) if not ops: return self ops.sort() half_pi = torch.pi / 2 # Rotate each factor into the Z basis (H X H = Z, RX(+pi/2) Y RX(-pi/2) = Z), # accumulate the parity of the whole product onto the last qubit, rotate it by # rz(2*theta), then undo both. Same construction as Term.get_time_evolution. for qubit, letter in ops: if letter == 'X': self.h[qubit] elif letter == 'Y': self.rx(half_pi)[qubit] for i in range(1, len(ops)): self.cx[ops[i - 1][0], ops[i][0]] self.rz(2 * theta)[ops[-1][0]] for i in range(len(ops) - 1, 0, -1): self.cx[ops[i - 1][0], ops[i][0]] for qubit, letter in ops: if letter == 'X': self.h[qubit] elif letter == 'Y': self.rx(-half_pi)[qubit] return self
[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] └─ ... Blocks appear by name with their contents beneath them; plain gates outside any block are listed at the top level.""" 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: # A negative pos resolves later as an index from the end, so an # ancilla silently lands on a data qubit -- and the automatic reset # on leaving the block erases it. `append_block` already refuses # negative offsets; this is the same rule. if pos < 0: raise ValueError(f"ancilla pos must be non-negative, got {pos}.") if stop is not None and stop < pos: raise ValueError(f"ancilla stop ({stop}) must not be below pos ({pos}).") 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