Source code for mimiqcircuits.circuit_extras

#
# Copyright © 2023-2025 QPerfect. All Rights Reserved.
#
# 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.
#

from mimiqcircuits.instruction import Instruction
from mimiqcircuits.operations.gates.standard.swap import GateSWAP
from mimiqcircuits.circuit import Circuit
from mimiqcircuits.operations.inverse import Inverse
from mimiqcircuits.operations.block import Block
from mimiqcircuits.gatedecl import GateDecl, GateCall
import mimiqcircuits as mc


[docs] def remove_unused(self): """ Removes unused qubits, bits, and zvars from the given circuit. Returns (new_circuit, qubit_map, bit_map, zvar_map). Examples: >>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), 0) 1-qubit circuit with 1 instruction: └── H @ q[0] <BLANKLINE> >>> c.push(GateCX(), 0, 2) # qubit 1 is unused 3-qubit circuit with 2 instructions: ├── H @ q[0] └── CX @ q[0], q[2] <BLANKLINE> >>> c.push(Measure(), 2, 2) # bits 0 & 1 are unused 3-qubit, 3-bit circuit with 3 instructions: ├── H @ q[0] ├── CX @ q[0], q[2] └── M @ q[2], c[2] <BLANKLINE> >>> c.push(ExpectationValue(GateX()), 0, 1) # zvar 0 is unused 3-qubit, 3-bit, 2-zvar circuit with 4 instructions: ├── H @ q[0] ├── CX @ q[0], q[2] ├── M @ q[2], c[2] └── ⟨X⟩ @ q[0], z[1] <BLANKLINE> >>> new_c, qmap, bmap, zmap = remove_unused(c) >>> new_c 2-qubit, 1-bit, 1-zvar circuit with 4 instructions: ├── H @ q[0] ├── CX @ q[0], q[1] ├── M @ q[1], c[0] └── ⟨X⟩ @ q[0], z[0] <BLANKLINE> >>> qmap {0: 0, 2: 1} >>> bmap {2: 0} >>> zmap {1: 0} """ used_qubits = set() used_bits = set() used_zvars = set() for g in self: used_qubits.update(g.get_qubits()) used_bits.update(g.get_bits()) used_zvars.update(g.get_zvars()) qubit_map = {q: i for i, q in enumerate(sorted(used_qubits))} bit_map = {b: i for i, b in enumerate(sorted(used_bits))} zvar_map = {z: i for i, z in enumerate(sorted(used_zvars))} new_circuit = Circuit() for g in self: new_qubits = tuple(qubit_map[q] for q in g.get_qubits() if q in qubit_map) new_bits = tuple(bit_map[b] for b in g.get_bits() if b in bit_map) new_ztargets = tuple(zvar_map[z] for z in g.get_zvars() if z in zvar_map) new_instr = Instruction(g.get_operation(), new_qubits, new_bits, new_ztargets) new_circuit.append([new_instr]) return new_circuit, qubit_map, bit_map, zvar_map
[docs] def remove_swaps(circuit, recursive=False, cache=None): """ Remove all SWAP gates from a quantum circuit by tracking qubit permutations and remapping subsequent operations to their correct physical qubits. Returns: tuple: A tuple containing: - new_circuit: Circuit with SWAP gates removed and operations remapped - qubit_permutation: Final permutation where qubit_permutation[i] gives the physical qubit location of logical qubit i Args: circuit: Input quantum circuit recursive: If True, recursively remove swaps from nested blocks/subcircuits Details: When a SWAP gate is encountered on qubits (i, j), instead of keeping the gate: 1. The qubit mapping is updated to track that logical qubits i and j have exchanged physical positions 2. All subsequent gates are automatically remapped to operate on the correct physical qubits This transformation preserves circuit semantics while eliminating SWAP overhead. Examples: >>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(GateH(), 1) 2-qubit circuit with 1 instruction: └── H @ q[1] <BLANKLINE> >>> c.push(GateSWAP(), 1, 2) 3-qubit circuit with 2 instructions: ├── H @ q[1] └── SWAP @ q[1:2] <BLANKLINE> >>> c.push(GateCX(), 2, 3) 4-qubit circuit with 3 instructions: ├── H @ q[1] ├── SWAP @ q[1:2] └── CX @ q[2], q[3] <BLANKLINE> >>> new_c, perm = remove_swaps(c) >>> new_c 4-qubit circuit with 2 instructions: ├── H @ q[1] └── CX @ q[1], q[3] <BLANKLINE> >>> perm [0, 2, 1, 3] >>> c = Circuit() >>> c.push(GateSWAP(), 1, 2) 3-qubit circuit with 1 instruction: └── SWAP @ q[1:2] <BLANKLINE> >>> c.push(GateSWAP(), 2, 3) 4-qubit circuit with 2 instructions: ├── SWAP @ q[1:2] └── SWAP @ q[2:3] <BLANKLINE> >>> c.push(GateCX(), 1, 3) 4-qubit circuit with 3 instructions: ├── SWAP @ q[1:2] ├── SWAP @ q[2:3] └── CX @ q[1], q[3] <BLANKLINE> >>> new_c, perm = remove_swaps(c) >>> new_c 3-qubit circuit with 1 instruction: └── CX @ q[2], q[1] <BLANKLINE> >>> perm [0, 2, 3, 1] # Example of checking recursive removel of swaps >>> inner = Circuit() >>> _ = inner.push(GateSWAP(), 0, 1) >>> _ = inner.push(GateCX(), 1, 0) >>> Inner = GateDecl("Inner", [], inner) >>> mid = Circuit() >>> _ = mid.push(GateSWAP(), 0, 1) >>> _ = mid.push(GateCall(Inner, ()), 0, 1) >>> Mid = GateDecl("Mid", [], mid) >>> outer = Circuit() >>> _ = outer.push(GateSWAP(), 1, 2) >>> _ = outer.push(GateCall(Mid, ()), 1, 2) >>> Outer = GateDecl("Outer", [], outer) >>> c = Circuit() >>> _ = c.push(GateSWAP(), 1, 2) >>> _ = c.push(GateCall(Outer, ()), 0, 1, 2) >>> c 3-qubit circuit with 2 instructions: ├── SWAP @ q[1:2] └── Outer() @ q[0:2] <BLANKLINE> >>> c.decompose() 3-qubit circuit with 13 instructions: ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] └── CX @ q[2], q[1] <BLANKLINE> >>> c.decompose().decompose() 3-qubit circuit with 13 instructions: ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] ├── CX @ q[1], q[2] ├── CX @ q[2], q[1] ├── CX @ q[1], q[2] └── CX @ q[2], q[1] <BLANKLINE> # Test to see that all swaps are removed recursively >>> new_c, _ = remove_swaps(c, recursive=True) >>> any(isinstance(i.get_operation(), GateSWAP) for i in new_c) False >>> Outercheck = new_c.instructions[0].get_operation()._decl >>> any(isinstance(i.get_operation(), GateSWAP) for i in Outercheck.circuit) False >>> Midcheck = Outercheck.circuit.instructions[0].get_operation()._decl >>> any(isinstance(i.get_operation(), GateSWAP) for i in Midcheck.circuit) False >>> Innercheck = Midcheck.circuit.instructions[0].get_operation()._decl >>> any(isinstance(i.get_operation(), GateSWAP) for i in Innercheck.circuit) False """ if cache is None: cache = {} perm = list(range(circuit.num_qubits())) new_circuit = Circuit() for instr in circuit: op = instr.get_operation() qubits = list(instr.get_qubits()) if isinstance(op, GateSWAP): q1, q2 = qubits perm[q1], perm[q2] = perm[q2], perm[q1] elif recursive and _is_composite_operation(op): new_op, block_map = _remove_swaps_from_operation(op, recursive, cache) new_qubits = tuple(perm[q] for q in qubits) new_instr = Instruction( new_op, new_qubits, instr.get_bits(), instr.get_zvars() ) new_circuit.append([new_instr]) # Apply block's permutation to our permutation # perm[qubits] = perm[qubits[block_map]] old_perm_values = [perm[qubits[i]] for i in block_map] for i, q in enumerate(qubits): perm[q] = old_perm_values[i] else: new_qubits = tuple(perm[q] for q in qubits) new_instr = Instruction(op, new_qubits, instr.get_bits(), instr.get_zvars()) new_circuit.append([new_instr]) _pad_to_arity(new_circuit.instructions, circuit.num_qubits()) return new_circuit, perm
def _pad_to_arity(instructions, target_nq): """Pad instruction list with GateID to preserve qubit arity.""" nq = _get_num_qubits(instructions) for q in range(nq, target_nq): instructions.append(mc.Instruction(mc.GateID(), (q,), (), ())) _PYOBJ_IDENTITY_CACHE_PREFIX = "__remove_swaps_identity__" def _get_identity_cached(cache, op): entry = cache.get((_PYOBJ_IDENTITY_CACHE_PREFIX, id(op))) if entry is None: return None cached_op, cached_value = entry if cached_op is op: return cached_value return None def _set_identity_cached(cache, op, value): cache[(_PYOBJ_IDENTITY_CACHE_PREFIX, id(op))] = (op, value) def _is_composite_operation(op): """Check if operation is a composite that may contain swaps. Note: Control(GateCall) and IfStatement(Block) are intentionally excluded. Their internal SWAPs are conditional (only execute when control is |1> or classical condition is met), so they cannot be tracked as static permutations. """ return ( isinstance(op, GateCall) or isinstance(op, Block) or (isinstance(op, Inverse) and isinstance(op.op, GateCall)) ) def _remove_swaps_from_operation(op, recursive, cache): if isinstance(op, GateCall): new_decl, qubit_map = _remove_swaps_from_gate_decl(op._decl, recursive, cache) return GateCall(new_decl, op._args), qubit_map elif isinstance(op, Block): cached = _get_identity_cached(cache, op) if cached is not None: return cached new_instrs, qubit_map = _remove_swaps_from_instructions( op.instructions, recursive, cache ) # Preserve original block dimensions (don't let Block shrink) nq = op._num_qubits nb = op._num_bits nz = op._num_zvars # Pad qubit_map with identity for qubits not used in instructions if len(qubit_map) < nq: qubit_map.extend(range(len(qubit_map), nq)) _pad_to_arity(new_instrs, nq) res = (Block(nq, nb, nz, new_instrs), qubit_map) _set_identity_cached(cache, op, res) return res elif isinstance(op, Inverse) and isinstance(op.op, GateCall): cached = _get_identity_cached(cache, op) if cached is not None: return cached gcall = op.op decl = gcall._decl # Expand the inverse body, remove swaps, then rebuild the forward body # to keep the Inverse(GateCall(...)) wrapper. inv_circuit = decl.circuit.inverse() inv_instrs = inv_circuit.instructions # Remove swaps from the expanded inverse new_inv_instrs, qubit_map = _remove_swaps_from_instructions( inv_instrs, recursive, cache ) # Preserve original arity orig_nq = decl.num_qubits() if callable(decl.num_qubits) else decl.num_qubits _pad_to_arity(new_inv_instrs, orig_nq) # Rebuild the forward body new_fwd_circuit = Circuit(new_inv_instrs).inverse() new_fwd_instrs = new_fwd_circuit.instructions fwd_decl = GateDecl(decl.name, decl.arguments, mc.Circuit(new_fwd_instrs)) res = (mc.Inverse(GateCall(fwd_decl, gcall._args)), qubit_map) _set_identity_cached(cache, op, res) return res else: raise ValueError(f"Unsupported composite operation type: {type(op)}") def _remove_swaps_from_instructions(instructions, recursive, cache): perm = list(range(_get_num_qubits(instructions))) new_instrs = [] for instr in instructions: op = instr.get_operation() qubits = list(instr.get_qubits()) if isinstance(op, GateSWAP): q1, q2 = qubits perm[q1], perm[q2] = perm[q2], perm[q1] elif recursive and _is_composite_operation(op): new_op, block_map = _remove_swaps_from_operation(op, recursive, cache) new_qubits = tuple(perm[q] for q in qubits) new_instrs.append( Instruction(new_op, new_qubits, instr.get_bits(), instr.get_zvars()) ) old_perm_values = [perm[qubits[i]] for i in block_map] for i, q in enumerate(qubits): perm[q] = old_perm_values[i] else: new_qubits = tuple(perm[q] for q in qubits) new_instrs.append( Instruction(op, new_qubits, instr.get_bits(), instr.get_zvars()) ) return new_instrs, perm def _remove_swaps_from_gate_decl(decl, recursive, cache): if decl in cache: return cache[decl] new_instrs, qubit_map = _remove_swaps_from_instructions( decl.circuit.instructions, recursive, cache ) # Preserve original arity (do NOT let GateDecl shrink) orig_nq = decl.num_qubits() if callable(decl.num_qubits) else decl.num_qubits _pad_to_arity(new_instrs, orig_nq) res = (GateDecl(decl.name, decl.arguments, mc.Circuit(new_instrs)), qubit_map) cache[decl] = res return res def _get_num_qubits(instructions): """Helper to determine number of qubits in instruction list.""" if not instructions: return 0 return max(max(instr.get_qubits(), default=-1) for instr in instructions) + 1