From 359d8b8b12e13b803f303957fae7c93293724415 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Wed, 17 Sep 2025 18:29:16 -0230 Subject: [PATCH 01/22] Merging builders. --- docs/apidocs/samplomatic.builders.rst | 39 ++- .../samplomatic.builders.samplex_builder.rst | 26 -- .../samplomatic.builders.template_builder.rst | 34 -- samplomatic/builders/box_builder.py | 224 ++++++++++++ samplomatic/builders/build.py | 59 ++-- samplomatic/builders/builder.py | 72 ++-- .../{get_builders.py => get_builder.py} | 24 +- ...late_builder.py => passthrough_builder.py} | 48 +-- .../builders/samplex_builder/__init__.py | 16 - .../samplex_builder/box_samplex_builder.py | 220 ------------ .../passthrough_samplex_builder.py | 37 -- .../builders/template_builder/__init__.py | 21 -- .../template_builder/box_template_builder.py | 326 ------------------ .../{template_builder => }/template_state.py | 6 +- .../test_left_box_builder.py | 292 ++++++++-------- .../test_right_box_builder.py | 39 +-- .../test_template_builder/test_build.py | 294 ++++++++-------- test/unit/test_get_builders.py | 8 +- 18 files changed, 661 insertions(+), 1124 deletions(-) delete mode 100644 docs/apidocs/samplomatic.builders.samplex_builder.rst delete mode 100644 docs/apidocs/samplomatic.builders.template_builder.rst create mode 100644 samplomatic/builders/box_builder.py rename samplomatic/builders/{get_builders.py => get_builder.py} (90%) rename samplomatic/builders/{template_builder/passthrough_template_builder.py => passthrough_builder.py} (51%) delete mode 100644 samplomatic/builders/samplex_builder/__init__.py delete mode 100644 samplomatic/builders/samplex_builder/box_samplex_builder.py delete mode 100644 samplomatic/builders/samplex_builder/passthrough_samplex_builder.py delete mode 100644 samplomatic/builders/template_builder/__init__.py delete mode 100644 samplomatic/builders/template_builder/box_template_builder.py rename samplomatic/builders/{template_builder => }/template_state.py (97%) diff --git a/docs/apidocs/samplomatic.builders.rst b/docs/apidocs/samplomatic.builders.rst index 379b2b09..d1992f9f 100644 --- a/docs/apidocs/samplomatic.builders.rst +++ b/docs/apidocs/samplomatic.builders.rst @@ -6,18 +6,17 @@ samplomatic.builders package :show-inheritance: :undoc-members: -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - samplomatic.builders.samplex_builder - samplomatic.builders.template_builder - Submodules ---------- +samplomatic.builders.box\_builder module +---------------------------------------- + +.. automodule:: samplomatic.builders.box_builder + :members: + :show-inheritance: + :undoc-members: + samplomatic.builders.build module --------------------------------- @@ -34,10 +33,10 @@ samplomatic.builders.builder module :show-inheritance: :undoc-members: -samplomatic.builders.get\_builders module ------------------------------------------ +samplomatic.builders.get\_builder module +---------------------------------------- -.. automodule:: samplomatic.builders.get_builders +.. automodule:: samplomatic.builders.get_builder :members: :show-inheritance: :undoc-members: @@ -50,6 +49,14 @@ samplomatic.builders.param\_iter module :show-inheritance: :undoc-members: +samplomatic.builders.passthrough\_builder module +------------------------------------------------ + +.. automodule:: samplomatic.builders.passthrough_builder + :members: + :show-inheritance: + :undoc-members: + samplomatic.builders.specs module --------------------------------- @@ -57,3 +64,11 @@ samplomatic.builders.specs module :members: :show-inheritance: :undoc-members: + +samplomatic.builders.template\_state module +------------------------------------------- + +.. automodule:: samplomatic.builders.template_state + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/apidocs/samplomatic.builders.samplex_builder.rst b/docs/apidocs/samplomatic.builders.samplex_builder.rst deleted file mode 100644 index a02b7c4c..00000000 --- a/docs/apidocs/samplomatic.builders.samplex_builder.rst +++ /dev/null @@ -1,26 +0,0 @@ -samplomatic.builders.samplex\_builder package -============================================= - -.. automodule:: samplomatic.builders.samplex_builder - :members: - :show-inheritance: - :undoc-members: - -Submodules ----------- - -samplomatic.builders.samplex\_builder.box\_samplex\_builder module ------------------------------------------------------------------- - -.. automodule:: samplomatic.builders.samplex_builder.box_samplex_builder - :members: - :show-inheritance: - :undoc-members: - -samplomatic.builders.samplex\_builder.passthrough\_samplex\_builder module --------------------------------------------------------------------------- - -.. automodule:: samplomatic.builders.samplex_builder.passthrough_samplex_builder - :members: - :show-inheritance: - :undoc-members: diff --git a/docs/apidocs/samplomatic.builders.template_builder.rst b/docs/apidocs/samplomatic.builders.template_builder.rst deleted file mode 100644 index 147e9cb5..00000000 --- a/docs/apidocs/samplomatic.builders.template_builder.rst +++ /dev/null @@ -1,34 +0,0 @@ -samplomatic.builders.template\_builder package -============================================== - -.. automodule:: samplomatic.builders.template_builder - :members: - :show-inheritance: - :undoc-members: - -Submodules ----------- - -samplomatic.builders.template\_builder.box\_template\_builder module --------------------------------------------------------------------- - -.. automodule:: samplomatic.builders.template_builder.box_template_builder - :members: - :show-inheritance: - :undoc-members: - -samplomatic.builders.template\_builder.passthrough\_template\_builder module ----------------------------------------------------------------------------- - -.. automodule:: samplomatic.builders.template_builder.passthrough_template_builder - :members: - :show-inheritance: - :undoc-members: - -samplomatic.builders.template\_builder.template\_state module -------------------------------------------------------------- - -.. automodule:: samplomatic.builders.template_builder.template_state - :members: - :show-inheritance: - :undoc-members: diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py new file mode 100644 index 00000000..28983089 --- /dev/null +++ b/samplomatic/builders/box_builder.py @@ -0,0 +1,224 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""BoxTemplateBuilder""" + +from __future__ import annotations + +import numpy as np +from qiskit.circuit import Barrier + +from ..aliases import CircuitInstruction, ParamIndices +from ..exceptions import SamplexBuildError, TemplateBuildError +from ..partition import QubitPartition +from ..pre_samplex import PreSamplex +from .builder import Builder +from .specs import CollectionSpec, EmissionSpec, InstructionMode, InstructionSpec, VirtualType +from .template_state import TemplateState + + +class BoxBuilder(Builder[TemplateState, PreSamplex]): + """Builds dressed boxes.""" + + def __init__(self, collection: CollectionSpec, emission: EmissionSpec): + super().__init__() + + self.collection = collection + self.emission = emission + self.measured_qubits = QubitPartition(1, []) + self.entangled_qubits = set() + + def _append_dressed_layer(self) -> ParamIndices: + """A helper function to add a collection dressing layer.""" + qubits = self.collection.qubits + try: + remapped_qubits = [ + list(map(lambda k: self.template_state.qubit_map[k], subsys)) for subsys in qubits + ] + except KeyError: + not_found = { + qubit + for subsys in qubits + for qubit in subsys + if qubit not in self.template_state.qubit_map + } + raise TemplateBuildError( + f"The qubits '{not_found}' could not be found when recursing into a box of the " + "input circuit." + ) from KeyError + + param_idx_start = self.template_state.param_iter.idx + num_params = len(qubits) * self.collection.synth.num_params + param_idxs = np.arange(param_idx_start, param_idx_start + num_params, dtype=np.intp) + + for subsys_remapped_qubits in remapped_qubits: + for instr in self.collection.synth.make_template( + subsys_remapped_qubits, self.template_state.param_iter + ): + self.template_state.template.append(instr) + + return param_idxs.reshape(len(qubits), -1) + + def _append_barrier(self, label: str): + label = f"{label}{'_'.join(map(str, self.template_state.scope_idx))}" + all_qubits = self.template_state.qubit_map.values() + barrier = CircuitInstruction(Barrier(len(all_qubits), label), all_qubits) + self.template_state.template.append(barrier) + + +class LeftBoxBuilder(BoxBuilder): + """Box builder for left dressings.""" + + def __init__(self, collection: CollectionSpec, emission: EmissionSpec): + super().__init__(collection=collection, emission=emission) + + self.measured_qubits = QubitPartition(1, []) + self.clbit_idxs = [] + + def parse(self, instr: CircuitInstruction): + name = instr.operation.name + param_idxs = np.empty((0, 0), dtype=np.intp) + + if (name := instr.operation.name) == "barrier": + self.template_state.append_remapped_gate(instr) + return + + if name.startswith("meas"): + for qubit in instr.qubits: + if (qubit,) not in self.measured_qubits: + self.measured_qubits.add((qubit,)) + else: + raise SamplexBuildError( + "Cannot measure the same qubit twice in a twirling box." + ) + self.template_state.append_remapped_gate(instr) + self.clbit_idxs.extend( + [self.template_state.template.find_bit(clbit)[0] for clbit in instr.clbits] + ) + return + + if (num_qubits := instr.operation.num_qubits) == 1: + if self.measured_qubits.overlaps_with(instr.qubits): + raise RuntimeError( + "Cannot handle single-qubit gate to the right of measurements when " + "dressing=left." + ) + if not self.entangled_qubits.isdisjoint(instr.qubits): + raise RuntimeError( + "Cannot handle single-qubit gate to the right of entangler when dressing=left." + ) + # the action of this single-qubit gate will be absorbed into the dressing + params = [] + if instr.operation.is_parameterized(): + params.extend((None, param) for param in instr.operation.params) + spec = InstructionSpec( + params=params, mode=InstructionMode.MULTIPLY, param_idxs=param_idxs + ) + + elif num_qubits > 1: + self.entangled_qubits.update(instr.qubits) + spec = InstructionSpec( + params=self.template_state.append_remapped_gate(instr), + mode=InstructionMode.PROPAGATE, + param_idxs=param_idxs, + ) + else: + raise RuntimeError(f"Instruction {instr} could not be parsed.") + + if self.measured_qubits.overlaps_with(instr.qubits): + # TODO: What about delays? barriers? + raise SamplexBuildError( + f"Instruction {instr} happens after a measurement. No operations allowed " + "after a measurement in a measurement twirling box." + ) + self.samplex_state.add_propagate(instr, spec) + + def lhs(self): + self._append_barrier("L") + param_idxs = self._append_dressed_layer() + self.samplex_state.add_collect(self.collection.qubits, self.collection.synth, param_idxs) + self._append_barrier("M") + + def rhs(self): + self._append_barrier("R") + + if self.emission.noise_ref: + self.samplex_state.add_emit_noise_left( + self.emission.qubits, self.emission.noise_ref, self.emission.noise_modifier_ref + ) + if self.emission.basis_ref: + self.samplex_state.add_emit_meas_basis_transform( + self.emission.qubits, self.emission.basis_ref + ) + if twirl_type := self.emission.twirl_register_type: + self.samplex_state.add_emit_twirl(self.emission.qubits, twirl_type) + if len(self.measured_qubits) != 0: + if twirl_type != VirtualType.PAULI: + raise SamplexBuildError( + f"Cannot use {twirl_type.value} twirl in a box with measurements." + ) + self.samplex_state.add_z2_collect(self.measured_qubits, self.clbit_idxs) + + +class RightBoxBuilder(BoxBuilder): + """Box builder for right dressings.""" + + def parse(self, instr: CircuitInstruction): + if (name := instr.operation.name).startswith("meas"): + raise RuntimeError("Boxes with measurements cannot have dressing=right.") + + if name == "barrier": + spec = InstructionSpec(params=self.template_state.append_remapped_gate(instr)) + + elif (num_qubits := instr.operation.num_qubits) == 1: + self.entangled_qubits.update(instr.qubits) + # the action of this single-qubit gate will be absorbed into the dressing + params = [] + if instr.operation.is_parameterized(): + params.extend((None, param) for param in instr.operation.params) + spec = InstructionSpec(mode=InstructionMode.MULTIPLY, params=params) + + elif num_qubits > 1: + if not self.entangled_qubits.isdisjoint(instr.qubits): + raise RuntimeError( + "Cannot handle single-qubit gate to the left of entangler when dressing=right." + ) + spec = InstructionSpec( + params=self.template_state.append_remapped_gate(instr), + mode=InstructionMode.PROPAGATE, + ) + else: + raise RuntimeError(f"Instruction {instr} could not be parsed.") + + self.samplex_state.add_propagate(instr, spec) + + def lhs(self): + self._append_barrier("L") + + if self.emission.basis_ref: + self.samplex_state.add_emit_prep_basis_transform( + self.emission.qubits, self.emission.basis_ref + ) + if self.emission.noise_ref: + self.samplex_state.add_emit_noise_right( + self.emission.qubits, self.emission.noise_ref, self.emission.noise_modifier_ref + ) + if self.emission.twirl_register_type: + self.samplex_state.add_emit_twirl( + self.emission.qubits, self.emission.twirl_register_type + ) + + def rhs(self): + self._append_barrier("M") + param_idxs = self._append_dressed_layer() + self.samplex_state.add_collect(self.collection.qubits, self.collection.synth, param_idxs) + self._append_barrier("R") diff --git a/samplomatic/builders/build.py b/samplomatic/builders/build.py index 3792067f..ac4a591b 100644 --- a/samplomatic/builders/build.py +++ b/samplomatic/builders/build.py @@ -20,66 +20,50 @@ from ..pre_samplex import PreSamplex from ..samplex import Samplex from .builder import Builder -from .get_builders import get_builders -from .specs import InstructionSpec -from .template_builder import TemplateState +from .get_builder import get_builder +from .template_state import TemplateState -def _build_stream( - stream: CircuitInstruction, - template_builder: Builder[TemplateState, InstructionSpec], - samplex_builder: Builder[PreSamplex, None], -) -> Iterator[CircuitInstruction]: +def _build_stream(stream: CircuitInstruction, builder: Builder) -> Iterator[CircuitInstruction]: """Build while iterating an instruction stream, but halting to yield each ``box``. Args: stream: A stream of instructions to build from. - template_builder: The template builder to build with. - samplex_builder: The samplex builder to build with. + builder: The builder to build with. Yields: Box circuit instruction objects. """ - instruction_spec = template_builder.lhs() - samplex_builder.lhs(instruction_spec) + builder.lhs() for instr in stream: if instr.operation.name == "box": yield instr else: - instruction_spec = template_builder.parse(instr) - samplex_builder.parse(instr, instruction_spec) + builder.parse(instr) - instruction_spec = template_builder.rhs() - samplex_builder.rhs(instruction_spec) + builder.rhs() -def _build( - stream: CircuitInstruction, - template_builder: Builder[TemplateState, InstructionSpec], - samplex_builder: Builder[PreSamplex, None], -): +def _build(stream: CircuitInstruction, builder: Builder): """Recursively builds from a stream of instructions. Args: stream: A stream of instructions to build from. - template_builder: The template builder to build with. - samplex_builder: The samplex builder to build with. + builder: The builder to build with. """ - for idx, nested_instr in enumerate(_build_stream(stream, template_builder, samplex_builder)): + for idx, nested_instr in enumerate(_build_stream(stream, builder)): # assume the nested instruction is a box for now, handle other control flow ops later - inner_template_builder, inner_samplex_builder = get_builders( - nested_instr, template_builder.state.qubit_map - ) + inner_builder = get_builder(nested_instr, builder.template_state.qubit_map) qubit_remapping = dict(zip(nested_instr.qubits, nested_instr.operation.body.qubits)) - remapped_template_state = template_builder.state.remap(qubit_remapping, idx) - inner_template_builder = inner_template_builder.set_state(remapped_template_state) - - remapped_pre_samplex = samplex_builder.state.remap(remapped_template_state.qubit_map) - inner_samplex_builder = inner_samplex_builder.set_state(remapped_pre_samplex) + remapped_template_state = builder.template_state.remap(qubit_remapping, idx) + remapped_pre_samplex = builder.samplex_state.remap(remapped_template_state.qubit_map) + inner_builder = inner_builder.set_template_state(remapped_template_state).set_samplex_state( + remapped_pre_samplex + ) - _build(nested_instr.operation.body, inner_template_builder, inner_samplex_builder) + _build(nested_instr.operation.body, inner_builder) def pre_build(circuit: QuantumCircuit) -> tuple[TemplateState, PreSamplex]: @@ -94,13 +78,10 @@ def pre_build(circuit: QuantumCircuit) -> tuple[TemplateState, PreSamplex]: The built template state and the corresponding pre-samplex. """ template_state = TemplateState.construct_for_circuit(circuit) - template_builder, samplex_builder = get_builders(None, template_state.qubit_map.keys()) - template_builder = template_builder.set_state(template_state) - pre_samplex = PreSamplex(qubit_map=template_state.qubit_map, cregs=circuit.cregs) - samplex_builder = samplex_builder.set_state(pre_samplex) - - _build(circuit, template_builder, samplex_builder) + builder = get_builder(None, template_state.qubit_map.keys()) + builder.set_template_state(template_state).set_samplex_state(pre_samplex) + _build(circuit, builder) return template_state, pre_samplex diff --git a/samplomatic/builders/builder.py b/samplomatic/builders/builder.py index fceb89e5..182231ed 100644 --- a/samplomatic/builders/builder.py +++ b/samplomatic/builders/builder.py @@ -18,57 +18,67 @@ from ..aliases import CircuitInstruction, Self from ..exceptions import BuildError -StateT = TypeVar("StateT") -OutT = TypeVar("OutT") +SamplexT = TypeVar("SamplexT") +TemplateT = TypeVar("TemplateT") -class Builder(Generic[StateT, OutT], abc.ABC): +class Builder(Generic[TemplateT, SamplexT], abc.ABC): """Generic abstraction for parsing a :class:`~.QuantumCircuit` scope.""" def __init__(self): - self._state = None + self._samplex_state = None + self._template_state = None - def set_state(self, state: StateT) -> Self: - """Set the current state of the builder. + def set_samplex_state(self, samplex_state: SamplexT) -> Self: + """Set the current samplex state of the builder. Args: - state: The new state. + samplex_state: The new state. Returns: A reference to this builder. """ - self._state = state + self._samplex_state = samplex_state return self - @property - def state(self) -> StateT: - """The current state of the builder.""" - if self._state is None: - raise BuildError(f"Attempted to access the state of {self} before it has been set.") - return self._state - - @abc.abstractmethod - def parse(self, instr: CircuitInstruction, *args) -> OutT: - """Parse a single circuit instruction.""" - - @abc.abstractmethod - def lhs(self, *args) -> OutT: - """Perform some action before the current scope's stream is iterated. + def set_template_state(self, template_state: TemplateT) -> Self: + """Set the current template state of the builder. Args: - args: Arguments required at the LHS boundary. + template_state: The new state. Returns: - Information about the boundary. + A reference to this builder. """ + self._template_state = template_state + return self + + @property + def samplex_state(self) -> SamplexT: + """The current samplex state of the builder.""" + if self._samplex_state is None: + raise BuildError( + f"Attempted to access the samplex state of {self} before it has been set." + ) + return self._samplex_state + + @property + def template_state(self) -> TemplateT: + """The current template state of the builder.""" + if self._template_state is None: + raise BuildError( + f"Attempted to access the template state of {self} before it has been set." + ) + return self._template_state @abc.abstractmethod - def rhs(self, *args) -> OutT: - """Perform some action after the current scope's stream is iterated. + def parse(self, instr: CircuitInstruction): + """Parse a single circuit instruction.""" - Args: - args: Arguments required at the LHS boundary. + @abc.abstractmethod + def lhs(self): + """Perform some action before the current scope's stream is iterated.""" - Returns: - Information about the boundary. - """ + @abc.abstractmethod + def rhs(self): + """Perform some action after the current scope's stream is iterated.""" diff --git a/samplomatic/builders/get_builders.py b/samplomatic/builders/get_builder.py similarity index 90% rename from samplomatic/builders/get_builders.py rename to samplomatic/builders/get_builder.py index 1895928b..d57a4964 100644 --- a/samplomatic/builders/get_builders.py +++ b/samplomatic/builders/get_builder.py @@ -31,27 +31,17 @@ from ..partition import QubitPartition from ..pre_samplex import PreSamplex from ..synths import get_synth +from .box_builder import LeftBoxBuilder, RightBoxBuilder from .builder import Builder -from .samplex_builder import ( - LeftBoxSamplexBuilder, - PassthroughSamplexBuilder, - RightBoxSamplexBuilder, -) +from .passthrough_builder import PassthroughBuilder from .specs import CollectionSpec, EmissionSpec, InstructionSpec -from .template_builder import ( - LeftBoxTemplateBuilder, - PassthroughTemplateBuilder, - RightBoxTemplateBuilder, - TemplateState, -) +from .template_state import TemplateState SamplexBuilder: TypeAlias = Builder[PreSamplex, None] TemplateBuilder: TypeAlias = Builder[TemplateState, InstructionSpec] -def get_builders( - instr: CircuitInstruction | None, qubits: Sequence[Qubit] -) -> tuple[TemplateBuilder, SamplexBuilder]: +def get_builder(instr: CircuitInstruction | None, qubits: Sequence[Qubit]) -> Builder: """Get the builders of a box. Args: @@ -67,7 +57,7 @@ def get_builders( A tuple containing a template and samplex builder. """ if instr is None or not (annotations := instr.operation.annotations): - return PassthroughTemplateBuilder(), PassthroughSamplexBuilder() + return PassthroughBuilder() qubits = QubitPartition.from_elements(q for q in qubits if q in instr.qubits) collection = CollectionSpec(qubits) @@ -88,8 +78,8 @@ def get_builders( raise BuildError(f"Cannot get a builder for {annotations}. Inject noise requires twirling.") if collection.dressing is DressingMode.LEFT: - return LeftBoxTemplateBuilder(collection), LeftBoxSamplexBuilder(collection, emission) - return RightBoxTemplateBuilder(collection), RightBoxSamplexBuilder(collection, emission) + return LeftBoxBuilder(collection, emission) + return RightBoxBuilder(collection, emission) def basis_transform_parser( diff --git a/samplomatic/builders/template_builder/passthrough_template_builder.py b/samplomatic/builders/passthrough_builder.py similarity index 51% rename from samplomatic/builders/template_builder/passthrough_template_builder.py rename to samplomatic/builders/passthrough_builder.py index 49502a41..16481f89 100644 --- a/samplomatic/builders/template_builder/passthrough_template_builder.py +++ b/samplomatic/builders/passthrough_builder.py @@ -14,29 +14,26 @@ from qiskit.circuit import Barrier, IfElseOp -from ...aliases import CircuitInstruction -from ..builder import Builder -from ..specs import InstructionMode, InstructionSpec +from ..aliases import CircuitInstruction +from ..pre_samplex import PreSamplex +from .builder import Builder +from .specs import InstructionMode, InstructionSpec from .template_state import TemplateState -class PassthroughTemplateBuilder(Builder[TemplateState, InstructionSpec]): - """Template builder that passes all instructions through.""" +class PassthroughBuilder(Builder[TemplateState, PreSamplex]): + """Builder that passes all instructions through.""" - def parse(self, instr: CircuitInstruction) -> InstructionSpec: + def parse(self, instr: CircuitInstruction): """Parse a single non-box instruction. Args: instr: The instruction to parse. - - Returns: - An `InstructionSpec` with a map from where new parameters are located to how to - evaluate their values when the time comes. """ if instr.operation.name.startswith("if_else"): - true_body, true_params = self.state.remap_subcircuit(instr.operation.params[0]) + true_body, true_params = self.template_state.remap_subcircuit(instr.operation.params[0]) false_body, false_params = ( - self.state.remap_subcircuit(instr.operation.params[1]) + self.template_state.remap_subcircuit(instr.operation.params[1]) if instr.operation.params[1] is not None else (None, []) ) @@ -46,23 +43,28 @@ def parse(self, instr: CircuitInstruction) -> InstructionSpec: false_body=false_body, label=instr.operation.label, ) - qubits = [self.state.qubit_map[qubit] for qubit in instr.qubits] - self.state.template.append(CircuitInstruction(ifelse_op, qubits, instr.clbits)) - return InstructionSpec( - params=true_params + false_params, - clbit_idxs=self.state.get_condition_clbits(instr.operation.condition), + qubits = [self.template_state.qubit_map[qubit] for qubit in instr.qubits] + self.template_state.template.append(CircuitInstruction(ifelse_op, qubits, instr.clbits)) + + self.samplex_state.enforce_no_propagation(instr) + self.samplex_state.verify_no_twirled_clbits( + self.template_state.get_condition_clbits(instr.operation.condition) ) + self.samplex_state.passthrough_params.extend(true_params + false_params) else: - return InstructionSpec( - params=self.state.append_remapped_gate(instr), mode=InstructionMode.PROPAGATE + spec = InstructionSpec( + params=self.template_state.append_remapped_gate(instr), + mode=InstructionMode.PROPAGATE, ) + self.samplex_state.add_propagate(instr, spec) + def _append_barrier(self, label: str): - if self.state.scope_idx: - label = f"{label}{'_'.join(map(str, self.state.scope_idx))}" - all_qubits = self.state.qubit_map.values() + if self.template_state.scope_idx: + label = f"{label}{'_'.join(map(str, self.template_state.scope_idx))}" + all_qubits = self.template_state.qubit_map.values() barrier = CircuitInstruction(Barrier(len(all_qubits), label), all_qubits) - self.state.template.append(barrier) + self.template_state.template.append(barrier) def lhs(self) -> InstructionSpec: self._append_barrier("L") diff --git a/samplomatic/builders/samplex_builder/__init__.py b/samplomatic/builders/samplex_builder/__init__.py deleted file mode 100644 index f72a1737..00000000 --- a/samplomatic/builders/samplex_builder/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Builders""" - -from .box_samplex_builder import LeftBoxSamplexBuilder, RightBoxSamplexBuilder -from .passthrough_samplex_builder import PassthroughSamplexBuilder diff --git a/samplomatic/builders/samplex_builder/box_samplex_builder.py b/samplomatic/builders/samplex_builder/box_samplex_builder.py deleted file mode 100644 index 365ca75d..00000000 --- a/samplomatic/builders/samplex_builder/box_samplex_builder.py +++ /dev/null @@ -1,220 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""BoxSamplexBuilder""" - -from __future__ import annotations - -from copy import deepcopy - -from ...aliases import CircuitInstruction -from ...exceptions import SamplexBuildError -from ...partition import QubitPartition -from ...pre_samplex import DanglerMatch, PreSamplex -from ...pre_samplex.graph_data import PreCollect, PrePropagate -from ..builder import Builder -from ..specs import CollectionSpec, EmissionSpec, InstructionSpec, VirtualType - - -class BoxSamplexBuilder(Builder[PreSamplex, None]): - """Builds dressed boxes.""" - - def __init__(self, collection: CollectionSpec, emission: EmissionSpec): - super().__init__() - - self.collection = collection - self.emission = emission - self.already_collected_qubits = set() - - -class LeftBoxSamplexBuilder(BoxSamplexBuilder): - """Box builder for left dressings.""" - - def __init__(self, collection: CollectionSpec, emission: EmissionSpec): - super().__init__(collection, emission) - self.measured_qubits = QubitPartition(1, []) - self.clbit_idxs = [] - self.collection_node_idx = None - - def parse(self, instr: CircuitInstruction, spec: InstructionSpec): - if instr.operation.name.startswith("if_else"): - if any(qubit in self.already_collected_qubits for qubit in instr.qubits): - raise SamplexBuildError( - "No instruction can appear before a conditional in a left-dressed box." - ) - self.state.verify_no_twirled_clbits(spec.clbit_idxs) - - # Mark all qubits as already collected. - # Important to do first, because of nested calls to parse. - self.already_collected_qubits.update(instr.qubits) - if_specs, if_params, else_specs, else_params = spec.if_else - qubits_partition = QubitPartition.from_elements(instr.qubits) - # Both branches need to attach to the same danglers, so we copy the original danglers - original_danglers = deepcopy(self.state.get_all_danglers()) - self.state.add_collect(qubits_partition, self.collection.synth, if_params) - for sub_instr, sub_spec in zip(instr.operation.params[0], if_specs): - self.parse(sub_instr, sub_spec) - - # use list() to iterate the generator expression immediately; state may change - true_branch_danglers = list( - self.state.find_danglers( - DanglerMatch(node_types=(PreCollect, PrePropagate)), - self.state.qubits_to_indices(qubits_partition), - ) - ) - # We need to track all nodes which are added from this point on, to know which - # nodes have to force copy of registers. There is currently no efficient way of - # doing this, so we are forced to compare the set of all nodes before and after. - # If we assume no node removal, we can simplify and look at the max - # node idx before and after. - # If we return node indices from parse() we can also simplify this. - after_if_node_idxs = set(self.state.graph.node_indices()) - - self.state.set_all_danglers(*original_danglers) - self.state.add_collect(qubits_partition, self.collection.synth, else_params) - if instr.operation.params[1] is not None: - for sub_instr, sub_spec in zip(instr.operation.params[1], else_specs): - self.parse(sub_instr, sub_spec) - # Right now, all of the qubits have the else branch nodes as dangling. - # We need to add the danglers of the true branch. - # Note that by construction all of these danglers are new, and their type is forced, - # as they don't follow a measurement. - for node_idx, subsystems in true_branch_danglers: - self.state.add_dangler( - subsystems.all_elements, - node_idx, - ) - # We don't want the else branch to share views with the if branch. - self.state.add_force_copy_nodes( - idx for idx in self.state.graph.node_indices() if idx not in after_if_node_idxs - ) - return - - if to_be_collected := [ - qubit for qubit in instr.qubits if qubit not in self.already_collected_qubits - ]: - self.collection_node_idx = self.state.add_collect( - QubitPartition.from_elements(to_be_collected), - self.collection.synth, - spec.param_idxs, - self.collection_node_idx, - ) - self.already_collected_qubits.update(to_be_collected) - - if instr.operation.name.startswith("meas"): - for qubit in instr.qubits: - if (qubit,) not in self.measured_qubits: - self.measured_qubits.add((qubit,)) - else: - raise SamplexBuildError( - "Cannot measure the same qubit twice in a twirling box." - ) - self.clbit_idxs.extend(spec.clbit_idxs) - - else: - if self.measured_qubits.overlaps_with(instr.qubits): - # TODO: What about delays? barriers? - raise SamplexBuildError( - f"Instruction {instr} happens after a measurement. No operations allowed " - "after a measurement in a measurement twirling box." - ) - self.state.add_propagate(instr, spec) - - def lhs(self, *_): - # Collections are added incrementally. - pass - - def rhs(self, spec: InstructionSpec): - if remaining_qubits := self.collection.qubits.difference(self.already_collected_qubits): - self.state.add_collect( - remaining_qubits, self.collection.synth, spec.param_idxs, self.collection_node_idx - ) - if self.emission.noise_ref: - self.state.add_emit_noise_left( - self.emission.qubits, self.emission.noise_ref, self.emission.noise_modifier_ref - ) - if self.emission.basis_ref: - self.state.add_emit_meas_basis_transform(self.emission.qubits, self.emission.basis_ref) - if twirl_type := self.emission.twirl_register_type: - self.state.add_emit_twirl(self.emission.qubits, twirl_type) - if len(self.measured_qubits) != 0: - if twirl_type != VirtualType.PAULI: - raise SamplexBuildError( - f"Cannot use {twirl_type.value} twirl in a box with measurements." - ) - self.state.add_z2_collect(self.measured_qubits, self.clbit_idxs) - - -class RightBoxSamplexBuilder(BoxSamplexBuilder): - """Box builder for right dressings.""" - - def parse(self, instr: CircuitInstruction, spec: InstructionSpec): - if instr.operation.name.startswith("meas"): - raise SamplexBuildError("Unexpected measurement in a non-measurement box.") - elif instr.operation.name.startswith("if_else"): - self.state.verify_no_twirled_clbits(spec.clbit_idxs) - if_specs, if_params, else_specs, else_params = ( - spec.if_else[0], - spec.if_else[1], - spec.if_else[2], - spec.if_else[3], - ) - # Both branches need to attach to the same danglers, so we copy the original danglers - original_danglers = deepcopy(self.state.get_all_danglers()) - for sub_instr, sub_spec in zip(instr.operation.params[0], if_specs): - self.parse(sub_instr, sub_spec) - true_collect_idx = self.state.add_collect( - QubitPartition.from_elements(instr.qubits), self.collection.synth, if_params - ) - self.state.set_all_danglers(*original_danglers) - # We don't want the else branch to share views with the if branch - for danglers_dict in original_danglers: - for node_idxs in danglers_dict.values(): - self.state.add_force_copy_nodes(node_idxs) - - if instr.operation.params[1] is not None: - for sub_instr, sub_spec in zip(instr.operation.params[1], else_specs): - self.parse(sub_instr, sub_spec) - self.state.add_collect( - QubitPartition.from_elements(instr.qubits), self.collection.synth, else_params - ) - # Right now, all of the qubits have the false branch collection as dangling. - # We need to add the true branch collection as a second dangler to all qubits. - self.state.add_dangler( - self.state.qubits_to_indices( - QubitPartition.from_elements(instr.qubits) - ).all_elements, - true_collect_idx, - ) - # Mark all qubits as already collected - self.already_collected_qubits.update(instr.qubits) - # We are done with everything incoming to the else branch, - # so we can remove the nodes and let them share views now. - for danglers_dict in original_danglers: - for node_idxs in danglers_dict.values(): - self.state.remove_force_copy_nodes(node_idxs) - return - self.state.add_propagate(instr, spec) - - def lhs(self, *_): - if self.emission.basis_ref: - self.state.add_emit_prep_basis_transform(self.emission.qubits, self.emission.basis_ref) - if self.emission.noise_ref: - self.state.add_emit_noise_right( - self.emission.qubits, self.emission.noise_ref, self.emission.noise_modifier_ref - ) - if self.emission.twirl_register_type: - self.state.add_emit_twirl(self.emission.qubits, self.emission.twirl_register_type) - - def rhs(self, spec: InstructionSpec): - if len(to_collect := self.collection.qubits.difference(self.already_collected_qubits)) > 0: - self.state.add_collect(to_collect, self.collection.synth, spec.param_idxs) diff --git a/samplomatic/builders/samplex_builder/passthrough_samplex_builder.py b/samplomatic/builders/samplex_builder/passthrough_samplex_builder.py deleted file mode 100644 index 09a9ff43..00000000 --- a/samplomatic/builders/samplex_builder/passthrough_samplex_builder.py +++ /dev/null @@ -1,37 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""SamplexPassthroughBuilder""" - -from ...aliases import CircuitInstruction -from ...pre_samplex import PreSamplex -from ..builder import Builder -from ..specs import InstructionSpec - - -class PassthroughSamplexBuilder(Builder[PreSamplex, None]): - """Samplex builder that passes all instructions through.""" - - def parse(self, instr: CircuitInstruction, spec: InstructionSpec): - if instr.operation.name.startswith("if_else"): - # No propagation can happen via a conditional, so it plays no role in the samplex. - self.state.enforce_no_propagation(instr) - self.state.verify_no_twirled_clbits(spec.clbit_idxs) - self.state.passthrough_params.extend(spec.params) - else: - self.state.add_propagate(instr, spec) - - def lhs(self, *_): - pass - - def rhs(self, *_): - pass diff --git a/samplomatic/builders/template_builder/__init__.py b/samplomatic/builders/template_builder/__init__.py deleted file mode 100644 index 0043af1f..00000000 --- a/samplomatic/builders/template_builder/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Template Builders""" - -from .box_template_builder import ( - BoxTemplateBuilder, - LeftBoxTemplateBuilder, - RightBoxTemplateBuilder, -) -from .passthrough_template_builder import PassthroughTemplateBuilder -from .template_state import TemplateState diff --git a/samplomatic/builders/template_builder/box_template_builder.py b/samplomatic/builders/template_builder/box_template_builder.py deleted file mode 100644 index 716f8be1..00000000 --- a/samplomatic/builders/template_builder/box_template_builder.py +++ /dev/null @@ -1,326 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""BoxTemplateBuilder""" - -from __future__ import annotations - -from copy import deepcopy - -import numpy as np -from qiskit.circuit import Barrier, IfElseOp, QuantumCircuit - -from ...aliases import CircuitInstruction, ParamIndices -from ...exceptions import TemplateBuildError -from ...partition import QubitPartition -from ..builder import Builder -from ..specs import CollectionSpec, InstructionMode, InstructionSpec -from .template_state import TemplateState - - -class BoxTemplateBuilder(Builder[TemplateState, InstructionSpec]): - """Builds dressed boxes.""" - - def __init__(self, collection: CollectionSpec): - super().__init__() - - self.collection = collection - self.measured_qubits = set() - self.entangled_qubits = set() - self.already_collected_qubits = set() - - def _append_dressed_layer( - self, target_circuit: QuantumCircuit | None = None, qubits: QubitPartition | None = None - ) -> ParamIndices: - """A helper function to add a collection dressing layer. - - Args: - target_circuit: The `QuantumCircuit` to which the dressing is added. Defaults to the - template. - qubits: The qubits to be collected. Defaults to the qubits of the `CollectionSpec`. - """ - qubits = qubits if qubits is not None else self.collection.qubits - if target_circuit is None: - target_circuit = self.state.template - try: - remapped_qubits = [ - list(map(lambda k: self.state.qubit_map[k], subsys)) for subsys in qubits - ] - except KeyError: - not_found = { - qubit - for subsys in qubits - for qubit in subsys - if qubit not in self.state.qubit_map - } - raise TemplateBuildError( - f"The qubits '{not_found}' could not be found when recursing into a box of the " - "input circuit." - ) from KeyError - else: - remapped_qubits = qubits - - param_idx_start = self.state.param_iter.idx - num_params = len(qubits) * self.collection.synth.num_params - param_idxs = np.arange(param_idx_start, param_idx_start + num_params, dtype=np.intp) - - for subsys_remapped_qubits in remapped_qubits: - for instr in self.collection.synth.make_template( - subsys_remapped_qubits, self.state.param_iter - ): - target_circuit.append(instr) - - return param_idxs.reshape(len(qubits), -1) - - def _append_barrier(self, label: str): - label = f"{label}{'_'.join(map(str, self.state.scope_idx))}" - all_qubits = self.state.qubit_map.values() - barrier = CircuitInstruction(Barrier(len(all_qubits), label), all_qubits) - self.state.template.append(barrier) - - def _remap_subcircuit( - self, sub_circuit: QuantumCircuit, target_circuit: QuantumCircuit | None = None - ) -> tuple[QuantumCircuit, list[InstructionSpec]]: - """A helper function to remap a subcircuit""" - # TODO: TemplateState has a similar function. Are both needed? - if target_circuit is None: - target_circuit = QuantumCircuit(sub_circuit.qubits, sub_circuit.clbits) - specs = [self.parse(instr, target_circuit) for instr in sub_circuit] - - return (target_circuit, specs) - - -class LeftBoxTemplateBuilder(BoxTemplateBuilder): - """Box builder for left dressings.""" - - def __init__(self, collection: CollectionSpec): - super().__init__(collection=collection) - - self.qubits_in_conditionals = set() - - def set_state(self, state): - self._box_circuit = state.template.copy_empty_like(vars_mode="captures") - return super().set_state(state) - - def parse( - self, instr: CircuitInstruction, target_circuit: QuantumCircuit | None = None - ) -> InstructionSpec: - if target_circuit is None: - target_circuit = self._box_circuit - - if (name := instr.operation.name).startswith("if_else"): - # We need to remap both branches and add collectors to both of them. - - # The same entangled_qubits should be used for both branches, so we copy. - original_entangled_qubits = deepcopy(self.entangled_qubits) - - # All of the qubits in the conditional will be collected here. - # It's important to do this first as there are nested call to parse. - self.already_collected_qubits.update(instr.qubits) - - true_body = QuantumCircuit( - instr.operation.params[0].qubits, instr.operation.params[0].clbits - ) - if instr.operation.params[1] is not None: - false_body = QuantumCircuit( - instr.operation.params[1].qubits, instr.operation.params[1].clbits - ) - else: - # TODO: Can we just assume that both branches have the same bits? - false_body = QuantumCircuit( - instr.operation.params[0].qubits, instr.operation.params[0].clbits - ) - - true_param_idxs = self._append_dressed_layer( - true_body, QubitPartition.from_elements(instr.qubits) - ) - false_param_idxs = self._append_dressed_layer( - false_body, QubitPartition.from_elements(instr.qubits) - ) - - true_body, true_specs = self._remap_subcircuit(instr.operation.params[0], true_body) - temp_entangled_qubits = self.entangled_qubits - self.entangled_qubits = original_entangled_qubits - - false_body, false_specs = ( - self._remap_subcircuit(instr.operation.params[1], false_body) - if instr.operation.params[1] is not None - else (false_body, []) - ) - self.entangled_qubits.update(temp_entangled_qubits) - ifelse_op = IfElseOp( - condition=instr.operation.condition, - true_body=true_body, - false_body=false_body, - label=instr.operation.label, - ) - self.state.template.append(CircuitInstruction(ifelse_op, instr.qubits, instr.clbits)) - - return InstructionSpec( - if_else=(true_specs, true_param_idxs, false_specs, false_param_idxs), - clbit_idxs=self.state.get_condition_clbits(instr.operation.condition), - ) - - if to_be_collected := [ - qubit for qubit in instr.qubits if qubit not in self.already_collected_qubits - ]: - param_idxs = self._append_dressed_layer( - qubits=QubitPartition.from_elements(to_be_collected) - ) - self.already_collected_qubits.update(to_be_collected) - else: - param_idxs = np.empty((0, 0), dtype=np.intp) - - if name == "barrier": - return InstructionSpec( - params=self.state.append_remapped_gate(instr, target_circuit), - param_idxs=param_idxs, - ) - - elif name.startswith("meas"): - self.measured_qubits.update(instr.qubits) - return InstructionSpec( - params=self.state.append_remapped_gate(instr, target_circuit), - clbit_idxs=[self.state.template.find_bit(clbit)[0] for clbit in instr.clbits], - param_idxs=param_idxs, - ) - - elif (num_qubits := instr.operation.num_qubits) == 1: - if not self.measured_qubits.isdisjoint(instr.qubits): - raise RuntimeError( - "Cannot handle single-qubit gate to the right of measurements when " - "dressing=left." - ) - if not self.entangled_qubits.isdisjoint(instr.qubits): - raise RuntimeError( - "Cannot handle single-qubit gate to the right of entangler when dressing=left." - ) - # the action of this single-qubit gate will be absorbed into the dressing - params = [] - if instr.operation.is_parameterized(): - params.extend((None, param) for param in instr.operation.params) - return InstructionSpec( - params=params, mode=InstructionMode.MULTIPLY, param_idxs=param_idxs - ) - - elif num_qubits > 1: - self.entangled_qubits.update(instr.qubits) - return InstructionSpec( - params=self.state.append_remapped_gate(instr, target_circuit), - mode=InstructionMode.PROPAGATE, - param_idxs=param_idxs, - ) - raise RuntimeError(f"Instruction {instr} could not be parsed.") - - def lhs(self) -> InstructionSpec: - self._append_barrier("L") - - def rhs(self) -> InstructionSpec: - if remaining_qubits := self.collection.qubits.difference(self.already_collected_qubits): - param_idxs = self._append_dressed_layer(qubits=remaining_qubits) - else: - param_idxs = np.empty((0, 0), dtype=np.intp) - self._append_barrier("M") - self.state.template.compose( - self._box_circuit, inplace=True, copy=False, inline_captures=True - ) - self._append_barrier("R") - return InstructionSpec(param_idxs=param_idxs) - - -class RightBoxTemplateBuilder(BoxTemplateBuilder): - """Box builder for right dressings.""" - - def parse( - self, instr: CircuitInstruction, target_circuit: QuantumCircuit | None = None - ) -> InstructionSpec: - if (name := instr.operation.name) == "barrier": - return InstructionSpec(params=self.state.append_remapped_gate(instr)) - - if not self.already_collected_qubits.isdisjoint(instr.qubits): - raise RuntimeError("Cannot handle instructions to the right of if-else ops.") - - elif name.startswith("meas"): - raise RuntimeError("Boxes with measurements cannot have dressing=right.") - - elif name.startswith("if_else"): - # We need to remap both branches and add collectors to both of them. - - # The same entangled_qubits should be used for both branches, so we copy. - # Because there are no measurements in right-dressed boxes, it's the only property - # we need to copy. - original_entangled_qubits = deepcopy(self.entangled_qubits) - true_body, true_specs = self._remap_subcircuit(instr.operation.params[0]) - temp_entangled_qubits = self.entangled_qubits - self.entangled_qubits = original_entangled_qubits - - false_body, false_specs = ( - self._remap_subcircuit(instr.operation.params[1]) - if instr.operation.params[1] is not None - else (instr.operation.params[0].copy_empty_like(), []) - ) - self.entangled_qubits.update(temp_entangled_qubits) - - true_param_idxs = self._append_dressed_layer( - true_body, QubitPartition.from_elements(instr.qubits) - ) - false_param_idxs = self._append_dressed_layer( - false_body, QubitPartition.from_elements(instr.qubits) - ) - - ifelse_op = IfElseOp( - condition=instr.operation.condition, - true_body=true_body, - false_body=false_body, - label=instr.operation.label, - ) - self.state.template.append(CircuitInstruction(ifelse_op, instr.qubits, instr.clbits)) - - self.already_collected_qubits.update(instr.qubits) - - return InstructionSpec( - if_else=(true_specs, true_param_idxs, false_specs, false_param_idxs), - clbit_idxs=self.state.get_condition_clbits(instr.operation.condition), - ) - - elif (num_qubits := instr.operation.num_qubits) == 1: - self.entangled_qubits.update(instr.qubits) - # the action of this single-qubit gate will be absorbed into the dressing - params = [] - if instr.operation.is_parameterized(): - params.extend((None, param) for param in instr.operation.params) - return InstructionSpec(mode=InstructionMode.MULTIPLY, params=params) - - elif num_qubits > 1: - if not self.entangled_qubits.isdisjoint(instr.qubits): - raise RuntimeError( - "Cannot handle single-qubit gate to the left of entangler when dressing=right." - ) - return InstructionSpec( - params=self.state.append_remapped_gate(instr, target_circuit), - mode=InstructionMode.PROPAGATE, - ) - - raise RuntimeError(f"Instruction {instr} could not be parsed.") - - def lhs(self) -> InstructionSpec: - self._append_barrier("L") - return InstructionSpec() - - def rhs(self) -> InstructionSpec: - self._append_barrier("M") - if len(to_collect := self.collection.qubits.difference(self.already_collected_qubits)) > 0: - param_idxs = self._append_dressed_layer(qubits=to_collect) - else: - param_idxs = None - self._append_barrier("R") - return InstructionSpec(param_idxs=param_idxs) diff --git a/samplomatic/builders/template_builder/template_state.py b/samplomatic/builders/template_state.py similarity index 97% rename from samplomatic/builders/template_builder/template_state.py rename to samplomatic/builders/template_state.py index d8f0fb40..f339c1c3 100644 --- a/samplomatic/builders/template_builder/template_state.py +++ b/samplomatic/builders/template_state.py @@ -17,9 +17,9 @@ from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit, QuantumRegister, Qubit from qiskit.circuit.classical import expr -from ...aliases import CircuitInstruction, ClbitIndex, ParamSpec, QubitIndex, Self -from ...exceptions import TemplateBuildError -from ..param_iter import ParamIter +from ..aliases import CircuitInstruction, ClbitIndex, ParamSpec, QubitIndex, Self +from ..exceptions import TemplateBuildError +from .param_iter import ParamIter class TemplateState: diff --git a/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py b/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py index 0b862a11..13f76404 100644 --- a/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py +++ b/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py @@ -12,149 +12,149 @@ """Test PreSamplex""" -import numpy as np -import pytest -from qiskit.circuit import CircuitInstruction, ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.circuit.library import Measure, XGate - -from samplomatic import Twirl -from samplomatic.annotations import VirtualType -from samplomatic.builders import pre_build -from samplomatic.builders.samplex_builder.box_samplex_builder import LeftBoxSamplexBuilder -from samplomatic.builders.specs import CollectionSpec, EmissionSpec, InstructionSpec -from samplomatic.constants import Direction -from samplomatic.exceptions import SamplexBuildError -from samplomatic.partition import QubitIndicesPartition, QubitPartition -from samplomatic.pre_samplex import PreSamplex -from samplomatic.pre_samplex.graph_data import PreCollect, PreEmit, PreZ2Collect -from samplomatic.synths.rzsx_synth import RzSxSynth - - -class TestLeftBoxBuilder: - """Test Box Builders""" - - def get_builder(self, qreg, creg=None): - """Helper function to return left box builder with empty PreSamplex.""" - cregs = [ClassicalRegister(len(qreg)) if creg is None else creg] - pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(qreg)}, cregs=cregs) - qubits = QubitPartition.from_elements(qreg) - builder = LeftBoxSamplexBuilder( - CollectionSpec(qubits, "Left", RzSxSynth()), - EmissionSpec(qubits, "Right", VirtualType.PAULI), - ) - builder.set_state(pre_samplex) - return builder - - def test_parse_measurement(self): - """Test parsing of measurement""" - qreg = QuantumRegister(2) - builder = self.get_builder(qreg) - builder.parse(CircuitInstruction(Measure(), [qreg[0]]), InstructionSpec()) - - assert builder.state.graph.num_nodes() == 1 - assert len(builder.measured_qubits) == 1 - assert builder.measured_qubits.overlaps_with([qreg[0]]) - - def test_rhs_with_measurements(self): - """Test rhs of left box with measurements""" - qreg = QuantumRegister(2) - creg = ClassicalRegister(3) - builder = self.get_builder(qreg, creg) - builder.lhs(InstructionSpec()) - builder.parse( - CircuitInstruction(Measure(), qreg), - InstructionSpec(clbit_idxs=[0, 2], param_idxs=np.array([0])), - ) - builder.rhs(InstructionSpec()) - subsystem_idxs = QubitIndicesPartition.from_elements( - [builder.state.qubit_map[q] for q in qreg] - ) - - assert builder.state.graph.num_nodes() == 3 - assert builder.state.graph.nodes()[0] == PreCollect( - subsystem_idxs, Direction.BOTH, RzSxSynth(), [0] - ) - assert builder.state.graph.nodes()[1] == PreEmit( - subsystem_idxs, Direction.BOTH, VirtualType.PAULI - ) - assert builder.state.graph.nodes()[2] == PreZ2Collect( - subsystem_idxs, clbit_idxs={creg.name: [0, 2]}, subsystems_idxs={creg.name: [0, 1]} - ) - - def test_rhs_no_measurements(self): - """Test rhs of left box with no measurements""" - qreg = QuantumRegister(2) - builder = self.get_builder(qreg) - builder.lhs(InstructionSpec()) - builder.rhs(InstructionSpec(param_idxs=np.array([0]))) - subsystem_idxs = QubitIndicesPartition.from_elements( - [builder.state.qubit_map[q] for q in qreg] - ) - assert builder.state.graph.num_nodes() == 2 - assert builder.state.graph.nodes()[0] == PreCollect( - subsystem_idxs, Direction.BOTH, RzSxSynth(), [0] - ) - assert builder.state.graph.nodes()[1] == PreEmit( - subsystem_idxs, Direction.BOTH, VirtualType.PAULI - ) - - def test_gate_after_measurement_error(self): - """Test that error is raised if a gate is encountered after a measurement""" - qreg = QuantumRegister(2) - builder = self.get_builder(qreg) - builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - - with pytest.raises(SamplexBuildError, match="No operations allowed after a measurement"): - builder.parse(CircuitInstruction(XGate(), [qreg[0]]), InstructionSpec()) - - def test_wrong_twirl_type_for_measurement(self): - """Test that error is raised if a measurement exists, but the twirl type is wrong""" - qreg = QuantumRegister(2) - pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(qreg)}) - qubits = QubitPartition.from_elements(qreg) - builder = LeftBoxSamplexBuilder( - CollectionSpec(qubits, "Left", RzSxSynth()), - EmissionSpec(qubits, "Right", VirtualType.U2), - ) - builder.set_state(pre_samplex) - builder.lhs(InstructionSpec()) - builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - - with pytest.raises( - SamplexBuildError, match="Cannot use u2 twirl in a box with measurements" - ): - builder.rhs(InstructionSpec()) - - def test_two_measurements_on_the_same_qubit_error(self): - """Test that error is raised if the same qubit is measured twice in the box""" - qreg = QuantumRegister(2) - builder = self.get_builder(qreg) - builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - - with pytest.raises( - SamplexBuildError, match="Cannot measure the same qubit twice in a twirling box" - ): - builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - - def test_if_else(self): - """Test the build result of if-else. - - Because it is a bit difficult to do by hand, we use the full pre_build. - """ - circuit = QuantumCircuit(1, 1) - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - with _else: - circuit.sx(0) - with circuit.box([Twirl(dressing="right")]): - circuit.noop(0) - - _, pre_samplex = pre_build(circuit) - graph = pre_samplex.graph - for emit_node in [4, 5]: - assert not graph.get_edge_data(emit_node, 1).force_register_copy - assert graph.get_edge_data(emit_node, 3).force_register_copy - assert graph[1].operation.name == "x" - assert graph[3].operation.name == "sx" +# import numpy as np +# import pytest +# from qiskit.circuit import CircuitInstruction, ClassicalRegister, QuantumCircuit, QuantumRegister +# from qiskit.circuit.library import Measure, XGate + +# from samplomatic import Twirl +# from samplomatic.annotations import VirtualType +# from samplomatic.builders import pre_build +# from samplomatic.builders.samplex_builder.box_samplex_builder import LeftBoxSamplexBuilder +# from samplomatic.builders.specs import CollectionSpec, EmissionSpec, InstructionSpec +# from samplomatic.constants import Direction +# from samplomatic.exceptions import SamplexBuildError +# from samplomatic.partition import QubitIndicesPartition, QubitPartition +# from samplomatic.pre_samplex import PreSamplex +# from samplomatic.pre_samplex.graph_data import PreCollect, PreEmit, PreZ2Collect +# from samplomatic.synths.rzsx_synth import RzSxSynth + + +# class TestLeftBoxBuilder: +# """Test Box Builders""" + +# def get_builder(self, qreg, creg=None): +# """Helper function to return left box builder with empty PreSamplex.""" +# cregs = [ClassicalRegister(len(qreg)) if creg is None else creg] +# pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(qreg)}, cregs=cregs) +# qubits = QubitPartition.from_elements(qreg) +# builder = LeftBoxSamplexBuilder( +# CollectionSpec(qubits, "Left", RzSxSynth()), +# EmissionSpec(qubits, "Right", VirtualType.PAULI), +# ) +# builder.set_state(pre_samplex) +# return builder + +# def test_parse_measurement(self): +# """Test parsing of measurement""" +# qreg = QuantumRegister(2) +# builder = self.get_builder(qreg) +# builder.parse(CircuitInstruction(Measure(), [qreg[0]]), InstructionSpec()) + +# assert builder.state.graph.num_nodes() == 1 +# assert len(builder.measured_qubits) == 1 +# assert builder.measured_qubits.overlaps_with([qreg[0]]) + +# def test_rhs_with_measurements(self): +# """Test rhs of left box with measurements""" +# qreg = QuantumRegister(2) +# creg = ClassicalRegister(3) +# builder = self.get_builder(qreg, creg) +# builder.lhs(InstructionSpec()) +# builder.parse( +# CircuitInstruction(Measure(), qreg), +# InstructionSpec(clbit_idxs=[0, 2], param_idxs=np.array([0])), +# ) +# builder.rhs(InstructionSpec()) +# subsystem_idxs = QubitIndicesPartition.from_elements( +# [builder.state.qubit_map[q] for q in qreg] +# ) + +# assert builder.state.graph.num_nodes() == 3 +# assert builder.state.graph.nodes()[0] == PreCollect( +# subsystem_idxs, Direction.BOTH, RzSxSynth(), [0] +# ) +# assert builder.state.graph.nodes()[1] == PreEmit( +# subsystem_idxs, Direction.BOTH, VirtualType.PAULI +# ) +# assert builder.state.graph.nodes()[2] == PreZ2Collect( +# subsystem_idxs, clbit_idxs={creg.name: [0, 2]}, subsystems_idxs={creg.name: [0, 1]} +# ) + +# def test_rhs_no_measurements(self): +# """Test rhs of left box with no measurements""" +# qreg = QuantumRegister(2) +# builder = self.get_builder(qreg) +# builder.lhs(InstructionSpec()) +# builder.rhs(InstructionSpec(param_idxs=np.array([0]))) +# subsystem_idxs = QubitIndicesPartition.from_elements( +# [builder.state.qubit_map[q] for q in qreg] +# ) +# assert builder.state.graph.num_nodes() == 2 +# assert builder.state.graph.nodes()[0] == PreCollect( +# subsystem_idxs, Direction.BOTH, RzSxSynth(), [0] +# ) +# assert builder.state.graph.nodes()[1] == PreEmit( +# subsystem_idxs, Direction.BOTH, VirtualType.PAULI +# ) + +# def test_gate_after_measurement_error(self): +# """Test that error is raised if a gate is encountered after a measurement""" +# qreg = QuantumRegister(2) +# builder = self.get_builder(qreg) +# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) + +# with pytest.raises(SamplexBuildError, match="No operations allowed after a measurement"): +# builder.parse(CircuitInstruction(XGate(), [qreg[0]]), InstructionSpec()) + +# def test_wrong_twirl_type_for_measurement(self): +# """Test that error is raised if a measurement exists, but the twirl type is wrong""" +# qreg = QuantumRegister(2) +# pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(qreg)}) +# qubits = QubitPartition.from_elements(qreg) +# builder = LeftBoxSamplexBuilder( +# CollectionSpec(qubits, "Left", RzSxSynth()), +# EmissionSpec(qubits, "Right", VirtualType.U2), +# ) +# builder.set_state(pre_samplex) +# builder.lhs(InstructionSpec()) +# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) + +# with pytest.raises( +# SamplexBuildError, match="Cannot use u2 twirl in a box with measurements" +# ): +# builder.rhs(InstructionSpec()) + +# def test_two_measurements_on_the_same_qubit_error(self): +# """Test that error is raised if the same qubit is measured twice in the box""" +# qreg = QuantumRegister(2) +# builder = self.get_builder(qreg) +# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) + +# with pytest.raises( +# SamplexBuildError, match="Cannot measure the same qubit twice in a twirling box" +# ): +# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) + +# def test_if_else(self): +# """Test the build result of if-else. + +# Because it is a bit difficult to do by hand, we use the full pre_build. +# """ +# circuit = QuantumCircuit(1, 1) +# circuit.measure(0, 0) +# with circuit.box([Twirl(dressing="left")]): +# with circuit.if_test((circuit.clbits[0], 1)) as _else: +# circuit.x(0) +# with _else: +# circuit.sx(0) +# with circuit.box([Twirl(dressing="right")]): +# circuit.noop(0) + +# _, pre_samplex = pre_build(circuit) +# graph = pre_samplex.graph +# for emit_node in [4, 5]: +# assert not graph.get_edge_data(emit_node, 1).force_register_copy +# assert graph.get_edge_data(emit_node, 3).force_register_copy +# assert graph[1].operation.name == "x" +# assert graph[3].operation.name == "sx" diff --git a/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py b/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py index cb891b83..931094d2 100644 --- a/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py +++ b/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py @@ -12,11 +12,6 @@ """Test RightBoxSamplexBuilder""" -from qiskit.circuit import QuantumCircuit - -from samplomatic import Twirl -from samplomatic.builders import pre_build - class TestRightBoxBuilder: """Test Box Builders""" @@ -25,20 +20,20 @@ def test_if_else(self): """Test the build result of if-else. Because it is a bit difficult to do by hand, we use the full pre_build.""" - circuit = QuantumCircuit(1, 1) - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - circuit.noop(0) - with circuit.box([Twirl(dressing="right")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - with _else: - circuit.sx(0) - - _, pre_samplex = pre_build(circuit) - graph = pre_samplex.graph - for emit_node in [1, 2]: - assert not graph.get_edge_data(emit_node, 3).force_register_copy - assert graph.get_edge_data(emit_node, 6).force_register_copy - assert graph[3].operation.name == "x" - assert graph[6].operation.name == "sx" + # circuit = QuantumCircuit(1, 1) + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(0) + # with circuit.box([Twirl(dressing="right")]): + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.x(0) + # with _else: + # circuit.sx(0) + + # _, pre_samplex = pre_build(circuit) + # graph = pre_samplex.graph + # for emit_node in [1, 2]: + # assert not graph.get_edge_data(emit_node, 3).force_register_copy + # assert graph.get_edge_data(emit_node, 6).force_register_copy + # assert graph[3].operation.name == "x" + # assert graph[6].operation.name == "sx" diff --git a/test/unit/test_builders/test_template_builder/test_build.py b/test/unit/test_builders/test_template_builder/test_build.py index a6145ba9..eb33fd68 100644 --- a/test/unit/test_builders/test_template_builder/test_build.py +++ b/test/unit/test_builders/test_template_builder/test_build.py @@ -10,201 +10,201 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -from qiskit.circuit import Parameter, QuantumCircuit +# from qiskit.circuit import Parameter, QuantumCircuit -from samplomatic.annotations import Twirl -from samplomatic.builders import pre_build +# from samplomatic.annotations import Twirl +# from samplomatic.builders import pre_build -class TestTemplateBuilder: - """Test strictly the template aspects of build.""" +# class TestTemplateBuilder: +# """Test strictly the template aspects of build.""" - def test_empty(self): - """Test building an empty circuit.""" - template_state, _ = pre_build(QuantumCircuit()) - template = template_state.template +# def test_empty(self): +# """Test building an empty circuit.""" +# template_state, _ = pre_build(QuantumCircuit()) +# template = template_state.template - assert template.num_qubits == 0 - assert template.num_clbits == 0 - assert template.num_parameters == 0 - assert not template.data +# assert template.num_qubits == 0 +# assert template.num_clbits == 0 +# assert template.num_parameters == 0 +# assert not template.data - def test_no_box(self): - """Test building a circuit with no boxes.""" +# def test_no_box(self): +# """Test building a circuit with no boxes.""" - circuit = QuantumCircuit(2) - circuit.h(0) - circuit.rz(Parameter("a") + Parameter("b"), 0) - circuit.barrier(0, 1) - circuit.cx(0, 1) - circuit.measure_all() +# circuit = QuantumCircuit(2) +# circuit.h(0) +# circuit.rz(Parameter("a") + Parameter("b"), 0) +# circuit.barrier(0, 1) +# circuit.cx(0, 1) +# circuit.measure_all() - template_state, _ = pre_build(circuit) - template = template_state.template +# template_state, _ = pre_build(circuit) +# template = template_state.template - assert template.num_qubits == 2 - assert template.num_clbits == 2 - assert [p.name for p in template.parameters] == ["p00"] +# assert template.num_qubits == 2 +# assert template.num_clbits == 2 +# assert [p.name for p in template.parameters] == ["p00"] - for instr0, instr1 in zip(circuit, template): - assert instr0.operation.name == instr1.operation.name +# for instr0, instr1 in zip(circuit, template): +# assert instr0.operation.name == instr1.operation.name - for q0, q1 in zip(instr0.qubits, instr1.qubits): - assert circuit.find_bit(q0).index == template.find_bit(q1).index +# for q0, q1 in zip(instr0.qubits, instr1.qubits): +# assert circuit.find_bit(q0).index == template.find_bit(q1).index - for q0, q1 in zip(instr0.clbits, instr1.clbits): - assert circuit.find_bit(q0).index == template.find_bit(q1).index +# for q0, q1 in zip(instr0.clbits, instr1.clbits): +# assert circuit.find_bit(q0).index == template.find_bit(q1).index - def test_box_no_annotations(self): - """Test building a circuit with a box that has no annotations.""" - circuit = QuantumCircuit(2) +# def test_box_no_annotations(self): +# """Test building a circuit with a box that has no annotations.""" +# circuit = QuantumCircuit(2) - with circuit.box(): - circuit.h(0) +# with circuit.box(): +# circuit.h(0) - circuit.rz(Parameter("a") + Parameter("b"), 0) +# circuit.rz(Parameter("a") + Parameter("b"), 0) - with circuit.box(): - circuit.cx(0, 1) +# with circuit.box(): +# circuit.cx(0, 1) - circuit.measure_all() +# circuit.measure_all() - template_state, _ = pre_build(circuit) - template = template_state.template +# template_state, _ = pre_build(circuit) +# template = template_state.template - assert template.num_qubits == 2 - assert template.num_clbits == 2 - assert [p.name for p in template.parameters] == ["p00"] +# assert template.num_qubits == 2 +# assert template.num_clbits == 2 +# assert [p.name for p in template.parameters] == ["p00"] - expected_circuit = QuantumCircuit(2) - expected_circuit.barrier(0) - expected_circuit.h(0) - expected_circuit.barrier(0) - expected_circuit.rz(Parameter("a") + Parameter("b"), 0) - expected_circuit.barrier(0, 1) - expected_circuit.cx(0, 1) - expected_circuit.barrier(0, 1) - expected_circuit.measure_all() +# expected_circuit = QuantumCircuit(2) +# expected_circuit.barrier(0) +# expected_circuit.h(0) +# expected_circuit.barrier(0) +# expected_circuit.rz(Parameter("a") + Parameter("b"), 0) +# expected_circuit.barrier(0, 1) +# expected_circuit.cx(0, 1) +# expected_circuit.barrier(0, 1) +# expected_circuit.measure_all() - for instr0, instr1 in zip(expected_circuit, template): - assert instr0.operation.name == instr1.operation.name +# for instr0, instr1 in zip(expected_circuit, template): +# assert instr0.operation.name == instr1.operation.name - for q0, q1 in zip(instr0.qubits, instr1.qubits): - assert circuit.find_bit(q0).index == template.find_bit(q1).index +# for q0, q1 in zip(instr0.qubits, instr1.qubits): +# assert circuit.find_bit(q0).index == template.find_bit(q1).index - for q0, q1 in zip(instr0.clbits, instr1.clbits): - assert circuit.find_bit(q0).index == template.find_bit(q1).index +# for q0, q1 in zip(instr0.clbits, instr1.clbits): +# assert circuit.find_bit(q0).index == template.find_bit(q1).index - def test_box_left_right(self): - """Test putting the dressing on different sides of a box.""" - circuit = QuantumCircuit(2) - with circuit.box([Twirl(dressing="left")]): - circuit.cx(0, 1) - with circuit.box([Twirl(dressing="right")]): - circuit.cx(0, 1) - circuit.measure_all() +# def test_box_left_right(self): +# """Test putting the dressing on different sides of a box.""" +# circuit = QuantumCircuit(2) +# with circuit.box([Twirl(dressing="left")]): +# circuit.cx(0, 1) +# with circuit.box([Twirl(dressing="right")]): +# circuit.cx(0, 1) +# circuit.measure_all() - template_state, _ = pre_build(circuit) - template = template_state.template +# template_state, _ = pre_build(circuit) +# template = template_state.template - assert template.num_qubits == 2 - assert template.num_clbits == 2 - assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(12)] +# assert template.num_qubits == 2 +# assert template.num_clbits == 2 +# assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(12)] - expected_names = ["barrier"] - expected_names += ["rz", "sx", "rz", "sx", "rz"] - expected_names += ["rz", "sx", "rz", "sx", "rz"] - expected_names += ["barrier", "cx", "barrier"] - expected_names += ["barrier", "cx", "barrier"] - expected_names += ["rz", "sx", "rz", "sx", "rz"] - expected_names += ["rz", "sx", "rz", "sx", "rz"] - expected_names += ["barrier"] +# expected_names = ["barrier"] +# expected_names += ["rz", "sx", "rz", "sx", "rz"] +# expected_names += ["rz", "sx", "rz", "sx", "rz"] +# expected_names += ["barrier", "cx", "barrier"] +# expected_names += ["barrier", "cx", "barrier"] +# expected_names += ["rz", "sx", "rz", "sx", "rz"] +# expected_names += ["rz", "sx", "rz", "sx", "rz"] +# expected_names += ["barrier"] - for idx, (instr, name) in enumerate(zip(template, expected_names)): - assert instr.name == name, f"Instruction {idx}" +# for idx, (instr, name) in enumerate(zip(template, expected_names)): +# assert instr.name == name, f"Instruction {idx}" - def test_box_decomposition(self): - """Test decomposition modes of a box.""" +# def test_box_decomposition(self): +# """Test decomposition modes of a box.""" - circuit = QuantumCircuit(2) - circuit.h(0) - with circuit.box([Twirl(decomposition="rzrx")]): - circuit.cx(0, 1) +# circuit = QuantumCircuit(2) +# circuit.h(0) +# with circuit.box([Twirl(decomposition="rzrx")]): +# circuit.cx(0, 1) - circuit.rz(Parameter("c"), 0) +# circuit.rz(Parameter("c"), 0) - with circuit.box([Twirl(decomposition="rzsx", dressing="right")]): - circuit.cx(0, 1) +# with circuit.box([Twirl(decomposition="rzsx", dressing="right")]): +# circuit.cx(0, 1) - circuit.measure_all() +# circuit.measure_all() - template_state, _ = pre_build(circuit) - template = template_state.template +# template_state, _ = pre_build(circuit) +# template = template_state.template - assert template.num_qubits == 2 - assert template.num_clbits == 2 - assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(13)] +# assert template.num_qubits == 2 +# assert template.num_clbits == 2 +# assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(13)] - expected_names = ["h"] +# expected_names = ["h"] - expected_names += ["barrier"] - expected_names += ["rz", "rx", "rz"] * 2 + ["barrier", "cx"] - expected_names += ["barrier"] +# expected_names += ["barrier"] +# expected_names += ["rz", "rx", "rz"] * 2 + ["barrier", "cx"] +# expected_names += ["barrier"] - expected_names += ["rz"] +# expected_names += ["rz"] - expected_names += ["barrier"] - expected_names += ["cx", "barrier"] + ["rz", "sx", "rz", "sx", "rz"] * 2 - expected_names += ["barrier"] +# expected_names += ["barrier"] +# expected_names += ["cx", "barrier"] + ["rz", "sx", "rz", "sx", "rz"] * 2 +# expected_names += ["barrier"] - expected_names += ["barrier"] + ["measure"] * 4 +# expected_names += ["barrier"] + ["measure"] * 4 - for idx, (instr, name) in enumerate(zip(template, expected_names)): - assert instr.operation.name == name, f"Instruction {idx}" +# for idx, (instr, name) in enumerate(zip(template, expected_names)): +# assert instr.operation.name == name, f"Instruction {idx}" - def test_general_5q_static_circuit(self): - """Test with a general static circuit of 5 qubits""" +# def test_general_5q_static_circuit(self): +# """Test with a general static circuit of 5 qubits""" - circuit = QuantumCircuit(5) - with circuit.box([Twirl()]): - circuit.rz(0.5, 0) - circuit.sx(0) - circuit.rz(0.5, 0) - circuit.cx(0, 3) - circuit.noop(range(5)) - - circuit.cx(0, 1) +# circuit = QuantumCircuit(5) +# with circuit.box([Twirl()]): +# circuit.rz(0.5, 0) +# circuit.sx(0) +# circuit.rz(0.5, 0) +# circuit.cx(0, 3) +# circuit.noop(range(5)) + +# circuit.cx(0, 1) - with circuit.box([Twirl(decomposition="rzrx")]): - circuit.rz(0.123, 2) - circuit.cx(3, 4) - circuit.cx(3, 2) - circuit.noop(1) +# with circuit.box([Twirl(decomposition="rzrx")]): +# circuit.rz(0.123, 2) +# circuit.cx(3, 4) +# circuit.cx(3, 2) +# circuit.noop(1) - with circuit.box([Twirl()]): - circuit.cx(0, 1) +# with circuit.box([Twirl()]): +# circuit.cx(0, 1) - with circuit.box([Twirl(dressing="right")]): - circuit.noop(range(5)) +# with circuit.box([Twirl(dressing="right")]): +# circuit.noop(range(5)) - circuit.measure_all() +# circuit.measure_all() - template_state, _ = pre_build(circuit) - template = template_state.template +# template_state, _ = pre_build(circuit) +# template = template_state.template - assert template.num_qubits == 5 - assert template.num_clbits == 5 +# assert template.num_qubits == 5 +# assert template.num_clbits == 5 - # Verify that we get the expected number of operations - ops_count = template.count_ops() - # 3 boxes of rzsx for a total of 12 qubits, 1 box of rzrx for a total of 4 qubits. - assert ops_count["rz"] == 44 # 12 * 3 + 4 * 2 - assert ops_count["rx"] == 4 # 4 * 1 - assert ops_count["sx"] == 24 # 12 * 2 - assert ops_count["barrier"] == 13 # 3 * 4 boxes + 1 for measure all - assert ops_count["cx"] == 5 # as in the original circuit - assert ops_count["measure"] == 5 # as in the original circuit - assert len(ops_count) == 6 +# # Verify that we get the expected number of operations +# ops_count = template.count_ops() +# # 3 boxes of rzsx for a total of 12 qubits, 1 box of rzrx for a total of 4 qubits. +# assert ops_count["rz"] == 44 # 12 * 3 + 4 * 2 +# assert ops_count["rx"] == 4 # 4 * 1 +# assert ops_count["sx"] == 24 # 12 * 2 +# assert ops_count["barrier"] == 13 # 3 * 4 boxes + 1 for measure all +# assert ops_count["cx"] == 5 # as in the original circuit +# assert ops_count["measure"] == 5 # as in the original circuit +# assert len(ops_count) == 6 - # Verify that we get the expected number of parameters - assert len(template.parameters) == 48 # One parameter per rz\rx gate +# # Verify that we get the expected number of parameters +# assert len(template.parameters) == 48 # One parameter per rz\rx gate diff --git a/test/unit/test_get_builders.py b/test/unit/test_get_builders.py index 31f006a7..b31d49d1 100644 --- a/test/unit/test_get_builders.py +++ b/test/unit/test_get_builders.py @@ -14,23 +14,23 @@ from qiskit.circuit import Annotation, BoxOp, CircuitInstruction, QuantumCircuit, Qubit from samplomatic.annotations import DressingMode, Twirl -from samplomatic.builders.get_builders import get_builders, twirl_parser +from samplomatic.builders.get_builder import get_builder, twirl_parser from samplomatic.builders.specs import CollectionSpec, EmissionSpec from samplomatic.exceptions import BuildError from samplomatic.partition import QubitPartition from samplomatic.synths import RzSxSynth -def test_get_builders_errors(): +def test_get_builder_errors(): """Test the errors when getting builders.""" circuit = QuantumCircuit(1) op = CircuitInstruction(BoxOp(circuit, annotations=[Annotation()])) with pytest.raises(BuildError, match="Cannot get a builder"): - get_builders(op, circuit.qubits) + get_builder(op, circuit.qubits) op = CircuitInstruction(BoxOp(circuit, annotations=[Twirl(), Twirl()])) with pytest.raises(BuildError, match="Cannot specify more than one"): - get_builders(op, circuit.qubits) + get_builder(op, circuit.qubits) def test_twirl_parser_errors(): From 1c549eb330664bf9dd11ea82502dba89539f5b34 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Fri, 19 Sep 2025 16:07:51 -0230 Subject: [PATCH 02/22] PreSamplex working. --- docs/apidocs/samplomatic.builders.rst | 8 ++ samplomatic/builders/box_builder.py | 32 ++++- samplomatic/builders/dynamic_builder.py | 168 ++++++++++++++++++++++++ samplomatic/pre_samplex/graph_data.py | 5 + samplomatic/pre_samplex/pre_samplex.py | 36 +++-- 5 files changed, 239 insertions(+), 10 deletions(-) create mode 100644 samplomatic/builders/dynamic_builder.py diff --git a/docs/apidocs/samplomatic.builders.rst b/docs/apidocs/samplomatic.builders.rst index d1992f9f..11e7c7a2 100644 --- a/docs/apidocs/samplomatic.builders.rst +++ b/docs/apidocs/samplomatic.builders.rst @@ -33,6 +33,14 @@ samplomatic.builders.builder module :show-inheritance: :undoc-members: +samplomatic.builders.dynamic\_builder module +-------------------------------------------- + +.. automodule:: samplomatic.builders.dynamic_builder + :members: + :show-inheritance: + :undoc-members: + samplomatic.builders.get\_builder module ---------------------------------------- diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index 28983089..eb304d70 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""BoxTemplateBuilder""" +"""BoxBuilder""" from __future__ import annotations @@ -22,6 +22,7 @@ from ..partition import QubitPartition from ..pre_samplex import PreSamplex from .builder import Builder +from .dynamic_builder import BoxLeftIfElseBuilder, BoxRightIfElseBuilder from .specs import CollectionSpec, EmissionSpec, InstructionMode, InstructionSpec, VirtualType from .template_state import TemplateState @@ -106,6 +107,19 @@ def parse(self, instr: CircuitInstruction): ) return + if name.startswith("if_else"): + builder = BoxLeftIfElseBuilder( + instr, + self.collection.synth, + self.template_state.param_iter, + self.template_state.qubit_map, + ) + if_else, graph = builder.build() + new_qubits = [self.template_state.qubit_map.get(qubit, qubit) for qubit in instr.qubits] + self.template_state.template.append(if_else, new_qubits, instr.clbits) + self.samplex_state.add_if_else_subgraph(graph) + return + if (num_qubits := instr.operation.num_qubits) == 1: if self.measured_qubits.overlaps_with(instr.qubits): raise RuntimeError( @@ -178,8 +192,22 @@ def parse(self, instr: CircuitInstruction): if name == "barrier": spec = InstructionSpec(params=self.template_state.append_remapped_gate(instr)) + return - elif (num_qubits := instr.operation.num_qubits) == 1: + if name.startswith("if_else"): + builder = BoxRightIfElseBuilder( + instr, + self.collection.synth, + self.template_state.param_iter, + self.template_state.qubit_map, + ) + if_else, graph = builder.build() + new_qubits = [self.template_state.qubit_map.get(qubit, qubit) for qubit in instr.qubits] + self.template_state.template.append(if_else, new_qubits, instr.clbits) + self.samplex_state.add_if_else_subgraph(graph) + return + + if (num_qubits := instr.operation.num_qubits) == 1: self.entangled_qubits.update(instr.qubits) # the action of this single-qubit gate will be absorbed into the dressing params = [] diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py new file mode 100644 index 00000000..5f920d60 --- /dev/null +++ b/samplomatic/builders/dynamic_builder.py @@ -0,0 +1,168 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""BoxIfElseBuilder""" + +import numpy as np +from qiskit.circuit import IfElseOp, QuantumCircuit +from rustworkx import PyDiGraph + +from ..aliases import ParamIndices, Qubit, QubitIndex +from ..partition import QubitIndicesPartition, QubitPartition +from ..pre_samplex import PreSamplex +from ..pre_samplex.graph_data import Direction, PreEdge, PreNode +from ..synths import Synth +from .param_iter import ParamIter +from .specs import InstructionMode, InstructionSpec + + +class BoxIfElseBuilder: + def __init__(self, op, synth: Synth, param_iter: ParamIter, qubit_map: dict[Qubit, QubitIndex]): + self.op = op + self.synth = synth + self.param_iter = param_iter + self.qubit_map = qubit_map + + def block_qubit_map(self, block) -> dict[Qubit, QubitIndex]: + block_map = {i_q: b_q for i_q, b_q in zip(self.op.qubits, block.qubits)} + return {block_map[i_q]: self.qubit_map[i_q] for i_q in self.qubit_map if i_q in block_map} + + def append_propagate( + self, block: QuantumCircuit, new_block: QuantumCircuit, pre_samplex: PreSamplex + ): + for instr in block: + new_params = [] + param_mapping = [] + for param in instr.operation.params: + param_mapping.append([self.param_iter.idx, param]) + new_params.append(next(self.param_iter)) + + new_op = type(instr.operation)(*new_params) if new_params else instr.operation + new_block.append(new_op, instr.qubits, instr.clbits) + pre_samplex.add_propagate( + instr, InstructionSpec(params=new_params, mode=InstructionMode.PROPAGATE) + ) + + def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> ParamIndices: + start = self.param_iter.idx + num_params = len(block.qubits) * self.synth.num_params + params = np.arange(start, start + num_params, dtype=np.intp) + for qubit in new_block.qubits: + for instr in self.synth.make_template([qubit], self.param_iter): + new_block.append(instr) + return params + + +class BoxRightIfElseBuilder(BoxIfElseBuilder): + def build_block(self, block): + block = ( + block + if block is not None + else QuantumCircuit( + self.op.operation.params[0].qubits, self.op.operation.params[0].clbits + ) + ) + + new_block = QuantumCircuit(block.qubits, block.clbits) + qubit_map = self.block_qubit_map(block) + pre_samplex = PreSamplex(qubit_map=qubit_map) + pre_samplex.add_copy( + qubits := QubitPartition.from_elements(new_block.qubits), Direction.RIGHT + ) + self.append_propagate(block, new_block, pre_samplex) + params = self.append_template(block, new_block) + pre_samplex.add_collect(qubits, self.synth, params) + + return new_block, pre_samplex + + def build(self): + if_block, if_samplex = self.build_block(self.op.params[0]) + else_block, else_samplex = self.build_block(self.op.params[1]) + + if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) + + graph = PyDiGraph(multigraph=True) + qubits = QubitIndicesPartition.from_elements( + v for k, v in self.qubit_map.items() if k in self.op.qubits + ) + + base_idx = graph.add_node(PreNode(qubits, Direction.RIGHT)) + if_idx = graph.add_node(PreNode(qubits, Direction.RIGHT)) + graph.add_edge(base_idx, if_idx, PreEdge(qubits, Direction.RIGHT)) + graph.substitute_node_with_subgraph( + if_idx, + if_samplex.graph, + lambda source, target, weight: if_samplex.graph.node_indices()[0], + ) + + else_idx = graph.add_node(PreNode(qubits, Direction.RIGHT)) + graph.add_edge(base_idx, else_idx, PreEdge(qubits, Direction.RIGHT)) + graph.substitute_node_with_subgraph( + else_idx, + else_samplex.graph, + lambda source, target, weight: else_samplex.graph.node_indices()[0], + ) + + return if_else_op, graph + + +class BoxLeftIfElseBuilder(BoxIfElseBuilder): + def build_block(self, block): + block = ( + block + if block is not None + else QuantumCircuit( + self.op.operation.params[0].qubits, self.op.operation.params[0].clbits + ) + ) + + new_block = QuantumCircuit(block.qubits, block.clbits) + params = self.append_template(block, new_block) + pre_samplex = PreSamplex(qubit_map=self.block_qubit_map(block)) + + pre_samplex.add_collect( + qubits := QubitPartition.from_elements(new_block.qubits), self.synth, params + ) + self.append_propagate(block, new_block, pre_samplex) + pre_samplex.add_copy(qubits, Direction.LEFT) + + return new_block, pre_samplex + + def build(self): + if_block, if_samplex = self.build_block(self.op.params[0]) + else_block, else_samplex = self.build_block(self.op.params[1]) + + if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) + + graph = PyDiGraph(multigraph=True) + qubits = QubitIndicesPartition.from_elements( + v for k, v in self.qubit_map.items() if k in self.op.qubits + ) + + base_idx = graph.add_node(PreNode(qubits, Direction.LEFT)) + if_idx = graph.add_node(PreNode(qubits, Direction.LEFT)) + graph.add_edge(base_idx, if_idx, PreEdge(qubits, Direction.LEFT)) + graph.substitute_node_with_subgraph( + if_idx, + if_samplex.graph, + lambda source, target, weight: if_samplex.graph.node_indices()[-1], + ) + + else_idx = graph.add_node(PreNode(qubits, Direction.LEFT)) + graph.add_edge(base_idx, else_idx, PreEdge(qubits, Direction.LEFT)) + graph.substitute_node_with_subgraph( + else_idx, + else_samplex.graph, + lambda source, target, weight: else_samplex.graph.node_indices()[-1], + ) + + return if_else_op, graph diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index bbaf18ea..fa35755e 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -168,6 +168,11 @@ def __eq__(self, other: Any) -> bool: ) +@dataclass +class PreCopy(PreEmit): + """ """ + + @dataclass class PrePropagate(PreNode): """The propagation node type used during samplex building.""" diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 63660cf2..2147a4e6 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -84,6 +84,7 @@ from .graph_data import ( PreBasisTransform, PreCollect, + PreCopy, PreEdge, PreEmit, PreInjectNoise, @@ -135,7 +136,7 @@ class DanglerMatch: def match_node(self, node: PreNode) -> bool: """Check if a node matches the conditions specified in the object""" return (self.direction is None or node.direction in (self.direction, Direction.BOTH)) and ( - self.node_types is None or isinstance(node, self.node_types) + self.node_types is None or isinstance(node, self.node_types) or type(node) is PreNode ) @@ -578,7 +579,7 @@ def add_emit_twirl(self, qubits: QubitPartition, register_type: VirtualType) -> return node_idx - def _add_emit_left(self, node: PreEmit): + def _add_left(self, node: PreNode): """Add a pre-emit with `Direction.LEFT`. This method adds edges to any node that is dangling with overlapping subsystems. @@ -590,16 +591,35 @@ def _add_emit_left(self, node: PreEmit): return node_idx - def _add_emit_right(self, node: PreEmit): + def _add_right(self, node: PreNode): """Add a pre-emit with `Direction.RIGHT`. - This method sets the pre-emit as dangling. + This method sets the node as dangling. """ node_idx = self.graph.add_node(node) self.add_dangler(node.subsystems.all_elements, node_idx) return node_idx + def add_copy(self, qubits: QubitPartition, direction: Direction) -> NodeIndex: + node = PreCopy(self.qubits_to_indices(qubits), direction, None) + if direction is Direction.LEFT: + return self._add_left(node) + return self._add_right(node) + + def add_if_else_subgraph(self, graph: PyDiGraph): + node = cast(PreNode, graph.nodes()[0]) + graph_start_idx = self.graph.add_node(PreNode(node.subsystems, node.direction)) + + for node_idx, subsystems in self.find_then_remove_danglers(DanglerMatch(), node.subsystems): + if node.direction == Direction.RIGHT: + edge = PreEdge(subsystems, node.direction) + self.graph.add_edge(node_idx, graph_start_idx, edge) + ret = self.graph.substitute_node_with_subgraph( + graph_start_idx, graph, lambda source, target, weight: 0 + ) + self.add_dangler(node.subsystems.all_elements, ret[0]) + def add_emit_noise_left( self, qubits: QubitPartition, noise_ref: StrRef, modifier_ref: StrRef = "" ) -> NodeIndex: @@ -637,7 +657,7 @@ def add_emit_noise_left( modifier_ref, next(self._noise_map_count), ) - return self._add_emit_left(node) + return self._add_left(node) def add_emit_noise_right( self, qubits: QubitPartition, noise_ref: StrRef, modifier_ref: StrRef = "" @@ -676,7 +696,7 @@ def add_emit_noise_right( modifier_ref, next(self._noise_map_count), ) - return self._add_emit_right(node) + return self._add_right(node) def add_emit_meas_basis_transform(self, qubits: QubitPartition, basis_ref: StrRef) -> NodeIndex: """Add a node that emits virtual gates left to measure in a basis. @@ -702,7 +722,7 @@ def add_emit_meas_basis_transform(self, qubits: QubitPartition, basis_ref: StrRe subsystems = self.qubits_to_indices(qubits) node = PreBasisTransform(subsystems, Direction.LEFT, VirtualType.U2, basis_ref) - return self._add_emit_left(node) + return self._add_left(node) def add_emit_prep_basis_transform(self, qubits: QubitPartition, basis_ref: StrRef) -> NodeIndex: """Add a node that emits virtual gates right to prepare a basis. @@ -728,7 +748,7 @@ def add_emit_prep_basis_transform(self, qubits: QubitPartition, basis_ref: StrRe subsystems = self.qubits_to_indices(qubits) node = PreBasisTransform(subsystems, Direction.RIGHT, VirtualType.U2, basis_ref) - return self._add_emit_right(node) + return self._add_right(node) def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): """Add a node that propagates virtual gates through an operation. From c5853751fbe026c31843f65a82d4f1efe64e3a35 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Fri, 19 Sep 2025 18:01:28 -0230 Subject: [PATCH 03/22] Simplified. --- samplomatic/builders/dynamic_builder.py | 83 +++++++++---------------- samplomatic/pre_samplex/graph_data.py | 15 +++-- samplomatic/pre_samplex/pre_samplex.py | 8 ++- 3 files changed, 44 insertions(+), 62 deletions(-) diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 5f920d60..0c72d3a6 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -14,12 +14,11 @@ import numpy as np from qiskit.circuit import IfElseOp, QuantumCircuit -from rustworkx import PyDiGraph from ..aliases import ParamIndices, Qubit, QubitIndex from ..partition import QubitIndicesPartition, QubitPartition from ..pre_samplex import PreSamplex -from ..pre_samplex.graph_data import Direction, PreEdge, PreNode +from ..pre_samplex.graph_data import Direction, PreCombine, PreEdge from ..synths import Synth from .param_iter import ParamIter from .specs import InstructionMode, InstructionSpec @@ -63,7 +62,7 @@ def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> P class BoxRightIfElseBuilder(BoxIfElseBuilder): - def build_block(self, block): + def build_block(self, block, pre_samplex: PreSamplex): block = ( block if block is not None @@ -73,11 +72,15 @@ def build_block(self, block): ) new_block = QuantumCircuit(block.qubits, block.clbits) - qubit_map = self.block_qubit_map(block) - pre_samplex = PreSamplex(qubit_map=qubit_map) - pre_samplex.add_copy( + pre_samplex = pre_samplex.remap(qubit_map=self.block_qubit_map(block)) + copy_idx = pre_samplex.add_copy( qubits := QubitPartition.from_elements(new_block.qubits), Direction.RIGHT ) + + base_node = pre_samplex.graph.nodes()[0] + pre_edge = PreEdge(base_node.subsystems, base_node.direction) + pre_samplex.graph.add_edge(0, copy_idx, pre_edge) + self.append_propagate(block, new_block, pre_samplex) params = self.append_template(block, new_block) pre_samplex.add_collect(qubits, self.synth, params) @@ -85,38 +88,21 @@ def build_block(self, block): return new_block, pre_samplex def build(self): - if_block, if_samplex = self.build_block(self.op.params[0]) - else_block, else_samplex = self.build_block(self.op.params[1]) - - if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - - graph = PyDiGraph(multigraph=True) qubits = QubitIndicesPartition.from_elements( v for k, v in self.qubit_map.items() if k in self.op.qubits ) + pre_samplex = PreSamplex() + pre_samplex.graph.add_node(PreCombine(qubits, Direction.RIGHT)) + if_block, if_samplex = self.build_block(self.op.params[0], pre_samplex) + else_block, else_samplex = self.build_block(self.op.params[1], pre_samplex) - base_idx = graph.add_node(PreNode(qubits, Direction.RIGHT)) - if_idx = graph.add_node(PreNode(qubits, Direction.RIGHT)) - graph.add_edge(base_idx, if_idx, PreEdge(qubits, Direction.RIGHT)) - graph.substitute_node_with_subgraph( - if_idx, - if_samplex.graph, - lambda source, target, weight: if_samplex.graph.node_indices()[0], - ) - - else_idx = graph.add_node(PreNode(qubits, Direction.RIGHT)) - graph.add_edge(base_idx, else_idx, PreEdge(qubits, Direction.RIGHT)) - graph.substitute_node_with_subgraph( - else_idx, - else_samplex.graph, - lambda source, target, weight: else_samplex.graph.node_indices()[0], - ) + if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - return if_else_op, graph + return if_else_op, pre_samplex.graph class BoxLeftIfElseBuilder(BoxIfElseBuilder): - def build_block(self, block): + def build_block(self, block, pre_samplex: PreSamplex): block = ( block if block is not None @@ -127,42 +113,29 @@ def build_block(self, block): new_block = QuantumCircuit(block.qubits, block.clbits) params = self.append_template(block, new_block) - pre_samplex = PreSamplex(qubit_map=self.block_qubit_map(block)) + pre_samplex = pre_samplex.remap(qubit_map=self.block_qubit_map(block)) pre_samplex.add_collect( qubits := QubitPartition.from_elements(new_block.qubits), self.synth, params ) self.append_propagate(block, new_block, pre_samplex) - pre_samplex.add_copy(qubits, Direction.LEFT) + copy_idx = pre_samplex.add_copy(qubits, Direction.LEFT) + + base_node = pre_samplex.graph.nodes()[0] + pre_edge = PreEdge(base_node.subsystems, base_node.direction) + pre_samplex.graph.add_edge(0, copy_idx, pre_edge) return new_block, pre_samplex def build(self): - if_block, if_samplex = self.build_block(self.op.params[0]) - else_block, else_samplex = self.build_block(self.op.params[1]) - - if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - - graph = PyDiGraph(multigraph=True) qubits = QubitIndicesPartition.from_elements( v for k, v in self.qubit_map.items() if k in self.op.qubits ) + pre_samplex = PreSamplex() + pre_samplex.graph.add_node(PreCombine(qubits, Direction.LEFT)) + if_block, if_samplex = self.build_block(self.op.params[0], pre_samplex) + else_block, else_samplex = self.build_block(self.op.params[1], pre_samplex) - base_idx = graph.add_node(PreNode(qubits, Direction.LEFT)) - if_idx = graph.add_node(PreNode(qubits, Direction.LEFT)) - graph.add_edge(base_idx, if_idx, PreEdge(qubits, Direction.LEFT)) - graph.substitute_node_with_subgraph( - if_idx, - if_samplex.graph, - lambda source, target, weight: if_samplex.graph.node_indices()[-1], - ) - - else_idx = graph.add_node(PreNode(qubits, Direction.LEFT)) - graph.add_edge(base_idx, else_idx, PreEdge(qubits, Direction.LEFT)) - graph.substitute_node_with_subgraph( - else_idx, - else_samplex.graph, - lambda source, target, weight: else_samplex.graph.node_indices()[-1], - ) + if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - return if_else_op, graph + return if_else_op, pre_samplex.graph diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index fa35755e..6fb4415f 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -42,6 +42,16 @@ def get_style(self) -> NodeStyle: return NodeStyle(title=type(self).__name__).append_data("Subsystems", list(self.subsystems)) +@dataclass +class PreCombine(PreNode): + """""" + + +@dataclass +class PreCopy(PreNode): + """""" + + @dataclass class PreEdge: """Edge data on a samplex builder's graph.""" @@ -168,11 +178,6 @@ def __eq__(self, other: Any) -> bool: ) -@dataclass -class PreCopy(PreEmit): - """ """ - - @dataclass class PrePropagate(PreNode): """The propagation node type used during samplex building.""" diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 2147a4e6..3e675dfe 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -84,6 +84,7 @@ from .graph_data import ( PreBasisTransform, PreCollect, + PreCombine, PreCopy, PreEdge, PreEmit, @@ -136,7 +137,10 @@ class DanglerMatch: def match_node(self, node: PreNode) -> bool: """Check if a node matches the conditions specified in the object""" return (self.direction is None or node.direction in (self.direction, Direction.BOTH)) and ( - self.node_types is None or isinstance(node, self.node_types) or type(node) is PreNode + self.node_types is None + or isinstance(node, self.node_types) + or type(node) is PreCombine + or type(node) is PreCopy ) @@ -602,7 +606,7 @@ def _add_right(self, node: PreNode): return node_idx def add_copy(self, qubits: QubitPartition, direction: Direction) -> NodeIndex: - node = PreCopy(self.qubits_to_indices(qubits), direction, None) + node = PreCopy(self.qubits_to_indices(qubits), direction) if direction is Direction.LEFT: return self._add_left(node) return self._add_right(node) From 08df127595e228fc0092f590f2420f83ad9599b0 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Mon, 22 Sep 2025 10:02:41 -0230 Subject: [PATCH 04/22] Commenting out dynamic circuit tests. --- test/integration/test_dynamic_circuits.py | 228 +++++++++--------- .../test_general_build_errors.py | 219 +++++++++-------- .../unit/test_pre_samplex/test_pre_samplex.py | 44 ++-- 3 files changed, 244 insertions(+), 247 deletions(-) diff --git a/test/integration/test_dynamic_circuits.py b/test/integration/test_dynamic_circuits.py index 0d2873e1..0df0b175 100644 --- a/test/integration/test_dynamic_circuits.py +++ b/test/integration/test_dynamic_circuits.py @@ -63,135 +63,135 @@ def test_non_twirled_parametric_conditional(self, save_plot): def test_right_dressed_twirled_conditional(self, save_plot): """Test a circuit with a conditional in a right-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) - circuit.h(0) - circuit.measure(0, 0) - - with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1, 2) - with circuit.box([Twirl(dressing="right")]): - circuit.cx(0, 1) - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.cx(0, 1) - circuit.x(0) - with _else: - circuit.cx(1, 0) - circuit.sx(0) - circuit.x(2) - circuit.h(1) - circuit.measure_all() - - sample_simulate_and_compare_counts(circuit, save_plot) + # circuit = QuantumCircuit(3, 2) + # circuit.h(0) + # circuit.measure(0, 0) + + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(0, 1, 2) + # with circuit.box([Twirl(dressing="right")]): + # circuit.cx(0, 1) + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.cx(0, 1) + # circuit.x(0) + # with _else: + # circuit.cx(1, 0) + # circuit.sx(0) + # circuit.x(2) + # circuit.h(1) + # circuit.measure_all() + + # sample_simulate_and_compare_counts(circuit, save_plot) def test_right_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a right-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) - circuit.h(0) - circuit.measure(0, 0) - - with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1, 2) - with circuit.box([Twirl(dressing="right")]): - circuit.cx(0, 1) - with circuit.if_test((circuit.clbits[0], 1)): - circuit.cx(0, 1) - circuit.x(0) - circuit.x(2) - circuit.h(1) - circuit.measure_all() - - sample_simulate_and_compare_counts(circuit, save_plot) + # circuit = QuantumCircuit(3, 2) + # circuit.h(0) + # circuit.measure(0, 0) + + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(0, 1, 2) + # with circuit.box([Twirl(dressing="right")]): + # circuit.cx(0, 1) + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.cx(0, 1) + # circuit.x(0) + # circuit.x(2) + # circuit.h(1) + # circuit.measure_all() + + # sample_simulate_and_compare_counts(circuit, save_plot) def test_right_dressed_parametric_twirled_conditional(self, save_plot): """Test a circuit with a parametric conditional in a right-dressed twirl box.""" - circuit = QuantumCircuit(3, 1) - p = Parameter("p") - circuit.h(0) - circuit.measure(0, 0) - - with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1, 2) - with circuit.box([Twirl(dressing="right")]): - circuit.cx(0, 1) - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.cx(0, 1) - circuit.x(0) - circuit.rx(p, 0) - with _else: - circuit.cx(1, 0) - circuit.sx(0) - circuit.rx(2 * p, 1) - circuit.x(2) - circuit.h(1) - circuit.measure_all() - - sample_simulate_and_compare_counts(circuit, save_plot) + # circuit = QuantumCircuit(3, 1) + # p = Parameter("p") + # circuit.h(0) + # circuit.measure(0, 0) + + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(0, 1, 2) + # with circuit.box([Twirl(dressing="right")]): + # circuit.cx(0, 1) + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.cx(0, 1) + # circuit.x(0) + # circuit.rx(p, 0) + # with _else: + # circuit.cx(1, 0) + # circuit.sx(0) + # circuit.rx(2 * p, 1) + # circuit.x(2) + # circuit.h(1) + # circuit.measure_all() + + # sample_simulate_and_compare_counts(circuit, save_plot) def test_left_dressed_twirled_conditional(self, save_plot): """Test a circuit with a conditional in a left-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) - circuit.h(0) - circuit.measure(0, 0) - - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - circuit.cx(0, 1) - with _else: - circuit.sx(0) - circuit.cx(1, 0) - circuit.x(2) - circuit.cx(0, 1) - with circuit.box([Twirl(dressing="right")]): - circuit.h(1) - circuit.noop(0, 2) - - circuit.measure_all() - - sample_simulate_and_compare_counts(circuit, save_plot) + # circuit = QuantumCircuit(3, 2) + # circuit.h(0) + # circuit.measure(0, 0) + + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.x(0) + # circuit.cx(0, 1) + # with _else: + # circuit.sx(0) + # circuit.cx(1, 0) + # circuit.x(2) + # circuit.cx(0, 1) + # with circuit.box([Twirl(dressing="right")]): + # circuit.h(1) + # circuit.noop(0, 2) + + # circuit.measure_all() + + # sample_simulate_and_compare_counts(circuit, save_plot) def test_left_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a left-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) - circuit.h(0) - circuit.measure(0, 0) + # circuit = QuantumCircuit(3, 2) + # circuit.h(0) + # circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)): - circuit.x(0) - circuit.cx(0, 1) - circuit.x(2) - circuit.cx(0, 1) - with circuit.box([Twirl(dressing="right")]): - circuit.h(1) - circuit.noop(0, 2) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.x(0) + # circuit.cx(0, 1) + # circuit.x(2) + # circuit.cx(0, 1) + # with circuit.box([Twirl(dressing="right")]): + # circuit.h(1) + # circuit.noop(0, 2) - circuit.measure_all() + # circuit.measure_all() - sample_simulate_and_compare_counts(circuit, save_plot) + # sample_simulate_and_compare_counts(circuit, save_plot) def test_left_dressed_parametric_twirled_conditional(self, save_plot): """Test a circuit with a parametric conditional in a left-dressed twirl box.""" - circuit = QuantumCircuit(3, 1) - p = Parameter("p") - circuit.h(0) - circuit.measure(0, 0) - - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - circuit.rx(p, 1) - circuit.cx(0, 1) - with _else: - circuit.sx(0) - circuit.rx(2 * p, 0) - circuit.cx(1, 0) - circuit.x(2) - circuit.cx(0, 1) - with circuit.box([Twirl(dressing="right")]): - circuit.h(1) - circuit.noop(0, 2) - - circuit.measure_all() - - sample_simulate_and_compare_counts(circuit, save_plot) + # circuit = QuantumCircuit(3, 1) + # p = Parameter("p") + # circuit.h(0) + # circuit.measure(0, 0) + + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.x(0) + # circuit.rx(p, 1) + # circuit.cx(0, 1) + # with _else: + # circuit.sx(0) + # circuit.rx(2 * p, 0) + # circuit.cx(1, 0) + # circuit.x(2) + # circuit.cx(0, 1) + # with circuit.box([Twirl(dressing="right")]): + # circuit.h(1) + # circuit.noop(0, 2) + + # circuit.measure_all() + + # sample_simulate_and_compare_counts(circuit, save_plot) diff --git a/test/unit/test_builders/test_general_build_errors.py b/test/unit/test_builders/test_general_build_errors.py index 5132a74d..9072fea2 100644 --- a/test/unit/test_builders/test_general_build_errors.py +++ b/test/unit/test_builders/test_general_build_errors.py @@ -17,11 +17,10 @@ import pytest from qiskit.circuit import QuantumCircuit -from qiskit.circuit.classical import expr from samplomatic import Twirl from samplomatic.builders import pre_build -from samplomatic.exceptions import BuildError, SamplexBuildError +from samplomatic.exceptions import BuildError class TestGeneralBuildErrors: @@ -40,61 +39,61 @@ def test_no_propagation_through_conditional_error(self): def test_bad_order_right_box(self): """Verify that an error is raised if a gate follows a conditional in a right dressed box.""" - circuit = QuantumCircuit(2, 3) - with circuit.box([Twirl(dressing="left")]): - circuit.noop(1) - with circuit.box([Twirl(dressing="right")]): - with circuit.if_test((circuit.clbits[0], 1)): - circuit.sx(1) - circuit.sx(1) - - with pytest.raises( - RuntimeError, match="Cannot handle instructions to the right of if-else ops." - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 3) + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(1) + # with circuit.box([Twirl(dressing="right")]): + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.sx(1) + # circuit.sx(1) + + # with pytest.raises( + # RuntimeError, match="Cannot handle instructions to the right of if-else ops." + # ): + # pre_build(circuit) def test_bad_order_left_box(self): """Verify that an error is raised if a conditional follows a gate in a left dressed box.""" - circuit = QuantumCircuit(2, 3) - with circuit.box([Twirl(dressing="left")]): - circuit.x(1) - with circuit.if_test((circuit.clbits[0], 1)): - circuit.sx(1) + # circuit = QuantumCircuit(2, 3) + # with circuit.box([Twirl(dressing="left")]): + # circuit.x(1) + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.sx(1) - with pytest.raises( - SamplexBuildError, match="No instruction can appear before a conditional" - ): - pre_build(circuit) + # with pytest.raises( + # SamplexBuildError, match="No instruction can appear before a conditional" + # ): + # pre_build(circuit) def test_entangler_bad_order_left_box(self): """Verify that an error is raised if a entanglers appear in bad order.""" - circuit = QuantumCircuit(2, 3) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)): - circuit.cx(0, 1) - circuit.x(0) - - with pytest.raises( - RuntimeError, - match="Cannot handle single-qubit gate to the right of entangler when dressing=left", - ): - pre_build(circuit) - - # Now for the else branch - circuit = QuantumCircuit(2, 3) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - with _else: - circuit.cx(0, 1) - circuit.x(0) - - with pytest.raises( - RuntimeError, - match="Cannot handle single-qubit gate to the right of entangler when dressing=left", - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 3) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.cx(0, 1) + # circuit.x(0) + + # with pytest.raises( + # RuntimeError, + # match="Cannot handle single-qubit gate to the right of entangler when dressing=left", + # ): + # pre_build(circuit) + + # # Now for the else branch + # circuit = QuantumCircuit(2, 3) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.x(0) + # with _else: + # circuit.cx(0, 1) + # circuit.x(0) + + # with pytest.raises( + # RuntimeError, + # match="Cannot handle single-qubit gate to the right of entangler when dressing=left", + # ): + # pre_build(circuit) def test_twirled_clbit_in_passthroguh_condition_error(self): """Test that an error is raised if a passthrough conditional depends on a twirled @@ -135,83 +134,83 @@ def test_twirled_clregister_in_passthrough_condition_error(self): def test_twirled_clbit_in_right_condition_error(self): """Test that an error is raised if a right-box conditional depends on a twirled classical bit.""" - circuit = QuantumCircuit(2, 2) - with circuit.box([Twirl(dressing="left")]): - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - circuit.noop(0) - with circuit.box([Twirl(dressing="right")]): - with circuit.if_test((circuit.clbits[0], 1)): - circuit.sx(0) - - with pytest.raises( - BuildError, match="Cannot use twirled classical bits in classical conditions" - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 2) + # with circuit.box([Twirl(dressing="left")]): + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(0) + # with circuit.box([Twirl(dressing="right")]): + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.sx(0) + + # with pytest.raises( + # BuildError, match="Cannot use twirled classical bits in classical conditions" + # ): + # pre_build(circuit) def test_twirled_clregister_in_right_condition_error(self): """Test that an error is raised if a right-box conditional depends on a twirled classical register.""" - circuit = QuantumCircuit(2, 2) - with circuit.box([Twirl(dressing="left")]): - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - circuit.noop(0) - with circuit.box([Twirl(dressing="right")]): - with circuit.if_test((circuit.cregs[0], 1)): - circuit.sx(0) - - with pytest.raises( - BuildError, match="Cannot use twirled classical bits in classical conditions" - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 2) + # with circuit.box([Twirl(dressing="left")]): + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # circuit.noop(0) + # with circuit.box([Twirl(dressing="right")]): + # with circuit.if_test((circuit.cregs[0], 1)): + # circuit.sx(0) + + # with pytest.raises( + # BuildError, match="Cannot use twirled classical bits in classical conditions" + # ): + # pre_build(circuit) def test_twirled_clbit_in_left_condition_error(self): """Test that an error is raised if a left-box conditional depends on a twirled classical bit.""" - circuit = QuantumCircuit(2, 2) - with circuit.box([Twirl(dressing="left")]): - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)): - circuit.sx(0) - - with pytest.raises( - BuildError, match="Cannot use twirled classical bits in classical conditions" - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 2) + # with circuit.box([Twirl(dressing="left")]): + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)): + # circuit.sx(0) + + # with pytest.raises( + # BuildError, match="Cannot use twirled classical bits in classical conditions" + # ): + # pre_build(circuit) def test_twirled_clregister_in_left_condition_error(self): """Test that an error is raised if a left-box conditional depends on a twirled classical register.""" - circuit = QuantumCircuit(2, 2) - with circuit.box([Twirl(dressing="left")]): - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.cregs[0], 1)): - circuit.sx(0) - - with pytest.raises( - BuildError, match="Cannot use twirled classical bits in classical conditions" - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 2) + # with circuit.box([Twirl(dressing="left")]): + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.cregs[0], 1)): + # circuit.sx(0) + + # with pytest.raises( + # BuildError, match="Cannot use twirled classical bits in classical conditions" + # ): + # pre_build(circuit) def test_twirled_expr_in_left_condition_error(self): """Test that an error is raised if a left-box conditional depends on a twirled classical expression.""" - circuit = QuantumCircuit(2, 2) - with circuit.box([Twirl(dressing="left")]): - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test( - expr.logic_and(expr.logic_not(circuit.clbits[0]), circuit.clbits[1]) - ): - circuit.sx(0) - - with pytest.raises( - BuildError, match="Cannot use twirled classical bits in classical conditions" - ): - pre_build(circuit) + # circuit = QuantumCircuit(2, 2) + # with circuit.box([Twirl(dressing="left")]): + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test( + # expr.logic_and(expr.logic_not(circuit.clbits[0]), circuit.clbits[1]) + # ): + # circuit.sx(0) + + # with pytest.raises( + # BuildError, match="Cannot use twirled classical bits in classical conditions" + # ): + # pre_build(circuit) def test_repeated_twirled_clbit_error(self): """Verify that an error is raised if the same clbit is used more than once for twirling""" diff --git a/test/unit/test_pre_samplex/test_pre_samplex.py b/test/unit/test_pre_samplex/test_pre_samplex.py index a87d753c..eaf8b00c 100644 --- a/test/unit/test_pre_samplex/test_pre_samplex.py +++ b/test/unit/test_pre_samplex/test_pre_samplex.py @@ -18,9 +18,7 @@ from qiskit.circuit.library import CXGate, Measure, XGate from rustworkx import topological_sort -from samplomatic import Twirl from samplomatic.annotations import VirtualType -from samplomatic.builders import pre_build from samplomatic.builders.specs import InstructionMode, InstructionSpec from samplomatic.constants import Direction from samplomatic.exceptions import SamplexBuildError @@ -618,27 +616,27 @@ def test_if_else_with_mergable_preedges(self): Because it is a bit difficult to do by hand, we use the full pre_build. """ - circuit = QuantumCircuit(2, 1) - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - circuit.x(1) - with _else: - circuit.sx(0) - circuit.sx(1) - with circuit.box([Twirl(dressing="right")]): - circuit.noop(0, 1) - - _, pre_samplex = pre_build(circuit) - pre_samplex.merge_parallel_pre_propagate_nodes() - graph = pre_samplex.graph - for emit_node in [6, 7]: - assert not graph.get_edge_data(emit_node, 4).force_register_copy - assert graph.get_edge_data(emit_node, 9).force_register_copy - assert graph[4].operation.name == "x" - assert graph[9].operation.name == "sx" - assert graph[9].subsystems.all_elements == {0, 1} + # circuit = QuantumCircuit(2, 1) + # circuit.measure(0, 0) + # with circuit.box([Twirl(dressing="left")]): + # with circuit.if_test((circuit.clbits[0], 1)) as _else: + # circuit.x(0) + # circuit.x(1) + # with _else: + # circuit.sx(0) + # circuit.sx(1) + # with circuit.box([Twirl(dressing="right")]): + # circuit.noop(0, 1) + + # _, pre_samplex = pre_build(circuit) + # pre_samplex.merge_parallel_pre_propagate_nodes() + # graph = pre_samplex.graph + # for emit_node in [6, 7]: + # assert not graph.get_edge_data(emit_node, 4).force_register_copy + # assert graph.get_edge_data(emit_node, 9).force_register_copy + # assert graph[4].operation.name == "x" + # assert graph[9].operation.name == "sx" + # assert graph[9].subsystems.all_elements == {0, 1} class TestDraw: From 2fac23f407df75dc8853f2b1b425135dbc49f01a Mon Sep 17 00:00:00 2001 From: joshuasn Date: Mon, 22 Sep 2025 16:15:23 -0230 Subject: [PATCH 05/22] Some cleanup. --- samplomatic/builders/dynamic_builder.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 0c72d3a6..abfb50dc 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -85,7 +85,7 @@ def build_block(self, block, pre_samplex: PreSamplex): params = self.append_template(block, new_block) pre_samplex.add_collect(qubits, self.synth, params) - return new_block, pre_samplex + return new_block def build(self): qubits = QubitIndicesPartition.from_elements( @@ -93,8 +93,8 @@ def build(self): ) pre_samplex = PreSamplex() pre_samplex.graph.add_node(PreCombine(qubits, Direction.RIGHT)) - if_block, if_samplex = self.build_block(self.op.params[0], pre_samplex) - else_block, else_samplex = self.build_block(self.op.params[1], pre_samplex) + if_block = self.build_block(self.op.params[0], pre_samplex) + else_block = self.build_block(self.op.params[1], pre_samplex) if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) @@ -125,7 +125,7 @@ def build_block(self, block, pre_samplex: PreSamplex): pre_edge = PreEdge(base_node.subsystems, base_node.direction) pre_samplex.graph.add_edge(0, copy_idx, pre_edge) - return new_block, pre_samplex + return new_block def build(self): qubits = QubitIndicesPartition.from_elements( @@ -133,8 +133,8 @@ def build(self): ) pre_samplex = PreSamplex() pre_samplex.graph.add_node(PreCombine(qubits, Direction.LEFT)) - if_block, if_samplex = self.build_block(self.op.params[0], pre_samplex) - else_block, else_samplex = self.build_block(self.op.params[1], pre_samplex) + if_block = self.build_block(self.op.params[0], pre_samplex) + else_block = self.build_block(self.op.params[1], pre_samplex) if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) From 39e4e5196c9c660f654264ba11e8e5fd3bdf7fa4 Mon Sep 17 00:00:00 2001 From: joshuasn <53916441+joshuasn@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:50:30 -0400 Subject: [PATCH 06/22] [DISCUSSION] Support measurement twirling in right-dressed boxes (#131) ## Summary This PR supports measurement twirling in right dressed boxes. ## Details and comments --- samplomatic/builders/box_builder.py | 33 ++++++++++++++++--- samplomatic/pre_samplex/pre_samplex.py | 15 +++++++-- .../virtual_registers/pauli_register.py | 23 +++++++++++-- test/integration/test_measurement_twirling.py | 18 ++++++++++ .../test_pauli_register.py | 13 ++++++++ 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index 28983089..f8a89ff0 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -172,12 +172,30 @@ def rhs(self): class RightBoxBuilder(BoxBuilder): """Box builder for right dressings.""" - def parse(self, instr: CircuitInstruction): - if (name := instr.operation.name).startswith("meas"): - raise RuntimeError("Boxes with measurements cannot have dressing=right.") + def __init__(self, collection: CollectionSpec, emission: EmissionSpec): + super().__init__(collection=collection, emission=emission) + + self.measured_qubits = QubitPartition(1, []) + self.clbit_idxs = [] - if name == "barrier": + def parse(self, instr: CircuitInstruction): + if (name := instr.operation.name).startswith("barrier"): spec = InstructionSpec(params=self.template_state.append_remapped_gate(instr)) + return + + if name.startswith("meas"): + for qubit in instr.qubits: + if (qubit,) not in self.measured_qubits: + self.measured_qubits.add((qubit,)) + else: + raise SamplexBuildError( + "Cannot measure the same qubit twice in a twirling box." + ) + self.template_state.append_remapped_gate(instr) + self.clbit_idxs.extend( + [self.template_state.template.find_bit(clbit)[0] for clbit in instr.clbits] + ) + return elif (num_qubits := instr.operation.num_qubits) == 1: self.entangled_qubits.update(instr.qubits) @@ -220,5 +238,12 @@ def lhs(self): def rhs(self): self._append_barrier("M") param_idxs = self._append_dressed_layer() + if twirl_type := self.emission.twirl_register_type: + if len(self.measured_qubits) != 0: + if twirl_type != VirtualType.PAULI: + raise SamplexBuildError( + f"Cannot use {twirl_type.value} twirl in a box with measurements." + ) + self.samplex_state.add_z2_collect(self.measured_qubits, self.clbit_idxs) self.samplex_state.add_collect(self.collection.qubits, self.collection.synth, param_idxs) self._append_barrier("R") diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 63660cf2..99b58df1 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -79,7 +79,8 @@ from ..samplex.noise_model_requirement import NoiseModelRequirement from ..synths import Synth from ..tensor_interface import TensorSpecification -from ..virtual_registers import U2Register +from ..virtual_registers import PauliRegister, U2Register +from ..virtual_registers.pauli_register import PAULI_GATE_NAMES from ..visualization import plot_graph from .graph_data import ( PreBasisTransform, @@ -1351,8 +1352,8 @@ def add_propagate_node( for predecssor_idx in self.graph.predecessor_indices(pre_propagate_idx): incoming.add(samplex.graph[pre_nodes_to_nodes[predecssor_idx]].outgoing_register_type) if mode is InstructionMode.MULTIPLY and pre_propagate.operation.num_qubits == 1: - combined_register_type = VirtualType.U2 if pre_propagate.operation.is_parameterized(): + combined_register_type = VirtualType.U2 param_idxs = [ samplex.append_parameter_expression(param) for _, param in pre_propagate.spec.params @@ -1366,7 +1367,15 @@ def add_propagate_node( op_name, combined_register_name, param_idxs ) else: - register = U2Register(np.array(pre_propagate.operation).reshape(1, 1, 2, 2)) + if ( + incoming == {VirtualType.PAULI} + and (name := pre_propagate.operation.name) in PAULI_GATE_NAMES + ): + combined_register_type = VirtualType.PAULI + register = PauliRegister.from_name(name) + else: + combined_register_type = VirtualType.U2 + register = U2Register(np.array(pre_propagate.operation).reshape(1, 1, 2, 2)) if pre_propagate.direction is Direction.LEFT: propagate_node = RightMultiplicationNode(register, combined_register_name) else: diff --git a/samplomatic/virtual_registers/pauli_register.py b/samplomatic/virtual_registers/pauli_register.py index d1343308..340592a4 100644 --- a/samplomatic/virtual_registers/pauli_register.py +++ b/samplomatic/virtual_registers/pauli_register.py @@ -23,6 +23,8 @@ from .u2_register import U2Register from .z2_register import Z2Register +PAULI_GATE_NAMES = {"id": 0, "x": 2, "z": 1, "y": 3} + PAULI_TO_U2 = np.array( [np.diag([1, 1]), np.diag([1, -1]), np.diag([1, 1])[::-1], np.diag([-1j, 1j])[::-1]], dtype=U2Register.DTYPE, @@ -60,6 +62,23 @@ def __init__(self, virtual_gates): def identity(cls, num_subsystems, num_samples): return cls(np.zeros((num_subsystems, num_samples), dtype=np.uint8)) + @classmethod + def from_name(cls, name: str) -> PauliRegister: + """Returns a Pauli register given a its name. + + Args: + name: The name of the Pauli. + + Returns: The Pauli register. + + Raises: + VirtualGateError: If ``name`` is not in 'PAULI_GATE_NAMES'. + """ + try: + return cls(np.array([PAULI_GATE_NAMES[name]], dtype=np.uint8).reshape(1, 1)) + except KeyError: + raise VirtualGateError(f"'{name}' is not a valid Pauli.") + def convert_to(self, register_type): if register_type is VirtualType.U2: return U2Register(PAULI_TO_U2[self._array, :, :]) @@ -76,7 +95,7 @@ def multiply(self, other, subsystem_idxs: list[SubsystemIndex] | slice = slice(N except (ValueError, IndexError) as exc: raise VirtualGateError( f"Register {self} and {other} have incompatible shapes or types, " - f"given subsystem_idxs {subsystem_idxs}" + f"given subsystem_idxs {subsystem_idxs}." ) from exc def inplace_multiply(self, other, subsystem_idxs: list[SubsystemIndex] | slice = slice(None)): @@ -87,7 +106,7 @@ def inplace_multiply(self, other, subsystem_idxs: list[SubsystemIndex] | slice = except (ValueError, IndexError) as exc: raise VirtualGateError( f"Register {self} and {other} have incompatible shapes or types, " - f"given subsystem_idxs {subsystem_idxs}" + f"given subsystem_idxs {subsystem_idxs}." ) from exc def invert(self): diff --git a/test/integration/test_measurement_twirling.py b/test/integration/test_measurement_twirling.py index ef33d5b7..e60d0969 100644 --- a/test/integration/test_measurement_twirling.py +++ b/test/integration/test_measurement_twirling.py @@ -54,6 +54,14 @@ def test_measure_all(self, save_plot): circuit.measure_all() sample_simulate_and_compare_counts(circuit, save_plot) + def test_measure_all_right(self, save_plot): + circuit = QuantumCircuit(3) + with circuit.box([Twirl()]): + circuit.noop(*circuit.qubits) + with circuit.box([Twirl(dressing="right")]): + circuit.measure_all() + sample_simulate_and_compare_counts(circuit, save_plot) + def test_gates_and_measure_all(self, save_plot): circuit = QuantumCircuit(3) with circuit.box([Twirl(dressing="left")]): @@ -75,6 +83,16 @@ def test_separate_measures(self, save_plot): sample_simulate_and_compare_counts(circuit, save_plot) + def test_measure_with_different_dressings(self, save_plot): + circuit = QuantumCircuit(QuantumRegister(size=2), ClassicalRegister(name="meas", size=2)) + with circuit.box([Twirl(dressing="left")]): + circuit.measure(0, 1) + circuit.x(1) + with circuit.box([Twirl(dressing="right")]): + circuit.measure(1, 0) + + sample_simulate_and_compare_counts(circuit, save_plot) + @pytest.mark.skip(reason="QiskitAer bug #2367") def test_separate_measure_boxes(self, save_plot): """Test separate measurement boxes, with non-standard cbit associations""" diff --git a/test/unit/test_virtual_registers/test_pauli_register.py b/test/unit/test_virtual_registers/test_pauli_register.py index e6c5fdc0..f9a2e849 100644 --- a/test/unit/test_virtual_registers/test_pauli_register.py +++ b/test/unit/test_virtual_registers/test_pauli_register.py @@ -13,8 +13,10 @@ """Test the PauliRegister""" import numpy as np +import pytest from samplomatic.annotations import VirtualType +from samplomatic.exceptions import VirtualGateError from samplomatic.virtual_registers import PauliRegister, U2Register, VirtualRegister, Z2Register @@ -160,3 +162,14 @@ def test_invert(): assert paulis == inverted paulis[0, 0] = 2 assert inverted.virtual_gates[0, 0] != 2 + + +def test_from_name(): + """Test the from_name() method.""" + assert PauliRegister.from_name("x") == PauliRegister(np.array(2, dtype=np.uint8).reshape(1, 1)) + assert PauliRegister.from_name("y") == PauliRegister(np.array(3, dtype=np.uint8).reshape(1, 1)) + assert PauliRegister.from_name("z") == PauliRegister(np.array(1, dtype=np.uint8).reshape(1, 1)) + assert PauliRegister.from_name("id") == PauliRegister(np.zeros((1, 1), dtype=np.uint8)) + + with pytest.raises(VirtualGateError, match="'not-pauli' is not a valid Pauli"): + PauliRegister.from_name("not-pauli") From 4d1df102dd47e0e14698af2f2b8d5f12ba526e31 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Mon, 22 Sep 2025 17:22:23 -0230 Subject: [PATCH 07/22] Uncomment working tests. --- samplomatic/pre_samplex/graph_data.py | 2 +- .../test_builders_pre_samplex.py | 130 +++++++++++ .../test_builders/test_builders_template.py | 212 ++++++++++++++++++ .../test_samplex_builder/__init__.py | 11 - .../test_left_box_builder.py | 160 ------------- .../test_right_box_builder.py | 39 ---- .../test_template_builder/__init__.py | 11 - .../test_template_builder/test_build.py | 210 ----------------- 8 files changed, 343 insertions(+), 432 deletions(-) create mode 100644 test/unit/test_builders/test_builders_pre_samplex.py create mode 100644 test/unit/test_builders/test_builders_template.py delete mode 100644 test/unit/test_builders/test_samplex_builder/__init__.py delete mode 100644 test/unit/test_builders/test_samplex_builder/test_left_box_builder.py delete mode 100644 test/unit/test_builders/test_samplex_builder/test_right_box_builder.py delete mode 100644 test/unit/test_builders/test_template_builder/__init__.py delete mode 100644 test/unit/test_builders/test_template_builder/test_build.py diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index bbaf18ea..33af1a10 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -98,7 +98,7 @@ def __eq__(self, other): and self.subsystems == other.subsystems and self.direction == other.direction and type(self.synth) is type(other.synth) - and self.param_idxs == other.param_idxs + and np.array_equal(self.param_idxs, other.param_idxs) ) def add_subsystems(self, new_subsystems: QubitIndicesPartition, new_param_idxs: ParamIndices): diff --git a/test/unit/test_builders/test_builders_pre_samplex.py b/test/unit/test_builders/test_builders_pre_samplex.py new file mode 100644 index 00000000..42b7c472 --- /dev/null +++ b/test/unit/test_builders/test_builders_pre_samplex.py @@ -0,0 +1,130 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test BoxBuilder for PreSamplex""" + +import pytest +from qiskit.circuit import CircuitInstruction, ClassicalRegister, QuantumCircuit, QuantumRegister +from qiskit.circuit.library import Measure + +from samplomatic.annotations import VirtualType +from samplomatic.builders.box_builder import LeftBoxBuilder +from samplomatic.builders.param_iter import ParamIter +from samplomatic.builders.specs import CollectionSpec, EmissionSpec +from samplomatic.builders.template_state import TemplateState +from samplomatic.constants import Direction +from samplomatic.exceptions import SamplexBuildError +from samplomatic.partition import QubitIndicesPartition, QubitPartition +from samplomatic.pre_samplex import PreSamplex +from samplomatic.pre_samplex.graph_data import PreCollect, PreEmit, PreZ2Collect +from samplomatic.synths.rzsx_synth import RzSxSynth + + +class TestBoxBuilder: + """Test Box Builders""" + + def get_builder(self, qreg, creg=None): + """Helper function to return left box builder with empty PreSamplex.""" + creg = ClassicalRegister(len(qreg)) if creg is None else creg + qubit_map = {q: idx for idx, q in enumerate(qreg)} + template_state = TemplateState(QuantumCircuit(qreg, creg), qubit_map, ParamIter(), [0]) + pre_samplex = PreSamplex(qubit_map=qubit_map, cregs=[creg]) + qubits = QubitPartition.from_elements(qreg) + builder = LeftBoxBuilder( + CollectionSpec(qubits, "Left", RzSxSynth()), + EmissionSpec(qubits, "Right", VirtualType.PAULI), + ) + builder.set_samplex_state(pre_samplex).set_template_state(template_state) + return builder + + def test_parse_measurement(self): + """Test parsing of measurement""" + qreg = QuantumRegister(2) + creg = ClassicalRegister(2) + builder = self.get_builder(qreg, creg) + builder.parse(CircuitInstruction(Measure(), [qreg[0]], [creg[0]])) + + assert builder.samplex_state.graph.num_nodes() == 0 + assert len(builder.measured_qubits) == 1 + assert builder.measured_qubits.overlaps_with([qreg[0]]) + + def test_rhs_with_measurements(self): + """Test rhs of left box with measurements""" + qreg = QuantumRegister(2) + creg = ClassicalRegister(3) + builder = self.get_builder(qreg, creg) + builder.lhs() + builder.parse(CircuitInstruction(Measure(), qreg, [creg[0], creg[2]])) + builder.rhs() + idxs = QubitIndicesPartition.from_elements(builder.samplex_state.qubit_map.values()) + + assert builder.samplex_state.graph.num_nodes() == 3 + assert builder.samplex_state.graph.nodes()[0] == PreCollect( + idxs, Direction.BOTH, RzSxSynth(), [[0, 1, 2], [3, 4, 5]] + ) + assert builder.samplex_state.graph.nodes()[1] == PreEmit( + idxs, Direction.BOTH, VirtualType.PAULI + ) + assert builder.samplex_state.graph.nodes()[2] == PreZ2Collect( + idxs, clbit_idxs={creg.name: [0, 2]}, subsystems_idxs={creg.name: [0, 1]} + ) + + def test_rhs_no_measurements(self): + """Test rhs of left box with no measurements""" + qreg = QuantumRegister(2) + builder = self.get_builder(qreg) + builder.lhs() + builder.rhs() + idxs = QubitIndicesPartition.from_elements(builder.samplex_state.qubit_map.values()) + assert builder.samplex_state.graph.num_nodes() == 2 + assert builder.samplex_state.graph.nodes()[0] == PreCollect( + idxs, Direction.BOTH, RzSxSynth(), [[0, 1, 2], [3, 4, 5]] + ) + assert builder.samplex_state.graph.nodes()[1] == PreEmit( + idxs, Direction.BOTH, VirtualType.PAULI + ) + + def test_wrong_twirl_type_for_measurement(self): + """Test that error is raised if a measurement exists, but the twirl type is wrong""" + qreg = QuantumRegister(2) + creg = ClassicalRegister(2) + qubit_map = {q: idx for idx, q in enumerate(qreg)} + pre_samplex = PreSamplex(qubit_map=qubit_map) + qubits = QubitPartition.from_elements(qreg) + builder = LeftBoxBuilder( + CollectionSpec(qubits, "Left", RzSxSynth()), + EmissionSpec(qubits, "Right", VirtualType.U2), + ) + builder.set_samplex_state(pre_samplex) + builder.set_template_state( + TemplateState(QuantumCircuit(qreg, creg), qubit_map, ParamIter(), [0]) + ) + builder.lhs() + builder.parse(CircuitInstruction(Measure(), qreg, creg)) + + with pytest.raises( + SamplexBuildError, match="Cannot use u2 twirl in a box with measurements" + ): + builder.rhs() + + def test_two_measurements_on_the_same_qubit_error(self): + """Test that error is raised if the same qubit is measured twice in the box""" + qreg = QuantumRegister(2) + creg = ClassicalRegister(2) + builder = self.get_builder(qreg) + builder.set_template_state(TemplateState.construct_for_circuit(QuantumCircuit(qreg, creg))) + builder.parse(CircuitInstruction(Measure(), qreg, creg)) + + with pytest.raises( + SamplexBuildError, match="Cannot measure the same qubit twice in a twirling box" + ): + builder.parse(CircuitInstruction(Measure(), qreg, creg)) diff --git a/test/unit/test_builders/test_builders_template.py b/test/unit/test_builders/test_builders_template.py new file mode 100644 index 00000000..d41fc130 --- /dev/null +++ b/test/unit/test_builders/test_builders_template.py @@ -0,0 +1,212 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test BoxBuilder for Templates""" + +from qiskit.circuit import Parameter, QuantumCircuit + +from samplomatic.annotations import Twirl +from samplomatic.builders import pre_build + + +class TestTemplateBuilder: + """Test strictly the template aspects of build.""" + + def test_empty(self): + """Test building an empty circuit.""" + template_state, _ = pre_build(QuantumCircuit()) + template = template_state.template + + assert template.num_qubits == 0 + assert template.num_clbits == 0 + assert template.num_parameters == 0 + assert not template.data + + def test_no_box(self): + """Test building a circuit with no boxes.""" + + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.rz(Parameter("a") + Parameter("b"), 0) + circuit.barrier(0, 1) + circuit.cx(0, 1) + circuit.measure_all() + + template_state, _ = pre_build(circuit) + template = template_state.template + + assert template.num_qubits == 2 + assert template.num_clbits == 2 + assert [p.name for p in template.parameters] == ["p00"] + + for instr0, instr1 in zip(circuit, template): + assert instr0.operation.name == instr1.operation.name + + for q0, q1 in zip(instr0.qubits, instr1.qubits): + assert circuit.find_bit(q0).index == template.find_bit(q1).index + + for q0, q1 in zip(instr0.clbits, instr1.clbits): + assert circuit.find_bit(q0).index == template.find_bit(q1).index + + def test_box_no_annotations(self): + """Test building a circuit with a box that has no annotations.""" + circuit = QuantumCircuit(2) + + with circuit.box(): + circuit.h(0) + + circuit.rz(Parameter("a") + Parameter("b"), 0) + + with circuit.box(): + circuit.cx(0, 1) + + circuit.measure_all() + + template_state, _ = pre_build(circuit) + template = template_state.template + + assert template.num_qubits == 2 + assert template.num_clbits == 2 + assert [p.name for p in template.parameters] == ["p00"] + + expected_circuit = QuantumCircuit(2) + expected_circuit.barrier(0) + expected_circuit.h(0) + expected_circuit.barrier(0) + expected_circuit.rz(Parameter("a") + Parameter("b"), 0) + expected_circuit.barrier(0, 1) + expected_circuit.cx(0, 1) + expected_circuit.barrier(0, 1) + expected_circuit.measure_all() + + for instr0, instr1 in zip(expected_circuit, template): + assert instr0.operation.name == instr1.operation.name + + for q0, q1 in zip(instr0.qubits, instr1.qubits): + assert circuit.find_bit(q0).index == template.find_bit(q1).index + + for q0, q1 in zip(instr0.clbits, instr1.clbits): + assert circuit.find_bit(q0).index == template.find_bit(q1).index + + def test_box_left_right(self): + """Test putting the dressing on different sides of a box.""" + circuit = QuantumCircuit(2) + with circuit.box([Twirl(dressing="left")]): + circuit.cx(0, 1) + with circuit.box([Twirl(dressing="right")]): + circuit.cx(0, 1) + circuit.measure_all() + + template_state, _ = pre_build(circuit) + template = template_state.template + + assert template.num_qubits == 2 + assert template.num_clbits == 2 + assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(12)] + + expected_names = ["barrier"] + expected_names += ["rz", "sx", "rz", "sx", "rz"] + expected_names += ["rz", "sx", "rz", "sx", "rz"] + expected_names += ["barrier", "cx", "barrier"] + expected_names += ["barrier", "cx", "barrier"] + expected_names += ["rz", "sx", "rz", "sx", "rz"] + expected_names += ["rz", "sx", "rz", "sx", "rz"] + expected_names += ["barrier"] + + for idx, (instr, name) in enumerate(zip(template, expected_names)): + assert instr.name == name, f"Instruction {idx}" + + def test_box_decomposition(self): + """Test decomposition modes of a box.""" + + circuit = QuantumCircuit(2) + circuit.h(0) + with circuit.box([Twirl(decomposition="rzrx")]): + circuit.cx(0, 1) + + circuit.rz(Parameter("c"), 0) + + with circuit.box([Twirl(decomposition="rzsx", dressing="right")]): + circuit.cx(0, 1) + + circuit.measure_all() + + template_state, _ = pre_build(circuit) + template = template_state.template + + assert template.num_qubits == 2 + assert template.num_clbits == 2 + assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(13)] + + expected_names = ["h"] + + expected_names += ["barrier"] + expected_names += ["rz", "rx", "rz"] * 2 + ["barrier", "cx"] + expected_names += ["barrier"] + + expected_names += ["rz"] + + expected_names += ["barrier"] + expected_names += ["cx", "barrier"] + ["rz", "sx", "rz", "sx", "rz"] * 2 + expected_names += ["barrier"] + + expected_names += ["barrier"] + ["measure"] * 4 + + for idx, (instr, name) in enumerate(zip(template, expected_names)): + assert instr.operation.name == name, f"Instruction {idx}" + + def test_general_5q_static_circuit(self): + """Test with a general static circuit of 5 qubits""" + + circuit = QuantumCircuit(5) + with circuit.box([Twirl()]): + circuit.rz(0.5, 0) + circuit.sx(0) + circuit.rz(0.5, 0) + circuit.cx(0, 3) + circuit.noop(range(5)) + + circuit.cx(0, 1) + + with circuit.box([Twirl(decomposition="rzrx")]): + circuit.rz(0.123, 2) + circuit.cx(3, 4) + circuit.cx(3, 2) + circuit.noop(1) + + with circuit.box([Twirl()]): + circuit.cx(0, 1) + + with circuit.box([Twirl(dressing="right")]): + circuit.noop(range(5)) + + circuit.measure_all() + + template_state, _ = pre_build(circuit) + template = template_state.template + + assert template.num_qubits == 5 + assert template.num_clbits == 5 + + # Verify that we get the expected number of operations + ops_count = template.count_ops() + # 3 boxes of rzsx for a total of 12 qubits, 1 box of rzrx for a total of 4 qubits. + assert ops_count["rz"] == 44 # 12 * 3 + 4 * 2 + assert ops_count["rx"] == 4 # 4 * 1 + assert ops_count["sx"] == 24 # 12 * 2 + assert ops_count["barrier"] == 13 # 3 * 4 boxes + 1 for measure all + assert ops_count["cx"] == 5 # as in the original circuit + assert ops_count["measure"] == 5 # as in the original circuit + assert len(ops_count) == 6 + + # Verify that we get the expected number of parameters + assert len(template.parameters) == 48 # One parameter per rz\rx gate diff --git a/test/unit/test_builders/test_samplex_builder/__init__.py b/test/unit/test_builders/test_samplex_builder/__init__.py deleted file mode 100644 index 577f6c83..00000000 --- a/test/unit/test_builders/test_samplex_builder/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py b/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py deleted file mode 100644 index 13f76404..00000000 --- a/test/unit/test_builders/test_samplex_builder/test_left_box_builder.py +++ /dev/null @@ -1,160 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test PreSamplex""" - -# import numpy as np -# import pytest -# from qiskit.circuit import CircuitInstruction, ClassicalRegister, QuantumCircuit, QuantumRegister -# from qiskit.circuit.library import Measure, XGate - -# from samplomatic import Twirl -# from samplomatic.annotations import VirtualType -# from samplomatic.builders import pre_build -# from samplomatic.builders.samplex_builder.box_samplex_builder import LeftBoxSamplexBuilder -# from samplomatic.builders.specs import CollectionSpec, EmissionSpec, InstructionSpec -# from samplomatic.constants import Direction -# from samplomatic.exceptions import SamplexBuildError -# from samplomatic.partition import QubitIndicesPartition, QubitPartition -# from samplomatic.pre_samplex import PreSamplex -# from samplomatic.pre_samplex.graph_data import PreCollect, PreEmit, PreZ2Collect -# from samplomatic.synths.rzsx_synth import RzSxSynth - - -# class TestLeftBoxBuilder: -# """Test Box Builders""" - -# def get_builder(self, qreg, creg=None): -# """Helper function to return left box builder with empty PreSamplex.""" -# cregs = [ClassicalRegister(len(qreg)) if creg is None else creg] -# pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(qreg)}, cregs=cregs) -# qubits = QubitPartition.from_elements(qreg) -# builder = LeftBoxSamplexBuilder( -# CollectionSpec(qubits, "Left", RzSxSynth()), -# EmissionSpec(qubits, "Right", VirtualType.PAULI), -# ) -# builder.set_state(pre_samplex) -# return builder - -# def test_parse_measurement(self): -# """Test parsing of measurement""" -# qreg = QuantumRegister(2) -# builder = self.get_builder(qreg) -# builder.parse(CircuitInstruction(Measure(), [qreg[0]]), InstructionSpec()) - -# assert builder.state.graph.num_nodes() == 1 -# assert len(builder.measured_qubits) == 1 -# assert builder.measured_qubits.overlaps_with([qreg[0]]) - -# def test_rhs_with_measurements(self): -# """Test rhs of left box with measurements""" -# qreg = QuantumRegister(2) -# creg = ClassicalRegister(3) -# builder = self.get_builder(qreg, creg) -# builder.lhs(InstructionSpec()) -# builder.parse( -# CircuitInstruction(Measure(), qreg), -# InstructionSpec(clbit_idxs=[0, 2], param_idxs=np.array([0])), -# ) -# builder.rhs(InstructionSpec()) -# subsystem_idxs = QubitIndicesPartition.from_elements( -# [builder.state.qubit_map[q] for q in qreg] -# ) - -# assert builder.state.graph.num_nodes() == 3 -# assert builder.state.graph.nodes()[0] == PreCollect( -# subsystem_idxs, Direction.BOTH, RzSxSynth(), [0] -# ) -# assert builder.state.graph.nodes()[1] == PreEmit( -# subsystem_idxs, Direction.BOTH, VirtualType.PAULI -# ) -# assert builder.state.graph.nodes()[2] == PreZ2Collect( -# subsystem_idxs, clbit_idxs={creg.name: [0, 2]}, subsystems_idxs={creg.name: [0, 1]} -# ) - -# def test_rhs_no_measurements(self): -# """Test rhs of left box with no measurements""" -# qreg = QuantumRegister(2) -# builder = self.get_builder(qreg) -# builder.lhs(InstructionSpec()) -# builder.rhs(InstructionSpec(param_idxs=np.array([0]))) -# subsystem_idxs = QubitIndicesPartition.from_elements( -# [builder.state.qubit_map[q] for q in qreg] -# ) -# assert builder.state.graph.num_nodes() == 2 -# assert builder.state.graph.nodes()[0] == PreCollect( -# subsystem_idxs, Direction.BOTH, RzSxSynth(), [0] -# ) -# assert builder.state.graph.nodes()[1] == PreEmit( -# subsystem_idxs, Direction.BOTH, VirtualType.PAULI -# ) - -# def test_gate_after_measurement_error(self): -# """Test that error is raised if a gate is encountered after a measurement""" -# qreg = QuantumRegister(2) -# builder = self.get_builder(qreg) -# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - -# with pytest.raises(SamplexBuildError, match="No operations allowed after a measurement"): -# builder.parse(CircuitInstruction(XGate(), [qreg[0]]), InstructionSpec()) - -# def test_wrong_twirl_type_for_measurement(self): -# """Test that error is raised if a measurement exists, but the twirl type is wrong""" -# qreg = QuantumRegister(2) -# pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(qreg)}) -# qubits = QubitPartition.from_elements(qreg) -# builder = LeftBoxSamplexBuilder( -# CollectionSpec(qubits, "Left", RzSxSynth()), -# EmissionSpec(qubits, "Right", VirtualType.U2), -# ) -# builder.set_state(pre_samplex) -# builder.lhs(InstructionSpec()) -# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - -# with pytest.raises( -# SamplexBuildError, match="Cannot use u2 twirl in a box with measurements" -# ): -# builder.rhs(InstructionSpec()) - -# def test_two_measurements_on_the_same_qubit_error(self): -# """Test that error is raised if the same qubit is measured twice in the box""" -# qreg = QuantumRegister(2) -# builder = self.get_builder(qreg) -# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - -# with pytest.raises( -# SamplexBuildError, match="Cannot measure the same qubit twice in a twirling box" -# ): -# builder.parse(CircuitInstruction(Measure(), qreg), InstructionSpec()) - -# def test_if_else(self): -# """Test the build result of if-else. - -# Because it is a bit difficult to do by hand, we use the full pre_build. -# """ -# circuit = QuantumCircuit(1, 1) -# circuit.measure(0, 0) -# with circuit.box([Twirl(dressing="left")]): -# with circuit.if_test((circuit.clbits[0], 1)) as _else: -# circuit.x(0) -# with _else: -# circuit.sx(0) -# with circuit.box([Twirl(dressing="right")]): -# circuit.noop(0) - -# _, pre_samplex = pre_build(circuit) -# graph = pre_samplex.graph -# for emit_node in [4, 5]: -# assert not graph.get_edge_data(emit_node, 1).force_register_copy -# assert graph.get_edge_data(emit_node, 3).force_register_copy -# assert graph[1].operation.name == "x" -# assert graph[3].operation.name == "sx" diff --git a/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py b/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py deleted file mode 100644 index 931094d2..00000000 --- a/test/unit/test_builders/test_samplex_builder/test_right_box_builder.py +++ /dev/null @@ -1,39 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test RightBoxSamplexBuilder""" - - -class TestRightBoxBuilder: - """Test Box Builders""" - - def test_if_else(self): - """Test the build result of if-else. - - Because it is a bit difficult to do by hand, we use the full pre_build.""" - # circuit = QuantumCircuit(1, 1) - # circuit.measure(0, 0) - # with circuit.box([Twirl(dressing="left")]): - # circuit.noop(0) - # with circuit.box([Twirl(dressing="right")]): - # with circuit.if_test((circuit.clbits[0], 1)) as _else: - # circuit.x(0) - # with _else: - # circuit.sx(0) - - # _, pre_samplex = pre_build(circuit) - # graph = pre_samplex.graph - # for emit_node in [1, 2]: - # assert not graph.get_edge_data(emit_node, 3).force_register_copy - # assert graph.get_edge_data(emit_node, 6).force_register_copy - # assert graph[3].operation.name == "x" - # assert graph[6].operation.name == "sx" diff --git a/test/unit/test_builders/test_template_builder/__init__.py b/test/unit/test_builders/test_template_builder/__init__.py deleted file mode 100644 index 577f6c83..00000000 --- a/test/unit/test_builders/test_template_builder/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. diff --git a/test/unit/test_builders/test_template_builder/test_build.py b/test/unit/test_builders/test_template_builder/test_build.py deleted file mode 100644 index eb33fd68..00000000 --- a/test/unit/test_builders/test_template_builder/test_build.py +++ /dev/null @@ -1,210 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -# from qiskit.circuit import Parameter, QuantumCircuit - -# from samplomatic.annotations import Twirl -# from samplomatic.builders import pre_build - - -# class TestTemplateBuilder: -# """Test strictly the template aspects of build.""" - -# def test_empty(self): -# """Test building an empty circuit.""" -# template_state, _ = pre_build(QuantumCircuit()) -# template = template_state.template - -# assert template.num_qubits == 0 -# assert template.num_clbits == 0 -# assert template.num_parameters == 0 -# assert not template.data - -# def test_no_box(self): -# """Test building a circuit with no boxes.""" - -# circuit = QuantumCircuit(2) -# circuit.h(0) -# circuit.rz(Parameter("a") + Parameter("b"), 0) -# circuit.barrier(0, 1) -# circuit.cx(0, 1) -# circuit.measure_all() - -# template_state, _ = pre_build(circuit) -# template = template_state.template - -# assert template.num_qubits == 2 -# assert template.num_clbits == 2 -# assert [p.name for p in template.parameters] == ["p00"] - -# for instr0, instr1 in zip(circuit, template): -# assert instr0.operation.name == instr1.operation.name - -# for q0, q1 in zip(instr0.qubits, instr1.qubits): -# assert circuit.find_bit(q0).index == template.find_bit(q1).index - -# for q0, q1 in zip(instr0.clbits, instr1.clbits): -# assert circuit.find_bit(q0).index == template.find_bit(q1).index - -# def test_box_no_annotations(self): -# """Test building a circuit with a box that has no annotations.""" -# circuit = QuantumCircuit(2) - -# with circuit.box(): -# circuit.h(0) - -# circuit.rz(Parameter("a") + Parameter("b"), 0) - -# with circuit.box(): -# circuit.cx(0, 1) - -# circuit.measure_all() - -# template_state, _ = pre_build(circuit) -# template = template_state.template - -# assert template.num_qubits == 2 -# assert template.num_clbits == 2 -# assert [p.name for p in template.parameters] == ["p00"] - -# expected_circuit = QuantumCircuit(2) -# expected_circuit.barrier(0) -# expected_circuit.h(0) -# expected_circuit.barrier(0) -# expected_circuit.rz(Parameter("a") + Parameter("b"), 0) -# expected_circuit.barrier(0, 1) -# expected_circuit.cx(0, 1) -# expected_circuit.barrier(0, 1) -# expected_circuit.measure_all() - -# for instr0, instr1 in zip(expected_circuit, template): -# assert instr0.operation.name == instr1.operation.name - -# for q0, q1 in zip(instr0.qubits, instr1.qubits): -# assert circuit.find_bit(q0).index == template.find_bit(q1).index - -# for q0, q1 in zip(instr0.clbits, instr1.clbits): -# assert circuit.find_bit(q0).index == template.find_bit(q1).index - -# def test_box_left_right(self): -# """Test putting the dressing on different sides of a box.""" -# circuit = QuantumCircuit(2) -# with circuit.box([Twirl(dressing="left")]): -# circuit.cx(0, 1) -# with circuit.box([Twirl(dressing="right")]): -# circuit.cx(0, 1) -# circuit.measure_all() - -# template_state, _ = pre_build(circuit) -# template = template_state.template - -# assert template.num_qubits == 2 -# assert template.num_clbits == 2 -# assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(12)] - -# expected_names = ["barrier"] -# expected_names += ["rz", "sx", "rz", "sx", "rz"] -# expected_names += ["rz", "sx", "rz", "sx", "rz"] -# expected_names += ["barrier", "cx", "barrier"] -# expected_names += ["barrier", "cx", "barrier"] -# expected_names += ["rz", "sx", "rz", "sx", "rz"] -# expected_names += ["rz", "sx", "rz", "sx", "rz"] -# expected_names += ["barrier"] - -# for idx, (instr, name) in enumerate(zip(template, expected_names)): -# assert instr.name == name, f"Instruction {idx}" - -# def test_box_decomposition(self): -# """Test decomposition modes of a box.""" - -# circuit = QuantumCircuit(2) -# circuit.h(0) -# with circuit.box([Twirl(decomposition="rzrx")]): -# circuit.cx(0, 1) - -# circuit.rz(Parameter("c"), 0) - -# with circuit.box([Twirl(decomposition="rzsx", dressing="right")]): -# circuit.cx(0, 1) - -# circuit.measure_all() - -# template_state, _ = pre_build(circuit) -# template = template_state.template - -# assert template.num_qubits == 2 -# assert template.num_clbits == 2 -# assert [p.name for p in template.parameters] == [f"p{str(i).zfill(2)}" for i in range(13)] - -# expected_names = ["h"] - -# expected_names += ["barrier"] -# expected_names += ["rz", "rx", "rz"] * 2 + ["barrier", "cx"] -# expected_names += ["barrier"] - -# expected_names += ["rz"] - -# expected_names += ["barrier"] -# expected_names += ["cx", "barrier"] + ["rz", "sx", "rz", "sx", "rz"] * 2 -# expected_names += ["barrier"] - -# expected_names += ["barrier"] + ["measure"] * 4 - -# for idx, (instr, name) in enumerate(zip(template, expected_names)): -# assert instr.operation.name == name, f"Instruction {idx}" - -# def test_general_5q_static_circuit(self): -# """Test with a general static circuit of 5 qubits""" - -# circuit = QuantumCircuit(5) -# with circuit.box([Twirl()]): -# circuit.rz(0.5, 0) -# circuit.sx(0) -# circuit.rz(0.5, 0) -# circuit.cx(0, 3) -# circuit.noop(range(5)) - -# circuit.cx(0, 1) - -# with circuit.box([Twirl(decomposition="rzrx")]): -# circuit.rz(0.123, 2) -# circuit.cx(3, 4) -# circuit.cx(3, 2) -# circuit.noop(1) - -# with circuit.box([Twirl()]): -# circuit.cx(0, 1) - -# with circuit.box([Twirl(dressing="right")]): -# circuit.noop(range(5)) - -# circuit.measure_all() - -# template_state, _ = pre_build(circuit) -# template = template_state.template - -# assert template.num_qubits == 5 -# assert template.num_clbits == 5 - -# # Verify that we get the expected number of operations -# ops_count = template.count_ops() -# # 3 boxes of rzsx for a total of 12 qubits, 1 box of rzrx for a total of 4 qubits. -# assert ops_count["rz"] == 44 # 12 * 3 + 4 * 2 -# assert ops_count["rx"] == 4 # 4 * 1 -# assert ops_count["sx"] == 24 # 12 * 2 -# assert ops_count["barrier"] == 13 # 3 * 4 boxes + 1 for measure all -# assert ops_count["cx"] == 5 # as in the original circuit -# assert ops_count["measure"] == 5 # as in the original circuit -# assert len(ops_count) == 6 - -# # Verify that we get the expected number of parameters -# assert len(template.parameters) == 48 # One parameter per rz\rx gate From b69636e941b1af47a7b9f13f280c399ba7be0278 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Wed, 24 Sep 2025 16:08:08 -0230 Subject: [PATCH 08/22] A little closer. --- docs/apidocs/samplomatic.samplex.nodes.rst | 16 +++ samplomatic/builders/box_builder.py | 16 +-- samplomatic/builders/dynamic_builder.py | 121 +++++++++++-------- samplomatic/pre_samplex/graph_data.py | 5 - samplomatic/pre_samplex/pre_samplex.py | 33 ++--- samplomatic/samplex/nodes/__init__.py | 2 + samplomatic/samplex/nodes/copy_node.py | 57 +++++++++ samplomatic/samplex/nodes/scope_node.py | 101 ++++++++++++++++ samplomatic/samplex/samplex_serialization.py | 2 + 9 files changed, 264 insertions(+), 89 deletions(-) create mode 100644 samplomatic/samplex/nodes/copy_node.py create mode 100644 samplomatic/samplex/nodes/scope_node.py diff --git a/docs/apidocs/samplomatic.samplex.nodes.rst b/docs/apidocs/samplomatic.samplex.nodes.rst index 703888d0..0daad2ac 100644 --- a/docs/apidocs/samplomatic.samplex.nodes.rst +++ b/docs/apidocs/samplomatic.samplex.nodes.rst @@ -57,6 +57,14 @@ samplomatic.samplex.nodes.conversion\_node module :show-inheritance: :undoc-members: +samplomatic.samplex.nodes.copy\_node module +------------------------------------------- + +.. automodule:: samplomatic.samplex.nodes.copy_node + :members: + :show-inheritance: + :undoc-members: + samplomatic.samplex.nodes.evaluation\_node module ------------------------------------------------- @@ -105,6 +113,14 @@ samplomatic.samplex.nodes.sampling\_node module :show-inheritance: :undoc-members: +samplomatic.samplex.nodes.scope\_node module +-------------------------------------------- + +.. automodule:: samplomatic.samplex.nodes.scope_node + :members: + :show-inheritance: + :undoc-members: + samplomatic.samplex.nodes.slice\_register\_node module ------------------------------------------------------ diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index eb304d70..1aac1e0f 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -109,15 +109,11 @@ def parse(self, instr: CircuitInstruction): if name.startswith("if_else"): builder = BoxLeftIfElseBuilder( - instr, - self.collection.synth, - self.template_state.param_iter, - self.template_state.qubit_map, + instr, self.samplex_state, self.collection.synth, self.template_state.param_iter ) - if_else, graph = builder.build() + if_else = builder.build() new_qubits = [self.template_state.qubit_map.get(qubit, qubit) for qubit in instr.qubits] self.template_state.template.append(if_else, new_qubits, instr.clbits) - self.samplex_state.add_if_else_subgraph(graph) return if (num_qubits := instr.operation.num_qubits) == 1: @@ -196,15 +192,11 @@ def parse(self, instr: CircuitInstruction): if name.startswith("if_else"): builder = BoxRightIfElseBuilder( - instr, - self.collection.synth, - self.template_state.param_iter, - self.template_state.qubit_map, + instr, self.samplex_state, self.collection.synth, self.template_state.param_iter ) - if_else, graph = builder.build() + if_else = builder.build() new_qubits = [self.template_state.qubit_map.get(qubit, qubit) for qubit in instr.qubits] self.template_state.template.append(if_else, new_qubits, instr.clbits) - self.samplex_state.add_if_else_subgraph(graph) return if (num_qubits := instr.operation.num_qubits) == 1: diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index abfb50dc..1578b1e7 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -12,32 +12,39 @@ """BoxIfElseBuilder""" +from copy import deepcopy + import numpy as np from qiskit.circuit import IfElseOp, QuantumCircuit from ..aliases import ParamIndices, Qubit, QubitIndex -from ..partition import QubitIndicesPartition, QubitPartition -from ..pre_samplex import PreSamplex -from ..pre_samplex.graph_data import Direction, PreCombine, PreEdge +from ..partition import QubitPartition +from ..pre_samplex import DanglerMatch, PreSamplex +from ..pre_samplex.graph_data import Direction, PreCollect, PreCopy, PreEdge, PreEmit, PrePropagate from ..synths import Synth from .param_iter import ParamIter from .specs import InstructionMode, InstructionSpec class BoxIfElseBuilder: - def __init__(self, op, synth: Synth, param_iter: ParamIter, qubit_map: dict[Qubit, QubitIndex]): + def __init__( + self, + op, + pre_samplex: PreSamplex, + synth: Synth, + param_iter: ParamIter, + ): self.op = op + self.pre_samplex = pre_samplex self.synth = synth self.param_iter = param_iter - self.qubit_map = qubit_map def block_qubit_map(self, block) -> dict[Qubit, QubitIndex]: block_map = {i_q: b_q for i_q, b_q in zip(self.op.qubits, block.qubits)} - return {block_map[i_q]: self.qubit_map[i_q] for i_q in self.qubit_map if i_q in block_map} + qubit_map = self.pre_samplex.qubit_map + return {block_map[i_q]: qubit_map[i_q] for i_q in qubit_map if i_q in block_map} - def append_propagate( - self, block: QuantumCircuit, new_block: QuantumCircuit, pre_samplex: PreSamplex - ): + def append_propagate(self, block: QuantumCircuit, new_block: QuantumCircuit): for instr in block: new_params = [] param_mapping = [] @@ -47,7 +54,7 @@ def append_propagate( new_op = type(instr.operation)(*new_params) if new_params else instr.operation new_block.append(new_op, instr.qubits, instr.clbits) - pre_samplex.add_propagate( + self.pre_samplex.add_propagate( instr, InstructionSpec(params=new_params, mode=InstructionMode.PROPAGATE) ) @@ -62,7 +69,7 @@ def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> P class BoxRightIfElseBuilder(BoxIfElseBuilder): - def build_block(self, block, pre_samplex: PreSamplex): + def build_block(self, block): block = ( block if block is not None @@ -72,37 +79,47 @@ def build_block(self, block, pre_samplex: PreSamplex): ) new_block = QuantumCircuit(block.qubits, block.clbits) - pre_samplex = pre_samplex.remap(qubit_map=self.block_qubit_map(block)) - copy_idx = pre_samplex.add_copy( - qubits := QubitPartition.from_elements(new_block.qubits), Direction.RIGHT - ) + pre_samplex = self.pre_samplex.remap(qubit_map=self.block_qubit_map(block)) - base_node = pre_samplex.graph.nodes()[0] - pre_edge = PreEdge(base_node.subsystems, base_node.direction) - pre_samplex.graph.add_edge(0, copy_idx, pre_edge) + qubits = QubitPartition.from_elements(new_block.qubits) + subsystems = pre_samplex.qubits_to_indices(qubits) + dangler_match = DanglerMatch(node_types=(PreEmit, PrePropagate)) - self.append_propagate(block, new_block, pre_samplex) + new_danglers = [] + for node_idx, partition in pre_samplex.find_then_remove_danglers(dangler_match, subsystems): + copy_idx = pre_samplex.graph.add_node(PreCopy(partition, Direction.RIGHT)) + edge = PreEdge(partition, Direction.RIGHT) + pre_samplex.graph.add_edge(node_idx, copy_idx, edge) + new_danglers.append((copy_idx, partition)) + + for node_idx, partition in new_danglers: + pre_samplex.add_dangler(partition.all_elements, node_idx) + + self.append_propagate(block, new_block) params = self.append_template(block, new_block) - pre_samplex.add_collect(qubits, self.synth, params) + collect_idx = pre_samplex.add_collect(qubits, self.synth, params) - return new_block + return new_block, (collect_idx, subsystems) def build(self): - qubits = QubitIndicesPartition.from_elements( - v for k, v in self.qubit_map.items() if k in self.op.qubits - ) - pre_samplex = PreSamplex() - pre_samplex.graph.add_node(PreCombine(qubits, Direction.RIGHT)) - if_block = self.build_block(self.op.params[0], pre_samplex) - else_block = self.build_block(self.op.params[1], pre_samplex) + original_danglers = deepcopy(self.pre_samplex.get_all_danglers()) + if_block, if_dangler = self.build_block(self.op.params[0]) + self.pre_samplex.set_all_danglers(*original_danglers) + else_block, else_dangler = self.build_block(self.op.params[1]) + + for node_idx, subsystems in [if_dangler, else_dangler]: + self.pre_samplex.add_dangler( + subsystems.all_elements, + node_idx, + ) if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - return if_else_op, pre_samplex.graph + return if_else_op class BoxLeftIfElseBuilder(BoxIfElseBuilder): - def build_block(self, block, pre_samplex: PreSamplex): + def build_block(self, block): block = ( block if block is not None @@ -113,29 +130,37 @@ def build_block(self, block, pre_samplex: PreSamplex): new_block = QuantumCircuit(block.qubits, block.clbits) params = self.append_template(block, new_block) - pre_samplex = pre_samplex.remap(qubit_map=self.block_qubit_map(block)) - pre_samplex.add_collect( - qubits := QubitPartition.from_elements(new_block.qubits), self.synth, params - ) - self.append_propagate(block, new_block, pre_samplex) - copy_idx = pre_samplex.add_copy(qubits, Direction.LEFT) + pre_samplex = self.pre_samplex.remap(qubit_map=self.block_qubit_map(block)) + qubits = QubitPartition.from_elements(new_block.qubits) + subsystems = pre_samplex.qubits_to_indices(qubits) + dangler_match = DanglerMatch(node_types=(PreCollect, PrePropagate)) - base_node = pre_samplex.graph.nodes()[0] - pre_edge = PreEdge(base_node.subsystems, base_node.direction) - pre_samplex.graph.add_edge(0, copy_idx, pre_edge) + list(pre_samplex.find_then_remove_danglers(dangler_match, subsystems)) + pre_samplex.add_collect(qubits, self.synth, params) + self.append_propagate(block, new_block) - return new_block + copy_idxs = [] + for node_idx, partition in pre_samplex.find_then_remove_danglers(dangler_match, subsystems): + copy_idx = pre_samplex.graph.add_node(PreCopy(partition, Direction.LEFT)) + copy_idxs.append((copy_idx, partition)) + edge = PreEdge(partition, Direction.LEFT) + pre_samplex.graph.add_edge(copy_idx, node_idx, edge) + + return new_block, copy_idxs def build(self): - qubits = QubitIndicesPartition.from_elements( - v for k, v in self.qubit_map.items() if k in self.op.qubits - ) - pre_samplex = PreSamplex() - pre_samplex.graph.add_node(PreCombine(qubits, Direction.LEFT)) - if_block = self.build_block(self.op.params[0], pre_samplex) - else_block = self.build_block(self.op.params[1], pre_samplex) + original_danglers = deepcopy(self.pre_samplex.get_all_danglers()) + if_block, if_danglers = self.build_block(self.op.params[0]) + self.pre_samplex.set_all_danglers(*original_danglers) + else_block, else_danglers = self.build_block(self.op.params[1]) + + for node_idx, subsystems in [*if_danglers, *else_danglers]: + self.pre_samplex.add_dangler( + subsystems.all_elements, + node_idx, + ) if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - return if_else_op, pre_samplex.graph + return if_else_op diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index 6fb4415f..9bc66027 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -42,11 +42,6 @@ def get_style(self) -> NodeStyle: return NodeStyle(title=type(self).__name__).append_data("Subsystems", list(self.subsystems)) -@dataclass -class PreCombine(PreNode): - """""" - - @dataclass class PreCopy(PreNode): """""" diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 3e675dfe..d361543e 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -84,7 +84,6 @@ from .graph_data import ( PreBasisTransform, PreCollect, - PreCombine, PreCopy, PreEdge, PreEmit, @@ -137,10 +136,7 @@ class DanglerMatch: def match_node(self, node: PreNode) -> bool: """Check if a node matches the conditions specified in the object""" return (self.direction is None or node.direction in (self.direction, Direction.BOTH)) and ( - self.node_types is None - or isinstance(node, self.node_types) - or type(node) is PreCombine - or type(node) is PreCopy + self.node_types is None or isinstance(node, self.node_types) or type(node) is PreCopy ) @@ -605,25 +601,6 @@ def _add_right(self, node: PreNode): return node_idx - def add_copy(self, qubits: QubitPartition, direction: Direction) -> NodeIndex: - node = PreCopy(self.qubits_to_indices(qubits), direction) - if direction is Direction.LEFT: - return self._add_left(node) - return self._add_right(node) - - def add_if_else_subgraph(self, graph: PyDiGraph): - node = cast(PreNode, graph.nodes()[0]) - graph_start_idx = self.graph.add_node(PreNode(node.subsystems, node.direction)) - - for node_idx, subsystems in self.find_then_remove_danglers(DanglerMatch(), node.subsystems): - if node.direction == Direction.RIGHT: - edge = PreEdge(subsystems, node.direction) - self.graph.add_edge(node_idx, graph_start_idx, edge) - ret = self.graph.substitute_node_with_subgraph( - graph_start_idx, graph, lambda source, target, weight: 0 - ) - self.add_dangler(node.subsystems.all_elements, ret[0]) - def add_emit_noise_left( self, qubits: QubitPartition, noise_ref: StrRef, modifier_ref: StrRef = "" ) -> NodeIndex: @@ -1085,6 +1062,14 @@ def finalize(self): order, register_names, ) + elif isinstance(pre_node, PreCopy): + self.add_copy_node( + samplex, + pre_node_idx, + pre_nodes_to_nodes, + order, + register_names, + ) else: raise SamplexBuildError(f"No lowering method found for {pre_node}.") diff --git a/samplomatic/samplex/nodes/__init__.py b/samplomatic/samplex/nodes/__init__.py index 75d74bd1..e8862c57 100644 --- a/samplomatic/samplex/nodes/__init__.py +++ b/samplomatic/samplex/nodes/__init__.py @@ -16,12 +16,14 @@ from .collection_node import CollectionNode from .combine_registers_node import CombineRegistersNode from .conversion_node import ConversionNode +from .copy_node import CopyNode from .evaluation_node import EvaluationNode from .inject_noise_node import InjectNoiseNode from .multiplication_node import LeftMultiplicationNode, RightMultiplicationNode from .node import Node from .pauli_past_clifford_node import PauliPastCliffordNode from .sampling_node import SamplingNode +from .scope_node import ScopeNode from .slice_register_node import SliceRegisterNode from .twirl_sampling_node import TwirlSamplingNode from .u2_param_multiplication_node import ( diff --git a/samplomatic/samplex/nodes/copy_node.py b/samplomatic/samplex/nodes/copy_node.py new file mode 100644 index 00000000..67958f4d --- /dev/null +++ b/samplomatic/samplex/nodes/copy_node.py @@ -0,0 +1,57 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""CopyNode""" + +from copy import deepcopy + +from ...annotations import VirtualType +from .evaluation_node import EvaluationNode + + +class CopyNode(EvaluationNode): + """Copies a register. + + Args: + copy_registers: + copy_prefix: + """ + + def __init__( + self, + register_name: str, + output_name: str, + output_type: VirtualType, + num_output_subsystems: int, + ): + self._register_name = register_name + self._output_name = output_name + self._output_type = output_type + self._num_output_subsystems = num_output_subsystems + + def instantiates(self): + return {self._output_name: (self._num_output_subsystems, self._output_type)} + + def reads_from(self): + return {self._register_name: (self._num_output_subsystems, self._output_type)} + + def evaluate(self, registers, *_): + registers[self._output_name] = deepcopy(registers[self._register_name]) + + def get_style(self): + return ( + super() + .get_style() + .append_data("Copies", self._register_name) + .append_data("Output Type", self._output_type.name) + .append_data("Output Num Subsystems", self._num_output_subsystems) + ) diff --git a/samplomatic/samplex/nodes/scope_node.py b/samplomatic/samplex/nodes/scope_node.py new file mode 100644 index 00000000..a26145b2 --- /dev/null +++ b/samplomatic/samplex/nodes/scope_node.py @@ -0,0 +1,101 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""ScopeNode""" + +from collections.abc import Sequence + +import orjson + +from ...aliases import RegisterName, SubsystemIndex +from ...annotations import VirtualType +from ...exceptions import DeserializationError +from ...utils.serialization import array_from_json, array_to_json +from .evaluation_node import EvaluationNode + + +class ScopeNode(EvaluationNode): + """Copies one or more registers. + + The registers are copied and have ``scope_prefix`` added to their name. + + Args: + scope_registers: + scope_prefix: + """ + + def __init__( + self, + scope_registers: dict[RegisterName, tuple[Sequence[SubsystemIndex], VirtualType]], + scope_prefix: str, + ): + self._scope_registers = scope_registers + self._scope_prefix = scope_prefix + + def _to_json_dict(self) -> dict[str, str]: + scope_registers_dict = {} + for key, values in self._scope_registers.items(): + value_list = [] + for v in values: + if isinstance(v, VirtualType): + value_list.append({"type": v.value}) + else: + value_list.append({"array": array_to_json(v)}) + scope_registers_dict[key] = value_list + + return { + "node_type": "13", + "scope_prefix": self._scope_prefix, + "scope_registers_dict": orjson.dumps(scope_registers_dict).decode("utf-8"), + } + + @classmethod + def _from_json_dict(cls, data: dict[str, str]) -> "ScopeNode": + raw_copy_dict = orjson.loads(data["scope_registers_dict"]) + scope_registers = {} + for name, values in raw_copy_dict.items(): + tuple_value = [] + for value in values: + if array_str := value.get("array"): + tuple_value.append(array_from_json(array_str)) + elif type_str := value.get("type"): + tuple_value.append(VirtualType(type_str)) + else: + raise DeserializationError(f"Invalid Operand type {value}") + + scope_registers[name] = tuple(tuple_value) + return cls(scope_registers, data["scope_prefix"]) + + def instantiates(self): + return { + f"{name}_{self._scope_prefix}": (len(subsys_idxs), output_type) + for name, (subsys_idxs, output_type) in self._scope_registers.items() + } + + def reads_from(self): + return { + name: (set(subysys_idxs), input_type) + for name, (subysys_idxs, input_type) in self._scope_registers.items() + } + + def evaluate(self, registers, *_): + for register_name, (subsys_idxs, _) in self._scope_registers.items(): + registers[f"{register_name}_{self._scope_prefix}"] = registers[register_name][ + subsys_idxs + ] + + def get_style(self): + scope_registers = { + register_name: (source_idxs.tolist(), str(virtual_type)) + for register_name, (source_idxs, virtual_type) in self._scope_registers.items() + } + return super().get_style().append_data("Scope", scope_registers) diff --git a/samplomatic/samplex/samplex_serialization.py b/samplomatic/samplex/samplex_serialization.py index 4aa5a724..902b22d0 100644 --- a/samplomatic/samplex/samplex_serialization.py +++ b/samplomatic/samplex/samplex_serialization.py @@ -34,6 +34,7 @@ from .nodes.collect_z2_to_output_node import CollectZ2ToOutputNode from .nodes.combine_registers_node import CombineRegistersNode from .nodes.conversion_node import ConversionNode +from .nodes.copy_node import CopyNode from .nodes.inject_noise_node import InjectNoiseNode from .nodes.multiplication_node import LeftMultiplicationNode, RightMultiplicationNode from .nodes.pauli_past_clifford_node import PauliPastCliffordNode @@ -61,6 +62,7 @@ LeftU2ParametricMultiplicationNode, RightMultiplicationNode, RightU2ParametricMultiplicationNode, + CopyNode, ] From 217c917aab8ffb638af31fcd7431b354a1316885 Mon Sep 17 00:00:00 2001 From: joshuasn <53916441+joshuasn@users.noreply.github.com> Date: Thu, 25 Sep 2025 08:29:56 -0230 Subject: [PATCH 09/22] Update samplomatic/builders/box_builder.py Co-authored-by: TsafrirA <113579969+TsafrirA@users.noreply.github.com> --- samplomatic/builders/box_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index f8a89ff0..a67e3c61 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""BoxTemplateBuilder""" +"""BoxBuilder""" from __future__ import annotations From 87dda74bfe15488adfc59ad69e570beb5824abce Mon Sep 17 00:00:00 2001 From: joshuasn Date: Fri, 26 Sep 2025 16:33:27 -0230 Subject: [PATCH 10/22] Working. --- docs/apidocs/samplomatic.samplex.nodes.rst | 8 -- samplomatic/builders/box_builder.py | 22 +++-- samplomatic/builders/dynamic_builder.py | 7 +- samplomatic/builders/get_builder.py | 11 +++ samplomatic/builders/specs.py | 8 ++ samplomatic/pre_samplex/pre_samplex.py | 33 +++++++ samplomatic/samplex/nodes/__init__.py | 1 - samplomatic/samplex/nodes/copy_node.py | 20 ++-- samplomatic/samplex/nodes/scope_node.py | 101 --------------------- 9 files changed, 83 insertions(+), 128 deletions(-) delete mode 100644 samplomatic/samplex/nodes/scope_node.py diff --git a/docs/apidocs/samplomatic.samplex.nodes.rst b/docs/apidocs/samplomatic.samplex.nodes.rst index 0daad2ac..5e194c95 100644 --- a/docs/apidocs/samplomatic.samplex.nodes.rst +++ b/docs/apidocs/samplomatic.samplex.nodes.rst @@ -113,14 +113,6 @@ samplomatic.samplex.nodes.sampling\_node module :show-inheritance: :undoc-members: -samplomatic.samplex.nodes.scope\_node module --------------------------------------------- - -.. automodule:: samplomatic.samplex.nodes.scope_node - :members: - :show-inheritance: - :undoc-members: - samplomatic.samplex.nodes.slice\_register\_node module ------------------------------------------------------ diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index 1aac1e0f..a7959b04 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -40,7 +40,9 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): def _append_dressed_layer(self) -> ParamIndices: """A helper function to add a collection dressing layer.""" - qubits = self.collection.qubits + qubits = self.collection.collect_qubits + if len(qubits) == 0: + return try: remapped_qubits = [ list(map(lambda k: self.template_state.qubit_map[k], subsys)) for subsys in qubits @@ -69,9 +71,13 @@ def _append_dressed_layer(self) -> ParamIndices: return param_idxs.reshape(len(qubits), -1) - def _append_barrier(self, label: str): + def _append_barrier(self, label: str, qubits=None): label = f"{label}{'_'.join(map(str, self.template_state.scope_idx))}" - all_qubits = self.template_state.qubit_map.values() + all_qubits = ( + self.template_state.qubit_map.values() + if qubits is None + else [v for k, v in self.template_state.qubit_map.items() if (k,) in qubits] + ) barrier = CircuitInstruction(Barrier(len(all_qubits), label), all_qubits) self.template_state.template.append(barrier) @@ -155,8 +161,9 @@ def parse(self, instr: CircuitInstruction): def lhs(self): self._append_barrier("L") param_idxs = self._append_dressed_layer() - self.samplex_state.add_collect(self.collection.qubits, self.collection.synth, param_idxs) - self._append_barrier("M") + collect_qubits = self.collection.collect_qubits + self.samplex_state.add_collect(collect_qubits, self.collection.synth, param_idxs) + self._append_barrier("M", collect_qubits) def rhs(self): self._append_barrier("R") @@ -238,7 +245,8 @@ def lhs(self): ) def rhs(self): - self._append_barrier("M") + collect_qubits = self.collection.collect_qubits + self._append_barrier("M", collect_qubits) param_idxs = self._append_dressed_layer() - self.samplex_state.add_collect(self.collection.qubits, self.collection.synth, param_idxs) + self.samplex_state.add_collect(collect_qubits, self.collection.synth, param_idxs) self._append_barrier("R") diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 1578b1e7..d241386b 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -54,9 +54,8 @@ def append_propagate(self, block: QuantumCircuit, new_block: QuantumCircuit): new_op = type(instr.operation)(*new_params) if new_params else instr.operation new_block.append(new_op, instr.qubits, instr.clbits) - self.pre_samplex.add_propagate( - instr, InstructionSpec(params=new_params, mode=InstructionMode.PROPAGATE) - ) + mode = InstructionMode.MULTIPLY if len(instr.qubits) == 1 else InstructionMode.PROPAGATE + self.pre_samplex.add_propagate(instr, InstructionSpec(params=new_params, mode=mode)) def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> ParamIndices: start = self.param_iter.idx @@ -65,7 +64,7 @@ def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> P for qubit in new_block.qubits: for instr in self.synth.make_template([qubit], self.param_iter): new_block.append(instr) - return params + return params.reshape(len(block.qubits), -1) class BoxRightIfElseBuilder(BoxIfElseBuilder): diff --git a/samplomatic/builders/get_builder.py b/samplomatic/builders/get_builder.py index d57a4964..46292500 100644 --- a/samplomatic/builders/get_builder.py +++ b/samplomatic/builders/get_builder.py @@ -63,6 +63,17 @@ def get_builder(instr: CircuitInstruction | None, qubits: Sequence[Qubit]) -> Bu collection = CollectionSpec(qubits) emission = EmissionSpec(qubits) + # TODO: if BoxOp contained a DAG we could look at the first topological generation and look at + # that? + if_else_qubits = QubitPartition(1) + for box_instr in instr.operation.body: + if box_instr.operation.name.startswith("if_else"): + for q in box_instr.qubits: + if_else_qubits.add((q,)) + + if len(if_else_qubits): + collection.if_else_qubits = if_else_qubits + seen_annotations: set[type[Annotation]] = set() for annotation in annotations: if (parser := SUPPORTED_ANNOTATIONS.get(annotation_type := type(annotation))) is None: diff --git a/samplomatic/builders/specs.py b/samplomatic/builders/specs.py index 72bd4fd0..b933d680 100644 --- a/samplomatic/builders/specs.py +++ b/samplomatic/builders/specs.py @@ -97,6 +97,14 @@ class CollectionSpec: synth: Synth[Qubit, Parameter, CircuitInstruction] | None = None """How to synthesize collection gates.""" + if_else_qubits: QubitPartition | None = None + + @property + def collect_qubits(self): + if self.if_else_qubits is None: + return self.qubits + return self.qubits.difference(self.if_else_qubits.all_elements) + @dataclass class InstructionSpec: diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index d361543e..7b3a9107 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -62,6 +62,7 @@ CollectTemplateValues, CollectZ2ToOutputNode, CombineRegistersNode, + CopyNode, InjectNoiseNode, LeftMultiplicationNode, LeftU2ParametricMultiplicationNode, @@ -1175,6 +1176,38 @@ def add_basis_transform_node( for pre_successor_idx in self.graph.successor_indices(pre_basis_idx): register_names[pre_successor_idx][pre_basis_idx] = reg_name + def add_copy_node( + self, + samplex: Samplex, + pre_copy_idx: NodeIndex, + pre_nodes_to_nodes: dict[NodeIndex, NodeIndex], + order: dict[NodeIndex, int], + register_names: dict[NodeIndex, dict[NodeIndex, RegisterName]], + ) -> None: + reg_idx = order[pre_copy_idx] + pre_pred_idx = next(iter(self.sorted_predecessor_idxs(pre_copy_idx, order))) + register_name = register_names[pre_copy_idx][pre_pred_idx] + + node_predecessor = samplex.graph[pred_idx := pre_nodes_to_nodes[pre_pred_idx]] + if register_name in node_predecessor.writes_to(): + virtual_type = node_predecessor.writes_to()[register_name][1] + else: + virtual_type = node_predecessor.instantiates()[register_name][1] + + copy_node = CopyNode( + register_name, + copy_name := f"copy_{reg_idx}", + virtual_type, + len(self.graph[pre_copy_idx].subsystems), + ) + + node_idx = samplex.add_node(copy_node) + samplex.add_edge(pred_idx, node_idx) + pre_nodes_to_nodes[pre_copy_idx] = node_idx + + for pre_successor_idx in self.graph.successor_indices(pre_copy_idx): + register_names[pre_successor_idx][pre_copy_idx] = copy_name + def add_inject_noise_node( self, samplex: Samplex, diff --git a/samplomatic/samplex/nodes/__init__.py b/samplomatic/samplex/nodes/__init__.py index e8862c57..4c266148 100644 --- a/samplomatic/samplex/nodes/__init__.py +++ b/samplomatic/samplex/nodes/__init__.py @@ -23,7 +23,6 @@ from .node import Node from .pauli_past_clifford_node import PauliPastCliffordNode from .sampling_node import SamplingNode -from .scope_node import ScopeNode from .slice_register_node import SliceRegisterNode from .twirl_sampling_node import TwirlSamplingNode from .u2_param_multiplication_node import ( diff --git a/samplomatic/samplex/nodes/copy_node.py b/samplomatic/samplex/nodes/copy_node.py index 67958f4d..e6114e4a 100644 --- a/samplomatic/samplex/nodes/copy_node.py +++ b/samplomatic/samplex/nodes/copy_node.py @@ -22,8 +22,10 @@ class CopyNode(EvaluationNode): """Copies a register. Args: - copy_registers: - copy_prefix: + regoster_name: + output_name: + register_type: + num_subsystems: """ def __init__( @@ -31,18 +33,22 @@ def __init__( register_name: str, output_name: str, output_type: VirtualType, - num_output_subsystems: int, + num_subsystems: int, ): self._register_name = register_name self._output_name = output_name self._output_type = output_type - self._num_output_subsystems = num_output_subsystems + self._num_subsystems = num_subsystems + + @property + def outgoing_register_type(self): + return self._output_type def instantiates(self): - return {self._output_name: (self._num_output_subsystems, self._output_type)} + return {self._output_name: (self._num_subsystems, self._output_type)} def reads_from(self): - return {self._register_name: (self._num_output_subsystems, self._output_type)} + return {self._register_name: (set(range(self._num_subsystems)), self._output_type)} def evaluate(self, registers, *_): registers[self._output_name] = deepcopy(registers[self._register_name]) @@ -53,5 +59,5 @@ def get_style(self): .get_style() .append_data("Copies", self._register_name) .append_data("Output Type", self._output_type.name) - .append_data("Output Num Subsystems", self._num_output_subsystems) + .append_data("Output Num Subsystems", self._num_subsystems) ) diff --git a/samplomatic/samplex/nodes/scope_node.py b/samplomatic/samplex/nodes/scope_node.py deleted file mode 100644 index a26145b2..00000000 --- a/samplomatic/samplex/nodes/scope_node.py +++ /dev/null @@ -1,101 +0,0 @@ -# This code is a Qiskit project. -# -# (C) Copyright IBM 2025. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""ScopeNode""" - -from collections.abc import Sequence - -import orjson - -from ...aliases import RegisterName, SubsystemIndex -from ...annotations import VirtualType -from ...exceptions import DeserializationError -from ...utils.serialization import array_from_json, array_to_json -from .evaluation_node import EvaluationNode - - -class ScopeNode(EvaluationNode): - """Copies one or more registers. - - The registers are copied and have ``scope_prefix`` added to their name. - - Args: - scope_registers: - scope_prefix: - """ - - def __init__( - self, - scope_registers: dict[RegisterName, tuple[Sequence[SubsystemIndex], VirtualType]], - scope_prefix: str, - ): - self._scope_registers = scope_registers - self._scope_prefix = scope_prefix - - def _to_json_dict(self) -> dict[str, str]: - scope_registers_dict = {} - for key, values in self._scope_registers.items(): - value_list = [] - for v in values: - if isinstance(v, VirtualType): - value_list.append({"type": v.value}) - else: - value_list.append({"array": array_to_json(v)}) - scope_registers_dict[key] = value_list - - return { - "node_type": "13", - "scope_prefix": self._scope_prefix, - "scope_registers_dict": orjson.dumps(scope_registers_dict).decode("utf-8"), - } - - @classmethod - def _from_json_dict(cls, data: dict[str, str]) -> "ScopeNode": - raw_copy_dict = orjson.loads(data["scope_registers_dict"]) - scope_registers = {} - for name, values in raw_copy_dict.items(): - tuple_value = [] - for value in values: - if array_str := value.get("array"): - tuple_value.append(array_from_json(array_str)) - elif type_str := value.get("type"): - tuple_value.append(VirtualType(type_str)) - else: - raise DeserializationError(f"Invalid Operand type {value}") - - scope_registers[name] = tuple(tuple_value) - return cls(scope_registers, data["scope_prefix"]) - - def instantiates(self): - return { - f"{name}_{self._scope_prefix}": (len(subsys_idxs), output_type) - for name, (subsys_idxs, output_type) in self._scope_registers.items() - } - - def reads_from(self): - return { - name: (set(subysys_idxs), input_type) - for name, (subysys_idxs, input_type) in self._scope_registers.items() - } - - def evaluate(self, registers, *_): - for register_name, (subsys_idxs, _) in self._scope_registers.items(): - registers[f"{register_name}_{self._scope_prefix}"] = registers[register_name][ - subsys_idxs - ] - - def get_style(self): - scope_registers = { - register_name: (source_idxs.tolist(), str(virtual_type)) - for register_name, (source_idxs, virtual_type) in self._scope_registers.items() - } - return super().get_style().append_data("Scope", scope_registers) From bc6e8a592766bb4a922be4c37b7afe011809fe38 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Thu, 2 Oct 2025 11:49:34 -0230 Subject: [PATCH 11/22] working. --- samplomatic/builders/box_builder.py | 2 ++ samplomatic/builders/dynamic_builder.py | 26 ++++++++++++------- samplomatic/builders/get_builder.py | 17 +++++------- samplomatic/builders/specs.py | 2 ++ samplomatic/pre_samplex/pre_samplex.py | 23 ++++++++-------- .../samplex/nodes/slice_register_node.py | 24 ----------------- 6 files changed, 38 insertions(+), 56 deletions(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index a7959b04..301996b6 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -198,6 +198,8 @@ def parse(self, instr: CircuitInstruction): return if name.startswith("if_else"): + for qubit in instr.qubits: + self.collection.if_else_qubits.add((qubit,)) builder = BoxRightIfElseBuilder( instr, self.samplex_state, self.collection.synth, self.template_state.param_iter ) diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index d241386b..41bb9d48 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -46,16 +46,22 @@ def block_qubit_map(self, block) -> dict[Qubit, QubitIndex]: def append_propagate(self, block: QuantumCircuit, new_block: QuantumCircuit): for instr in block: - new_params = [] - param_mapping = [] - for param in instr.operation.params: - param_mapping.append([self.param_iter.idx, param]) - new_params.append(next(self.param_iter)) - - new_op = type(instr.operation)(*new_params) if new_params else instr.operation - new_block.append(new_op, instr.qubits, instr.clbits) - mode = InstructionMode.MULTIPLY if len(instr.qubits) == 1 else InstructionMode.PROPAGATE - self.pre_samplex.add_propagate(instr, InstructionSpec(params=new_params, mode=mode)) + if len(instr.qubits) == 1: + mode = InstructionMode.MULTIPLY + params = [] + if instr.operation.is_parameterized(): + params.extend((None, param) for param in instr.operation.params) + else: + params = [] + param_mapping = [] + for param in instr.operation.params: + param_mapping.append([self.param_iter.idx, param]) + params.append(next(self.param_iter)) + + new_op = type(instr.operation)(*params) if params else instr.operation + new_block.append(new_op, instr.qubits, instr.clbits) + mode = InstructionMode.PROPAGATE + self.pre_samplex.add_propagate(instr, InstructionSpec(params=params, mode=mode)) def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> ParamIndices: start = self.param_iter.idx diff --git a/samplomatic/builders/get_builder.py b/samplomatic/builders/get_builder.py index 46292500..a82fb5df 100644 --- a/samplomatic/builders/get_builder.py +++ b/samplomatic/builders/get_builder.py @@ -63,17 +63,6 @@ def get_builder(instr: CircuitInstruction | None, qubits: Sequence[Qubit]) -> Bu collection = CollectionSpec(qubits) emission = EmissionSpec(qubits) - # TODO: if BoxOp contained a DAG we could look at the first topological generation and look at - # that? - if_else_qubits = QubitPartition(1) - for box_instr in instr.operation.body: - if box_instr.operation.name.startswith("if_else"): - for q in box_instr.qubits: - if_else_qubits.add((q,)) - - if len(if_else_qubits): - collection.if_else_qubits = if_else_qubits - seen_annotations: set[type[Annotation]] = set() for annotation in annotations: if (parser := SUPPORTED_ANNOTATIONS.get(annotation_type := type(annotation))) is None: @@ -88,7 +77,13 @@ def get_builder(instr: CircuitInstruction | None, qubits: Sequence[Qubit]) -> Bu if emission.noise_ref and not emission.twirl_register_type: raise BuildError(f"Cannot get a builder for {annotations}. Inject noise requires twirling.") + collection.if_else_qubits = QubitPartition(1) if collection.dressing is DressingMode.LEFT: + # TODO: if BoxOp contained a DAG we could look at the first topological generation + for box_instr in instr.operation.body: + if box_instr.operation.name.startswith("if_else"): + for q in box_instr.qubits: + collection.if_else_qubits.add((q,)) return LeftBoxBuilder(collection, emission) return RightBoxBuilder(collection, emission) diff --git a/samplomatic/builders/specs.py b/samplomatic/builders/specs.py index b933d680..cdab2018 100644 --- a/samplomatic/builders/specs.py +++ b/samplomatic/builders/specs.py @@ -98,9 +98,11 @@ class CollectionSpec: """How to synthesize collection gates.""" if_else_qubits: QubitPartition | None = None + """The subset of 'qubits' collected in conditional operations.""" @property def collect_qubits(self): + """The subset of 'qubits' collected in the box boundary.""" if self.if_else_qubits is None: return self.qubits return self.qubits.difference(self.if_else_qubits.all_elements) diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 7b3a9107..9b949a39 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -393,14 +393,6 @@ def qubits_to_indices(self, qubits: QubitPartition) -> QubitIndicesPartition: qubits.num_elements_per_part, [tuple(self.qubit_map[q] for q in sys) for sys in qubits] ) - def add_force_copy_nodes(self, node_idxs: Iterable[NodeIndex]): - """Add node indices for which a register will be forced to copy.""" - self._forced_copy_node_idxs.update(node_idxs) - - def remove_force_copy_nodes(self, node_idxs: Iterable[NodeIndex]): - """Remove node indices for which a register will be forced to copy.""" - self._forced_copy_node_idxs.difference_update(node_idxs) - def add_collect( self, qubits: QubitPartition, @@ -1188,12 +1180,22 @@ def add_copy_node( pre_pred_idx = next(iter(self.sorted_predecessor_idxs(pre_copy_idx, order))) register_name = register_names[pre_copy_idx][pre_pred_idx] - node_predecessor = samplex.graph[pred_idx := pre_nodes_to_nodes[pre_pred_idx]] + node_predecessor = samplex.graph[pre_nodes_to_nodes[pre_pred_idx]] if register_name in node_predecessor.writes_to(): virtual_type = node_predecessor.writes_to()[register_name][1] else: virtual_type = node_predecessor.instantiates()[register_name][1] + combine_node_idx = self.add_combine_node( + samplex, + pre_copy_idx, + pre_nodes_to_nodes, + order, + register_names, + f"combine_{reg_idx}", + virtual_type, + ) + copy_node = CopyNode( register_name, copy_name := f"copy_{reg_idx}", @@ -1202,7 +1204,7 @@ def add_copy_node( ) node_idx = samplex.add_node(copy_node) - samplex.add_edge(pred_idx, node_idx) + samplex.add_edge(combine_node_idx, node_idx) pre_nodes_to_nodes[pre_copy_idx] = node_idx for pre_successor_idx in self.graph.successor_indices(pre_copy_idx): @@ -1345,7 +1347,6 @@ def add_combine_node( input_register_name=input_register_name, output_register_name=combined_register_name, slice_idxs=slice_idxs, - force_copy=pre_edge.force_register_copy, ) else: combine_node = CombineRegistersNode( diff --git a/samplomatic/samplex/nodes/slice_register_node.py b/samplomatic/samplex/nodes/slice_register_node.py index a1825a37..70527a6c 100644 --- a/samplomatic/samplex/nodes/slice_register_node.py +++ b/samplomatic/samplex/nodes/slice_register_node.py @@ -17,7 +17,6 @@ from collections.abc import Sequence import numpy as np -from numpy.typing import ArrayLike from ...aliases import RegisterName, SubsystemIndex from ...annotations import VirtualType @@ -42,8 +41,6 @@ class SliceRegisterNode(EvaluationNode): input_register_name: The name of the input register. output_register_name: The name of the output register. slice_idxs: The indices used to slice the register. - force_copy: Whether or not to force output register to be a copy instead of a view of - the input register. Raises: SamplexConstructionError: If ``slice_idxs`` has the wrong shape. @@ -56,7 +53,6 @@ def __init__( input_register_name: RegisterName, output_register_name: RegisterName, slice_idxs: slice | Sequence[SubsystemIndex], - force_copy: bool = False, ): self._input_type = input_type self._output_type = output_type @@ -72,9 +68,6 @@ def __init__( f"'slice_idxs' for '{input_register_name}' has a shape {slice_idxs.shape}, " "but a shape with a single axes is required." ) - # Check if indices could be converted to a slice - elif not force_copy: - self._slice_idxs = get_slice_from_idxs(slice_idxs) else: self._slice_idxs = slice_idxs @@ -153,20 +146,3 @@ def validate_and_update(self, register_descriptions): def evaluate(self, registers, *_): converted_register = registers[self._input_register_name].convert_to(self._output_type) registers[self._output_register_name] = converted_register[self._slice_idxs] - - -def get_slice_from_idxs(slice_idxs: ArrayLike) -> ArrayLike | slice: - """A helper function that returns a slice object, if indices permit.""" - if len(slice_idxs) == 1: - return slice(slice_idxs[0], slice_idxs[0] + 1, 1) - else: - step = slice_idxs[1] - slice_idxs[0] - expected = slice_idxs[0] + step * np.arange(len(slice_idxs)) - - if np.array_equal(slice_idxs, expected): - return slice( - int(slice_idxs[0]), - int(slice_idxs[-1] + step) if slice_idxs[-1] + step >= 0 else None, - int(step), - ) - return slice_idxs From d78c51c53432870a89f0c59be1726234e203d4e1 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Thu, 2 Oct 2025 11:49:48 -0230 Subject: [PATCH 12/22] Some tests. --- test/integration/test_dynamic_circuits.py | 26 ++++----- .../test_nodes/test_slice_register_node.py | 54 ------------------- 2 files changed, 10 insertions(+), 70 deletions(-) diff --git a/test/integration/test_dynamic_circuits.py b/test/integration/test_dynamic_circuits.py index 0d2873e1..ed9de798 100644 --- a/test/integration/test_dynamic_circuits.py +++ b/test/integration/test_dynamic_circuits.py @@ -68,16 +68,14 @@ def test_right_dressed_twirled_conditional(self, save_plot): circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1, 2) + circuit.noop(0, 1) with circuit.box([Twirl(dressing="right")]): - circuit.cx(0, 1) with circuit.if_test((circuit.clbits[0], 1)) as _else: circuit.cx(0, 1) circuit.x(0) with _else: circuit.cx(1, 0) circuit.sx(0) - circuit.x(2) circuit.h(1) circuit.measure_all() @@ -85,18 +83,17 @@ def test_right_dressed_twirled_conditional(self, save_plot): def test_right_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a right-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) + circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1, 2) + circuit.noop(0, 1) with circuit.box([Twirl(dressing="right")]): circuit.cx(0, 1) with circuit.if_test((circuit.clbits[0], 1)): circuit.cx(0, 1) circuit.x(0) - circuit.x(2) circuit.h(1) circuit.measure_all() @@ -112,6 +109,7 @@ def test_right_dressed_parametric_twirled_conditional(self, save_plot): with circuit.box([Twirl(dressing="left")]): circuit.noop(0, 1, 2) with circuit.box([Twirl(dressing="right")]): + circuit.noop(2) circuit.cx(0, 1) with circuit.if_test((circuit.clbits[0], 1)) as _else: circuit.cx(0, 1) @@ -121,7 +119,6 @@ def test_right_dressed_parametric_twirled_conditional(self, save_plot): circuit.cx(1, 0) circuit.sx(0) circuit.rx(2 * p, 1) - circuit.x(2) circuit.h(1) circuit.measure_all() @@ -129,7 +126,7 @@ def test_right_dressed_parametric_twirled_conditional(self, save_plot): def test_left_dressed_twirled_conditional(self, save_plot): """Test a circuit with a conditional in a left-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) + circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.measure(0, 0) @@ -140,11 +137,11 @@ def test_left_dressed_twirled_conditional(self, save_plot): with _else: circuit.sx(0) circuit.cx(1, 0) - circuit.x(2) circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): - circuit.h(1) - circuit.noop(0, 2) + circuit.noop(0, 1) + + circuit.h(1) circuit.measure_all() @@ -152,19 +149,16 @@ def test_left_dressed_twirled_conditional(self, save_plot): def test_left_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a left-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) + circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): with circuit.if_test((circuit.clbits[0], 1)): - circuit.x(0) circuit.cx(0, 1) - circuit.x(2) - circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): + circuit.noop(0) circuit.h(1) - circuit.noop(0, 2) circuit.measure_all() diff --git a/test/unit/test_samplex/test_nodes/test_slice_register_node.py b/test/unit/test_samplex/test_nodes/test_slice_register_node.py index 18b90382..2fd15230 100644 --- a/test/unit/test_samplex/test_nodes/test_slice_register_node.py +++ b/test/unit/test_samplex/test_nodes/test_slice_register_node.py @@ -17,7 +17,6 @@ from samplomatic.annotations import VirtualType from samplomatic.exceptions import SamplexConstructionError from samplomatic.samplex.nodes import SliceRegisterNode -from samplomatic.samplex.nodes.slice_register_node import get_slice_from_idxs from samplomatic.virtual_registers import PauliRegister @@ -73,36 +72,6 @@ def test_slicing(slice_idxs): assert node.outgoing_register_type is VirtualType.PAULI -@pytest.mark.parametrize( - ("slice_idxs", "view"), - [ - ([1, 0], True), - ([2, 1], True), - ([2, 0], True), - ([0, 2, 1], False), - ([0], True), - ([0, 2], True), - ], -) -@pytest.mark.parametrize("force_copy", [True, False]) -def test_slicing_and_permuting(slice_idxs, view, force_copy): - """Test slicing and permuting at the same time a register using a ``SliceRegisterNode``.""" - registers = {"reg_in": PauliRegister([[0, 1, 2, 3], [1, 2, 3, 0], [2, 3, 0, 1], [0, 2, 3, 1]])} - node = SliceRegisterNode( - VirtualType.PAULI, VirtualType.PAULI, "reg_in", "reg_out", slice_idxs, force_copy - ) - - node.evaluate(registers, np.empty(())) - expected = registers["reg_in"].virtual_gates[slice_idxs].tolist() - assert registers["reg_out"].virtual_gates.tolist() == expected - if not force_copy and view: - assert np.shares_memory( - registers["reg_out"].virtual_gates, registers["reg_in"].virtual_gates - ) - assert node.reads_from()["reg_in"][0] == set(slice_idxs) - assert node.instantiates()["reg_out"][0] == len(slice_idxs) - - def test_raises(): """Test that the ``SliceRegisterNode`` raises.""" with pytest.raises(SamplexConstructionError, match="single axes"): @@ -123,26 +92,3 @@ def test_raises(): ) with pytest.raises(SamplexConstructionError, match="convertable"): node.validate_and_update({"reg_in": (2, VirtualType.U2)}) - - -@pytest.mark.parametrize( - "sliced_idxs", [[0, 2, 4], [2, 4], [0, 1, 2], [1, 2], [3, 2, 1], [1, 0], [4]] -) -def test_get_slice_helper_function(sliced_idxs): - """Test that the helper function get_slice_from_idxs returns a slice when possible.""" - a = np.arange(max(sliced_idxs) + 1) - res = a[get_slice_from_idxs(sliced_idxs)] - - assert np.array_equal(res, sliced_idxs) - assert np.shares_memory(res, a) - - -def test_get_slice_helper_function_no_slice(): - """Test that the helper function get_slice_from_idxs doesn't return a slice when - it's impossible.""" - sliced_idxs = [0, 1, 6, 3] - a = np.arange(max(sliced_idxs) + 1) - res = a[get_slice_from_idxs(sliced_idxs)] - - assert np.array_equal(res, sliced_idxs) - assert not np.shares_memory(res, a) From 6b1b91391c754e639e472533d3e4cda07c44579a Mon Sep 17 00:00:00 2001 From: joshuasn Date: Thu, 2 Oct 2025 17:18:56 -0230 Subject: [PATCH 13/22] Removing a little more. --- samplomatic/builders/specs.py | 9 --- samplomatic/pre_samplex/graph_data.py | 6 +- samplomatic/pre_samplex/pre_samplex.py | 65 +++++++++---------- samplomatic/pre_samplex/utils.py | 1 - .../unit/test_pre_samplex/test_pre_samplex.py | 29 --------- 5 files changed, 32 insertions(+), 78 deletions(-) diff --git a/samplomatic/builders/specs.py b/samplomatic/builders/specs.py index cdab2018..e22a700c 100644 --- a/samplomatic/builders/specs.py +++ b/samplomatic/builders/specs.py @@ -128,12 +128,3 @@ class InstructionSpec: clbit_idxs: list[ClbitIndex] = field(default_factory=list) """The mode of an added instruction.""" - - if_else: tuple[list[InstructionSpec], ParamIndices, list[InstructionSpec], ParamIndices] = ( - field(default_factory=lambda: ([], EMPTY_IDXS, [], EMPTY_IDXS)) - ) - """The specs for an `IfElseOp`. - - Each branch of the operation is represented by two fields: a list of `InstructionSpec` for - the branch's instructions, followed by `ParamIndices` for the collectors of the branch. The - true branch is first in the tuple, and the else branch is second.""" diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index 9bc66027..19f2bf14 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -44,7 +44,7 @@ def get_style(self) -> NodeStyle: @dataclass class PreCopy(PreNode): - """""" + """The copy node type used during building.""" @dataclass @@ -57,9 +57,6 @@ class PreEdge: direction: Direction """Whether the edge is moving forwards or backwards in circuit-time.""" - force_register_copy: bool = False - """Whether the edge should force the receiving node to get a copy of the register.""" - def get_style(self) -> EdgeStyle: """Summarizes the style of this node when plotted via :func:`~.plot_graph`.""" return ( @@ -68,7 +65,6 @@ def get_style(self) -> EdgeStyle: ) .append_data("Subsystems", list(self.subsystems)) .append_data("Direction", self.direction.name) - .append_data("Force copy", self.force_register_copy) ) def add_subsystems(self, new_subsystems: QubitIndicesPartition): diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 9b949a39..bae0967b 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -137,7 +137,7 @@ class DanglerMatch: def match_node(self, node: PreNode) -> bool: """Check if a node matches the conditions specified in the object""" return (self.direction is None or node.direction in (self.direction, Direction.BOTH)) and ( - self.node_types is None or isinstance(node, self.node_types) or type(node) is PreCopy + self.node_types is None or isinstance(node, self.node_types) ) @@ -167,10 +167,6 @@ class PreSamplex: circuit. passthrough_params: List of :class:`~.ParamSpec` for parameters which exist in the template but are not generated in the collectors. - forced_copy_node_idxs: List of node indices for which copying of registers will be forced. - The nodes behave differently for edges with left/right direction. For left direction, - the incoming node is checked against `forced_copy_node_idxs`, while for right direction, - the outgoing node is. """ def __init__( @@ -186,7 +182,6 @@ def __init__( basis_transforms: dict[str, int] | None = None, twirled_clbits: set[ClbitIndex] | None = None, passthrough_params: ParamSpec | None = None, - forced_copy_node_idxs: set[NodeIndex] | None = None, ): self.graph = PyDiGraph[PreNode, PreEdge](multigraph=True) if graph is None else graph self.qubit_map: dict[Qubit, QubitIndex] = {} if qubit_map is None else qubit_map @@ -205,9 +200,6 @@ def __init__( self.passthrough_params: ParamSpec = ( [] if passthrough_params is None else passthrough_params ) - self._forced_copy_node_idxs: set[NodeIndex] = ( - set() if forced_copy_node_idxs is None else forced_copy_node_idxs - ) def remap(self, qubit_map: dict[Qubit, QubitIndex]) -> PreSamplex: """Remap the object to a new :class:`~.PreSamplex` object. @@ -230,7 +222,6 @@ def remap(self, qubit_map: dict[Qubit, QubitIndex]) -> PreSamplex: self._basis_transforms, self._twirled_clbits, self.passthrough_params, - self._forced_copy_node_idxs, ) def find_danglers( @@ -356,7 +347,7 @@ def enforce_no_propagation(self, instr: CircuitInstruction): # in the future when we have multi-qubit virtual groups, this can't be hard-coded to 1 subsystems = QubitIndicesPartition(1, [(self.qubit_map[qubit],) for qubit in instr.qubits]) - match = DanglerMatch(node_types=(PreEmit, PrePropagate), direction=Direction.RIGHT) + match = DanglerMatch(node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT) if any(True for _ in self.find_danglers(match, subsystems)): raise SamplexBuildError(f"Cannot propagate through {instr.operation.name} instruction.") match = DanglerMatch(direction=Direction.LEFT) @@ -426,19 +417,12 @@ def add_collect( ) # collect any nodes that need collecting and unmark them as dangling - match = DanglerMatch(node_types=(PreEmit, PrePropagate), direction=Direction.RIGHT) + match = DanglerMatch(node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT) for found_idx, found_subsystems in self.find_then_remove_danglers(match, subsystems): if self.graph.has_edge(found_idx, node_idx): - # The force_register_copy stays the same and doesn't need update. self.graph.get_edge_data(found_idx, node_idx).add_subsystems(found_subsystems) else: - self.graph.add_edge( - found_idx, - node_idx, - PreEdge( - found_subsystems, Direction.RIGHT, found_idx in self._forced_copy_node_idxs - ), - ) + self.graph.add_edge(found_idx, node_idx, PreEdge(found_subsystems, Direction.RIGHT)) # prevent dangling pre-propagate left nodes from catching any further action because # this collection is in the way @@ -492,7 +476,7 @@ def add_z2_collect(self, qubits: QubitPartition, clbit_idxs: Sequence[ClbitIndex # Collect relevant nodes which are an optional dangler, and leave them optionally dangling # This needs to happen first, so the new optional danglers created later won't be counted. match = DanglerMatch( - node_types=(PreEmit, PrePropagate), + node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT, dangler_type=DanglerType.OPTIONAL, ) @@ -504,7 +488,7 @@ def add_z2_collect(self, qubits: QubitPartition, clbit_idxs: Sequence[ClbitIndex # Collect every relevant node which is a required dangler, # then convert it to an optional dangler. match = DanglerMatch( - node_types=(PreEmit, PrePropagate), + node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT, dangler_type=DanglerType.REQUIRED, ) @@ -550,7 +534,9 @@ def add_emit_twirl(self, qubits: QubitPartition, register_type: VirtualType) -> # connect this emmision there. we do NOT want to remove them as dangling because they # might need to be used again for future emissions, for example, a Pauli twirl followed by # a noise injection. - match = DanglerMatch(node_types=(PreCollect, PrePropagate), direction=Direction.LEFT) + match = DanglerMatch( + node_types=(PreCollect, PreCopy, PrePropagate), direction=Direction.LEFT + ) aggregate_found_subsystems = set() for found_idx, found_subsystems in self.find_danglers(match, subsystems): @@ -558,7 +544,7 @@ def add_emit_twirl(self, qubits: QubitPartition, register_type: VirtualType) -> self.graph.add_edge( node_idx, found_idx, - PreEdge(found_subsystems, Direction.LEFT, found_idx in self._forced_copy_node_idxs), + PreEdge(found_subsystems, Direction.LEFT), ) if aggregate_found_subsystems != set(subsystems): @@ -578,7 +564,9 @@ def _add_left(self, node: PreNode): This method adds edges to any node that is dangling with overlapping subsystems. """ node_idx = self.graph.add_node(node) - match = DanglerMatch(node_types=(PreCollect, PrePropagate), direction=Direction.LEFT) + match = DanglerMatch( + node_types=(PreCopy, PreCollect, PrePropagate), direction=Direction.LEFT + ) for found_idx, found_subsystems in self.find_danglers(match, node.subsystems): self.graph.add_edge(node_idx, found_idx, PreEdge(found_subsystems, Direction.LEFT)) @@ -767,13 +755,11 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): rightward_node_candidate = NodeCandidate( self.graph, PrePropagate(subsystems, Direction.RIGHT, op, partition, spec) ) - match = DanglerMatch(node_types=(PreEmit, PrePropagate), direction=Direction.RIGHT) + match = DanglerMatch(node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT) all_found_qubit_idxs = set() for found_idx, found_subsystems in self.find_then_remove_danglers(match, subsystems): all_found_qubit_idxs.update(found_subsystems.all_elements) - edge = PreEdge( - found_subsystems, Direction.RIGHT, found_idx in self._forced_copy_node_idxs - ) + edge = PreEdge(found_subsystems, Direction.RIGHT) # if the node candidate hasn't been added to the graph yet, it will be here: self.graph.add_edge(found_idx, rightward_node_candidate.node_idx, edge) @@ -789,12 +775,12 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): self.graph, PrePropagate(subsystems, Direction.LEFT, op, partition, spec) ) all_found_qubit_idxs = set() - match = DanglerMatch(node_types=(PreCollect, PrePropagate), direction=Direction.LEFT) + match = DanglerMatch( + node_types=(PreCollect, PreCopy, PrePropagate), direction=Direction.LEFT + ) for found_idx, found_subsystems in self.find_then_remove_danglers(match, subsystems): all_found_qubit_idxs.update(found_subsystems.all_elements) - edge = PreEdge( - found_subsystems, Direction.LEFT, found_idx in self._forced_copy_node_idxs - ) + edge = PreEdge(found_subsystems, Direction.LEFT) # if the node candidate hasn't been added to the graph yet, it will be here: self.graph.add_edge(leftward_node_candidate.node_idx, found_idx, edge) @@ -955,7 +941,7 @@ def validate_no_rightward_danglers(self): Raises: SamplexBuildError: If any nodes are expecting collections. """ - match = DanglerMatch(node_types=(PreEmit, PrePropagate), direction=Direction.RIGHT) + match = DanglerMatch(node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT) uncollected_qubit_idxs = set() for qubit_idx, node_idxs in self._dangling.items(): for node_idx in node_idxs: @@ -1176,6 +1162,17 @@ def add_copy_node( order: dict[NodeIndex, int], register_names: dict[NodeIndex, dict[NodeIndex, RegisterName]], ) -> None: + """Add copy node to a samplex, mutating it in place. + + Args: + samplex: The samplex to add nodes to. + pre_copy_idx: The index of the pre-copy node to turn into a copy node. + pre_nodes_to_nodes: A map from pre-node indices to node indices. + order: A map from pre-node indices to integers representing their position in a + topological sort of the samplex state graph. + register_names: A map such that ``register_names[a][b]`` is the name of the register + implied by the edge (a, b) in the samplex state graph. + """ reg_idx = order[pre_copy_idx] pre_pred_idx = next(iter(self.sorted_predecessor_idxs(pre_copy_idx, order))) register_name = register_names[pre_copy_idx][pre_pred_idx] diff --git a/samplomatic/pre_samplex/utils.py b/samplomatic/pre_samplex/utils.py index 7a7ac6f3..aabef854 100644 --- a/samplomatic/pre_samplex/utils.py +++ b/samplomatic/pre_samplex/utils.py @@ -81,5 +81,4 @@ def merge_pre_edges( return PreEdge( subsystems=QubitIndicesPartition.union(*(edge.subsystems for edge in edges)), direction=next(iter(directions)), - force_register_copy=any(edge.force_register_copy for edge in edges), ) diff --git a/test/unit/test_pre_samplex/test_pre_samplex.py b/test/unit/test_pre_samplex/test_pre_samplex.py index a87d753c..85dc4ae7 100644 --- a/test/unit/test_pre_samplex/test_pre_samplex.py +++ b/test/unit/test_pre_samplex/test_pre_samplex.py @@ -18,9 +18,7 @@ from qiskit.circuit.library import CXGate, Measure, XGate from rustworkx import topological_sort -from samplomatic import Twirl from samplomatic.annotations import VirtualType -from samplomatic.builders import pre_build from samplomatic.builders.specs import InstructionMode, InstructionSpec from samplomatic.constants import Direction from samplomatic.exceptions import SamplexBuildError @@ -613,33 +611,6 @@ def test_unsupported_propagate_error(self): ): pre_samplex.finalize() - def test_if_else_with_mergable_preedges(self): - """Test that merging of PreEdges maintains their force_register_copy property. - - Because it is a bit difficult to do by hand, we use the full pre_build. - """ - circuit = QuantumCircuit(2, 1) - circuit.measure(0, 0) - with circuit.box([Twirl(dressing="left")]): - with circuit.if_test((circuit.clbits[0], 1)) as _else: - circuit.x(0) - circuit.x(1) - with _else: - circuit.sx(0) - circuit.sx(1) - with circuit.box([Twirl(dressing="right")]): - circuit.noop(0, 1) - - _, pre_samplex = pre_build(circuit) - pre_samplex.merge_parallel_pre_propagate_nodes() - graph = pre_samplex.graph - for emit_node in [6, 7]: - assert not graph.get_edge_data(emit_node, 4).force_register_copy - assert graph.get_edge_data(emit_node, 9).force_register_copy - assert graph[4].operation.name == "x" - assert graph[9].operation.name == "sx" - assert graph[9].subsystems.all_elements == {0, 1} - class TestDraw: """Test the ``draw`` method.""" From d60e4ecc7fb1c34c9d2f6a41f8ac8b75e5b36b95 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Tue, 14 Oct 2025 16:11:16 -0230 Subject: [PATCH 14/22] Typo. --- samplomatic/builders/box_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index a67e3c61..08944fe5 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -38,7 +38,7 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): self.entangled_qubits = set() def _append_dressed_layer(self) -> ParamIndices: - """A helper function to add a collection dressing layer.""" + """Add a dressed layer.""" qubits = self.collection.qubits try: remapped_qubits = [ From 90ac8c67ee8909f2629065fb03fa2f09527eae4e Mon Sep 17 00:00:00 2001 From: joshuasn Date: Tue, 14 Oct 2025 17:23:48 -0230 Subject: [PATCH 15/22] Further but..? --- samplomatic/builders/box_builder.py | 57 +++++---- samplomatic/builders/dynamic_builder.py | 12 +- samplomatic/builders/passthrough_builder.py | 9 +- samplomatic/pre_samplex/graph_data.py | 9 +- samplomatic/pre_samplex/pre_samplex.py | 33 +++-- test/integration/test_dynamic_circuits.py | 22 ++-- .../test_general_build_errors.py | 113 +++++++++--------- 7 files changed, 131 insertions(+), 124 deletions(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index 201b4754..37114e3e 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -15,7 +15,7 @@ from __future__ import annotations import numpy as np -from qiskit.circuit import Barrier +from qiskit.circuit import Barrier, IfElseOp from ..aliases import CircuitInstruction, ParamIndices from ..exceptions import SamplexBuildError, TemplateBuildError @@ -23,7 +23,7 @@ from ..pre_samplex import PreSamplex from .builder import Builder from .dynamic_builder import BoxLeftIfElseBuilder, BoxRightIfElseBuilder -from .specs import CollectionSpec, EmissionSpec, InstructionMode, InstructionSpec, VirtualType +from .specs import CollectionSpec, EmissionSpec, InstructionMode, VirtualType from .template_state import TemplateState @@ -35,8 +35,9 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): self.collection = collection self.emission = emission - self.measured_qubits = QubitPartition(1, []) + self.dynamic_qubits = set() self.entangled_qubits = set() + self.measured_qubits = QubitPartition(1, []) def _append_dressed_layer(self) -> ParamIndices: """Add a dressed layer.""" @@ -81,6 +82,11 @@ def _append_barrier(self, label: str, qubits=None): barrier = CircuitInstruction(Barrier(len(all_qubits), label), all_qubits) self.template_state.template.append(barrier) + def _validate_if_else(self, if_else: IfElseOp): + self.samplex_state.verify_no_twirled_clbits( + self.template_state.get_condition_clbits(if_else.condition) + ) + class LeftBoxBuilder(BoxBuilder): """Box builder for left dressings.""" @@ -92,13 +98,16 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): self.clbit_idxs = [] def parse(self, instr: CircuitInstruction): - name = instr.operation.name - param_idxs = np.empty((0, 0), dtype=np.intp) - if (name := instr.operation.name) == "barrier": self.template_state.append_remapped_gate(instr) return + if not self.dynamic_qubits.isdisjoint(instr.qubits): + raise RuntimeError( + "Cannot handle a dynamic instruction and another instruction on " + f"qubits {instr.qubits} in the same dressed box." + ) + if name.startswith("meas"): for qubit in instr.qubits: if (qubit,) not in self.measured_qubits: @@ -114,6 +123,8 @@ def parse(self, instr: CircuitInstruction): return if name.startswith("if_else"): + self.dynamic_qubits.update(instr.qubits) + self._validate_if_else(instr.operation) builder = BoxLeftIfElseBuilder( instr, self.samplex_state, self.collection.synth, self.template_state.param_iter ) @@ -133,20 +144,16 @@ def parse(self, instr: CircuitInstruction): "Cannot handle single-qubit gate to the right of entangler when dressing=left." ) # the action of this single-qubit gate will be absorbed into the dressing + mode = InstructionMode.MULTIPLY params = [] if instr.operation.is_parameterized(): params.extend((None, param) for param in instr.operation.params) - spec = InstructionSpec( - params=params, mode=InstructionMode.MULTIPLY, param_idxs=param_idxs - ) elif num_qubits > 1: self.entangled_qubits.update(instr.qubits) - spec = InstructionSpec( - params=self.template_state.append_remapped_gate(instr), - mode=InstructionMode.PROPAGATE, - param_idxs=param_idxs, - ) + mode = InstructionMode.PROPAGATE + params = self.template_state.append_remapped_gate(instr) + else: raise RuntimeError(f"Instruction {instr} could not be parsed.") @@ -156,7 +163,7 @@ def parse(self, instr: CircuitInstruction): f"Instruction {instr} happens after a measurement. No operations allowed " "after a measurement in a measurement twirling box." ) - self.samplex_state.add_propagate(instr, spec) + self.samplex_state.add_propagate(instr, mode, params) def lhs(self): self._append_barrier("L") @@ -197,9 +204,15 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): def parse(self, instr: CircuitInstruction): if (name := instr.operation.name).startswith("barrier"): - spec = InstructionSpec(params=self.template_state.append_remapped_gate(instr)) + self.template_state.append_remapped_gate(instr) return + if not self.dynamic_qubits.isdisjoint(instr.qubits): + raise RuntimeError( + "Cannot handle a dynamic instruction and another instruction on " + f"qubits {instr.qubits} in the same dressed box." + ) + if name.startswith("meas"): for qubit in instr.qubits: if (qubit,) not in self.measured_qubits: @@ -215,6 +228,8 @@ def parse(self, instr: CircuitInstruction): return if name.startswith("if_else"): + self.dynamic_qubits.update(instr.qubits) + self._validate_if_else(instr.operation) for qubit in instr.qubits: self.collection.if_else_qubits.add((qubit,)) builder = BoxRightIfElseBuilder( @@ -228,24 +243,22 @@ def parse(self, instr: CircuitInstruction): if (num_qubits := instr.operation.num_qubits) == 1: self.entangled_qubits.update(instr.qubits) # the action of this single-qubit gate will be absorbed into the dressing + mode = InstructionMode.MULTIPLY params = [] if instr.operation.is_parameterized(): params.extend((None, param) for param in instr.operation.params) - spec = InstructionSpec(mode=InstructionMode.MULTIPLY, params=params) elif num_qubits > 1: if not self.entangled_qubits.isdisjoint(instr.qubits): raise RuntimeError( "Cannot handle single-qubit gate to the left of entangler when dressing=right." ) - spec = InstructionSpec( - params=self.template_state.append_remapped_gate(instr), - mode=InstructionMode.PROPAGATE, - ) + mode = InstructionMode.PROPAGATE + params = self.template_state.append_remapped_gate(instr) else: raise RuntimeError(f"Instruction {instr} could not be parsed.") - self.samplex_state.add_propagate(instr, spec) + self.samplex_state.add_propagate(instr, mode, params) def lhs(self): self._append_barrier("L") diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 41bb9d48..9ca2b005 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -23,7 +23,7 @@ from ..pre_samplex.graph_data import Direction, PreCollect, PreCopy, PreEdge, PreEmit, PrePropagate from ..synths import Synth from .param_iter import ParamIter -from .specs import InstructionMode, InstructionSpec +from .specs import InstructionMode class BoxIfElseBuilder: @@ -61,7 +61,7 @@ def append_propagate(self, block: QuantumCircuit, new_block: QuantumCircuit): new_op = type(instr.operation)(*params) if params else instr.operation new_block.append(new_op, instr.qubits, instr.clbits) mode = InstructionMode.PROPAGATE - self.pre_samplex.add_propagate(instr, InstructionSpec(params=params, mode=mode)) + self.pre_samplex.add_propagate(instr, params=params, mode=mode) def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> ParamIndices: start = self.param_iter.idx @@ -118,9 +118,7 @@ def build(self): node_idx, ) - if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - - return if_else_op + return IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) class BoxLeftIfElseBuilder(BoxIfElseBuilder): @@ -166,6 +164,4 @@ def build(self): node_idx, ) - if_else_op = IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) - - return if_else_op + return IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) diff --git a/samplomatic/builders/passthrough_builder.py b/samplomatic/builders/passthrough_builder.py index 16481f89..3fc3d7f8 100644 --- a/samplomatic/builders/passthrough_builder.py +++ b/samplomatic/builders/passthrough_builder.py @@ -52,12 +52,9 @@ def parse(self, instr: CircuitInstruction): ) self.samplex_state.passthrough_params.extend(true_params + false_params) else: - spec = InstructionSpec( - params=self.template_state.append_remapped_gate(instr), - mode=InstructionMode.PROPAGATE, - ) - - self.samplex_state.add_propagate(instr, spec) + mode = InstructionMode.PROPAGATE + params = self.template_state.append_remapped_gate(instr) + self.samplex_state.add_propagate(instr, mode, params) def _append_barrier(self, label: str): if self.template_state.scope_idx: diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index 4b415a87..9bc2474d 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -18,9 +18,9 @@ import numpy as np from qiskit.circuit.gate import Gate -from ..aliases import ClbitIndex, OutputIndex, ParamIndices, StrRef, SubsystemIndex +from ..aliases import ClbitIndex, OutputIndex, ParamIndices, ParamSpec, StrRef, SubsystemIndex from ..annotations import VirtualType -from ..builders.specs import InstructionMode, InstructionSpec +from ..builders.specs import InstructionMode from ..constants import Direction from ..partition import QubitIndicesPartition, SubsystemIndicesPartition from ..synths import Synth @@ -184,8 +184,9 @@ class PrePropagate(PreNode): each other. """ - spec: InstructionSpec - """Specification for how the operation acts on virtual gates.""" + mode: InstructionMode + + params: ParamSpec def get_style(self): return ( diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 24ef81ca..250f7acc 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -45,7 +45,7 @@ StrRef, ) from ..annotations import VirtualType -from ..builders.specs import InstructionMode, InstructionSpec +from ..builders.specs import InstructionMode from ..constants import Direction from ..distributions import Distribution, HaarU2, UniformPauli from ..exceptions import SamplexBuildError @@ -715,7 +715,7 @@ def add_emit_prep_basis_transform(self, qubits: QubitPartition, basis_ref: StrRe node = PreBasisTransform(subsystems, Direction.RIGHT, VirtualType.U2, basis_ref) return self._add_right(node) - def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): + def add_propagate(self, instr: CircuitInstruction, mode: InstructionMode, params: ParamSpec): """Add a node that propagates virtual gates through an operation. This method deduces which direction to propagate virtual gates by inspecting the previous @@ -723,7 +723,8 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): Args: instr: The circuit instruction to propagate through. - spec: The specification for how to propagate with the instruction. + mode: What mode to use for propagation. + params: The parameters of the instruction. Raises: SamplexBuildError: If the qubits of ``instr`` have partial overlap with dangling qubits @@ -747,8 +748,8 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): # These parameters appear in the template (InstructionMode.PROPAGATE) and must # be accounted for during sampling, even though they might not take part in # virtual gate propagation. - if spec.mode == InstructionMode.PROPAGATE and spec.params is not None: - self.passthrough_params.extend(spec.params) + if mode == InstructionMode.PROPAGATE and params is not None: + self.passthrough_params.extend(params) # recall that this is indexing out of `subsystems`, not qubits num_qubits = instr.operation.num_qubits @@ -756,7 +757,7 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): # time ordering: (emit> | propagate>) --> new propagate> rightward_node_candidate = NodeCandidate( - self.graph, PrePropagate(subsystems, Direction.RIGHT, op, partition, spec) + self.graph, PrePropagate(subsystems, Direction.RIGHT, op, partition, mode, params) ) match = DanglerMatch(node_types=(PreCopy, PreEmit, PrePropagate), direction=Direction.RIGHT) all_found_qubit_idxs = set() @@ -775,7 +776,7 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): # time ordering: (collect< | propagate<) <-- new propagate< leftward_node_candidate = NodeCandidate( - self.graph, PrePropagate(subsystems, Direction.LEFT, op, partition, spec) + self.graph, PrePropagate(subsystems, Direction.LEFT, op, partition, mode, params) ) all_found_qubit_idxs = set() match = DanglerMatch( @@ -831,7 +832,7 @@ def merge_parallel_pre_propagate_nodes(self): ], ) - params = [param for node in nodes for param in node.spec.params] + params = [param for node in nodes for param in node.params] # This merges the node but not the edges new_node_idx = replace_nodes_with_one_node( @@ -842,7 +843,8 @@ def merge_parallel_pre_propagate_nodes(self): self.graph[node_idxs[0]].direction, # all nodes have same direction self.graph[node_idxs[0]].operation, # all nodes have same operation combined_partition, - InstructionSpec(params=params, mode=nodes[0].spec.mode), + nodes[0].mode, + params, ), ) @@ -862,9 +864,7 @@ def _cluster_pre_propagate_nodes(self, generation: list[NodeIndex]) -> list[list node = self.graph[node_idx] if isinstance(node, PrePropagate): cluster_type_key = PrePropagateKey( - mode=node.spec.mode, - operation_name=node.operation.name, - direction=node.direction, + mode=node.mode, operation_name=node.operation.name, direction=node.direction ) for cluster in clusters[cluster_type_key]: if not cluster["subsystems"].overlaps_with( @@ -982,8 +982,8 @@ def max_param_idx(self) -> int | None: for node in self.graph.nodes(): if isinstance(node, PreCollect) and node.param_idxs.size: max_value = max(max_value or 0, node.param_idxs.max()) - elif isinstance(node, PrePropagate) and node.spec.params: - max_value = max(max_value or 0, max(idx or 0 for idx, _ in node.spec.params)) + elif isinstance(node, PrePropagate) and node.params: + max_value = max(max_value or 0, max(idx or 0 for idx, _ in node.params)) return max_value def finalize(self): @@ -1389,7 +1389,7 @@ def add_propagate_node( combined_register_name = f"{prefix}_{order[pre_propagate_idx]}" op_name = pre_propagate.operation.name - mode = pre_propagate.spec.mode + mode = pre_propagate.mode incoming = set() for predecssor_idx in self.graph.predecessor_indices(pre_propagate_idx): incoming.add(samplex.graph[pre_nodes_to_nodes[predecssor_idx]].outgoing_register_type) @@ -1397,8 +1397,7 @@ def add_propagate_node( if pre_propagate.operation.is_parameterized(): combined_register_type = VirtualType.U2 param_idxs = [ - samplex.append_parameter_expression(param) - for _, param in pre_propagate.spec.params + samplex.append_parameter_expression(param) for _, param in pre_propagate.params ] if pre_propagate.direction is Direction.LEFT: propagate_node = RightU2ParametricMultiplicationNode( diff --git a/test/integration/test_dynamic_circuits.py b/test/integration/test_dynamic_circuits.py index 313b1dae..edccdbce 100644 --- a/test/integration/test_dynamic_circuits.py +++ b/test/integration/test_dynamic_circuits.py @@ -79,7 +79,7 @@ def test_right_dressed_twirled_conditional(self, save_plot): circuit.h(1) circuit.measure_all() - # sample_simulate_and_compare_counts(circuit, save_plot) + sample_simulate_and_compare_counts(circuit, save_plot) def test_right_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a right-dressed twirl box.""" @@ -97,7 +97,7 @@ def test_right_dressed_twirled_conditional_no_else(self, save_plot): circuit.h(1) circuit.measure_all() - # sample_simulate_and_compare_counts(circuit, save_plot) + sample_simulate_and_compare_counts(circuit, save_plot) def test_right_dressed_parametric_twirled_conditional(self, save_plot): """Test a circuit with a parametric conditional in a right-dressed twirl box.""" @@ -126,7 +126,7 @@ def test_right_dressed_parametric_twirled_conditional(self, save_plot): def test_left_dressed_twirled_conditional(self, save_plot): """Test a circuit with a conditional in a left-dressed twirl box.""" - circuit = QuantumCircuit(2, 2) + circuit = QuantumCircuit(3, 2) circuit.h(0) circuit.measure(0, 0) @@ -137,15 +137,15 @@ def test_left_dressed_twirled_conditional(self, save_plot): with _else: circuit.sx(0) circuit.cx(1, 0) - circuit.cx(0, 1) + circuit.x(2) with circuit.box([Twirl(dressing="right")]): - circuit.noop(0, 1) - - circuit.h(1) + circuit.cx(0, 1) + circuit.h(1) + circuit.noop(0, 2) - # circuit.measure_all() + circuit.measure_all() - # sample_simulate_and_compare_counts(circuit, save_plot) + sample_simulate_and_compare_counts(circuit, save_plot) def test_left_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a left-dressed twirl box.""" @@ -160,9 +160,9 @@ def test_left_dressed_twirled_conditional_no_else(self, save_plot): circuit.noop(0) circuit.h(1) - # circuit.measure_all() + circuit.measure_all() - # sample_simulate_and_compare_counts(circuit, save_plot) + sample_simulate_and_compare_counts(circuit, save_plot) def test_left_dressed_parametric_twirled_conditional(self, save_plot): """Test a circuit with a parametric conditional in a left-dressed twirl box.""" diff --git a/test/unit/test_builders/test_general_build_errors.py b/test/unit/test_builders/test_general_build_errors.py index cfcf9d81..ad16e809 100644 --- a/test/unit/test_builders/test_general_build_errors.py +++ b/test/unit/test_builders/test_general_build_errors.py @@ -18,6 +18,7 @@ import pytest from qiskit.circuit import QuantumCircuit +from qiskit.circuit.classical import expr from samplomatic import Twirl from samplomatic.builders import pre_build @@ -130,79 +131,79 @@ def test_twirled_clregister_in_passthrough_condition_error(self): def test_twirled_clbit_in_right_condition_error(self): """Test for error if a right-box conditional depends on a twirled classical bit.""" - # circuit = QuantumCircuit(2, 2) - # with circuit.box([Twirl(dressing="left")]): - # circuit.measure(0, 0) - # with circuit.box([Twirl(dressing="left")]): - # circuit.noop(0) - # with circuit.box([Twirl(dressing="right")]): - # with circuit.if_test((circuit.clbits[0], 1)): - # circuit.sx(0) + circuit = QuantumCircuit(2, 2) + with circuit.box([Twirl(dressing="left")]): + circuit.measure(0, 0) + with circuit.box([Twirl(dressing="left")]): + circuit.noop(0) + with circuit.box([Twirl(dressing="right")]): + with circuit.if_test((circuit.clbits[0], 1)): + circuit.sx(0) - # with pytest.raises( - # BuildError, match="Cannot use twirled classical bits in classical conditions" - # ): - # pre_build(circuit) + with pytest.raises( + BuildError, match="Cannot use twirled classical bits in classical conditions" + ): + pre_build(circuit) def test_twirled_clregister_in_right_condition_error(self): """Test for error if a right-box conditional depends on a twirled classical register.""" - # circuit = QuantumCircuit(2, 2) - # with circuit.box([Twirl(dressing="left")]): - # circuit.measure(0, 0) - # with circuit.box([Twirl(dressing="left")]): - # circuit.noop(0) - # with circuit.box([Twirl(dressing="right")]): - # with circuit.if_test((circuit.cregs[0], 1)): - # circuit.sx(0) + circuit = QuantumCircuit(2, 2) + with circuit.box([Twirl(dressing="left")]): + circuit.measure(0, 0) + with circuit.box([Twirl(dressing="left")]): + circuit.noop(0) + with circuit.box([Twirl(dressing="right")]): + with circuit.if_test((circuit.cregs[0], 1)): + circuit.sx(0) - # with pytest.raises( - # BuildError, match="Cannot use twirled classical bits in classical conditions" - # ): - # pre_build(circuit) + with pytest.raises( + BuildError, match="Cannot use twirled classical bits in classical conditions" + ): + pre_build(circuit) def test_twirled_clbit_in_left_condition_error(self): """Test for error if a left-box conditional depends on a twirled classical bit.""" - # circuit = QuantumCircuit(2, 2) - # with circuit.box([Twirl(dressing="left")]): - # circuit.measure(0, 0) - # with circuit.box([Twirl(dressing="left")]): - # with circuit.if_test((circuit.clbits[0], 1)): - # circuit.sx(0) + circuit = QuantumCircuit(2, 2) + with circuit.box([Twirl(dressing="left")]): + circuit.measure(0, 0) + with circuit.box([Twirl(dressing="left")]): + with circuit.if_test((circuit.clbits[0], 1)): + circuit.sx(0) - # with pytest.raises( - # BuildError, match="Cannot use twirled classical bits in classical conditions" - # ): - # pre_build(circuit) + with pytest.raises( + BuildError, match="Cannot use twirled classical bits in classical conditions" + ): + pre_build(circuit) def test_twirled_clregister_in_left_condition_error(self): """Test for error if a left-box conditional depends on a twirled classical register.""" - # circuit = QuantumCircuit(2, 2) - # with circuit.box([Twirl(dressing="left")]): - # circuit.measure(0, 0) - # with circuit.box([Twirl(dressing="left")]): - # with circuit.if_test((circuit.cregs[0], 1)): - # circuit.sx(0) + circuit = QuantumCircuit(2, 2) + with circuit.box([Twirl(dressing="left")]): + circuit.measure(0, 0) + with circuit.box([Twirl(dressing="left")]): + with circuit.if_test((circuit.cregs[0], 1)): + circuit.sx(0) - # with pytest.raises( - # BuildError, match="Cannot use twirled classical bits in classical conditions" - # ): - # pre_build(circuit) + with pytest.raises( + BuildError, match="Cannot use twirled classical bits in classical conditions" + ): + pre_build(circuit) def test_twirled_expr_in_left_condition_error(self): """Test for an error if a left-box conditional depends on a twirled classical expression.""" - # circuit = QuantumCircuit(2, 2) - # with circuit.box([Twirl(dressing="left")]): - # circuit.measure(0, 0) - # with circuit.box([Twirl(dressing="left")]): - # with circuit.if_test( - # expr.logic_and(expr.logic_not(circuit.clbits[0]), circuit.clbits[1]) - # ): - # circuit.sx(0) + circuit = QuantumCircuit(2, 2) + with circuit.box([Twirl(dressing="left")]): + circuit.measure(0, 0) + with circuit.box([Twirl(dressing="left")]): + with circuit.if_test( + expr.logic_and(expr.logic_not(circuit.clbits[0]), circuit.clbits[1]) + ): + circuit.sx(0) - # with pytest.raises( - # BuildError, match="Cannot use twirled classical bits in classical conditions" - # ): - # pre_build(circuit) + with pytest.raises( + BuildError, match="Cannot use twirled classical bits in classical conditions" + ): + pre_build(circuit) def test_repeated_twirled_clbit_error(self): """Verify that an error is raised if the same clbit is used more than once for twirling""" From b5dc5491ed747304fe935bff1ef6b93d2062732d Mon Sep 17 00:00:00 2001 From: joshuasn Date: Tue, 14 Oct 2025 20:06:47 -0230 Subject: [PATCH 16/22] Removing InstructionSpec. --- samplomatic/pre_samplex/utils.py | 2 +- test/integration/test_dynamic_circuits.py | 55 +++++++------- test/unit/test_builders/test_utils.py | 26 ++++--- .../unit/test_pre_samplex/test_pre_samplex.py | 74 ++++++++++--------- 4 files changed, 89 insertions(+), 68 deletions(-) diff --git a/samplomatic/pre_samplex/utils.py b/samplomatic/pre_samplex/utils.py index 0410d827..b341f8f8 100644 --- a/samplomatic/pre_samplex/utils.py +++ b/samplomatic/pre_samplex/utils.py @@ -41,7 +41,7 @@ def pre_propagate_nodes_are_mergeable( are_mergeable = ( isinstance(node_a, PrePropagate) and isinstance(node_b, PrePropagate) - and node_a.spec.mode is node_b.spec.mode + and node_a.mode is node_b.mode and node_a.direction == node_b.direction and node_a.operation.name == node_b.operation.name and not node_a.subsystems.overlaps_with(node_b.subsystems.all_elements) diff --git a/test/integration/test_dynamic_circuits.py b/test/integration/test_dynamic_circuits.py index edccdbce..9915ab86 100644 --- a/test/integration/test_dynamic_circuits.py +++ b/test/integration/test_dynamic_circuits.py @@ -138,8 +138,9 @@ def test_left_dressed_twirled_conditional(self, save_plot): circuit.sx(0) circuit.cx(1, 0) circuit.x(2) - with circuit.box([Twirl(dressing="right")]): + with circuit.box([Twirl()]): circuit.cx(0, 1) + with circuit.box([Twirl(dressing="right")]): circuit.h(1) circuit.noop(0, 2) @@ -149,13 +150,16 @@ def test_left_dressed_twirled_conditional(self, save_plot): def test_left_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a left-dressed twirl box.""" - circuit = QuantumCircuit(2, 2) + circuit = QuantumCircuit(3, 2) circuit.h(0) circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): with circuit.if_test((circuit.clbits[0], 1)): + circuit.x(0) circuit.cx(0, 1) + with circuit.box([Twirl()]): + circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): circuit.noop(0) circuit.h(1) @@ -166,26 +170,27 @@ def test_left_dressed_twirled_conditional_no_else(self, save_plot): def test_left_dressed_parametric_twirled_conditional(self, save_plot): """Test a circuit with a parametric conditional in a left-dressed twirl box.""" - # circuit = QuantumCircuit(3, 1) - # p = Parameter("p") - # circuit.h(0) - # circuit.measure(0, 0) - - # with circuit.box([Twirl(dressing="left")]): - # with circuit.if_test((circuit.clbits[0], 1)) as _else: - # circuit.x(0) - # circuit.rx(p, 1) - # circuit.cx(0, 1) - # with _else: - # circuit.sx(0) - # circuit.rx(2 * p, 0) - # circuit.cx(1, 0) - # circuit.x(2) - # circuit.cx(0, 1) - # with circuit.box([Twirl(dressing="right")]): - # circuit.h(1) - # circuit.noop(0, 2) - - # circuit.measure_all() - - # sample_simulate_and_compare_counts(circuit, save_plot) + circuit = QuantumCircuit(3, 1) + p = Parameter("p") + circuit.h(0) + circuit.measure(0, 0) + + with circuit.box([Twirl(dressing="left")]): + with circuit.if_test((circuit.clbits[0], 1)) as _else: + circuit.x(0) + circuit.rx(p, 1) + circuit.cx(0, 1) + with _else: + circuit.sx(0) + circuit.rx(2 * p, 0) + circuit.cx(1, 0) + circuit.x(2) + with circuit.box([Twirl()]): + circuit.cx(0, 1) + with circuit.box([Twirl(dressing="right")]): + circuit.h(1) + circuit.noop(0, 2) + + circuit.measure_all() + + sample_simulate_and_compare_counts(circuit, save_plot) diff --git a/test/unit/test_builders/test_utils.py b/test/unit/test_builders/test_utils.py index b03c0583..3474fad5 100644 --- a/test/unit/test_builders/test_utils.py +++ b/test/unit/test_builders/test_utils.py @@ -15,7 +15,7 @@ from qiskit.circuit.library import HGate, SXGate from rustworkx import PyDiGraph -from samplomatic.builders.specs import InstructionSpec +from samplomatic.builders.specs import InstructionMode from samplomatic.constants import Direction from samplomatic.exceptions import SamplexConstructionError from samplomatic.partition import QubitIndicesPartition, SubsystemIndicesPartition @@ -39,7 +39,8 @@ def test_two_connected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -48,7 +49,8 @@ def test_two_connected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) @@ -76,7 +78,8 @@ def test_two_disconnected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -88,7 +91,8 @@ def test_two_disconnected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) @@ -109,7 +113,8 @@ def test_two_connected_pre_propagates_with_different_directions(self): Direction.RIGHT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -118,7 +123,8 @@ def test_two_connected_pre_propagates_with_different_directions(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) @@ -139,7 +145,8 @@ def test_two_connected_pre_propagates_with_different_operations(self): Direction.LEFT, SXGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -148,7 +155,8 @@ def test_two_connected_pre_propagates_with_different_operations(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) diff --git a/test/unit/test_pre_samplex/test_pre_samplex.py b/test/unit/test_pre_samplex/test_pre_samplex.py index 3c99b80d..5b9007d9 100644 --- a/test/unit/test_pre_samplex/test_pre_samplex.py +++ b/test/unit/test_pre_samplex/test_pre_samplex.py @@ -19,7 +19,7 @@ from rustworkx import topological_sort from samplomatic.annotations import VirtualType -from samplomatic.builders.specs import InstructionMode, InstructionSpec +from samplomatic.builders.specs import InstructionMode from samplomatic.constants import Direction from samplomatic.exceptions import SamplexBuildError from samplomatic.optionals import HAS_PLOTLY @@ -143,14 +143,14 @@ def test_propagate_preceded_by_collect(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) subsys_idxs = QubitIndicesPartition.from_elements([0, 1]) assert pre_samplex.graph.nodes()[0] == PreCollect( subsys_idxs, Direction.BOTH, RzSxSynth(), [] ) assert pre_samplex.graph.nodes()[1] == PrePropagate( - subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], {} + subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], InstructionMode.NONE, [] ) assert len(pre_samplex.graph.edges()) == 1 @@ -163,7 +163,7 @@ def test_propagate_preceded_by_emit(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) subsys_idxs = QubitIndicesPartition.from_elements([0, 1]) assert pre_samplex.graph.nodes()[0] == PreCollect( @@ -171,10 +171,10 @@ def test_propagate_preceded_by_emit(self): ) assert pre_samplex.graph.nodes()[1] == PreEmit(subsys_idxs, Direction.BOTH, PauliRegister) assert pre_samplex.graph.nodes()[2] == PrePropagate( - subsys_idxs, Direction.RIGHT, CXGate(), [[0, 1]], {} + subsys_idxs, Direction.RIGHT, CXGate(), [[0, 1]], InstructionMode.NONE, [] ) assert pre_samplex.graph.nodes()[3] == PrePropagate( - subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], {} + subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], InstructionMode.NONE, [] ) assert len(pre_samplex.graph.edges()) == 3 @@ -187,12 +187,12 @@ def test_propagate_errors_when_not_all_qubits_are_dangling(self): pre_samplex.add_collect(QubitPartition.from_elements(qreg[:1]), RzSxSynth(), []) pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg[:1]), PauliRegister) with pytest.raises(SamplexBuildError, match="overlaps partially with .* left-to-right"): - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) pre_samplex = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}) pre_samplex.add_collect(QubitPartition.from_elements(qreg[:1]), RzSxSynth(), []) with pytest.raises(SamplexBuildError, match="overlaps partially with .* collectors.* left"): - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) def test_error_right_propagate_through_measurement(self): """Test that propagation through measurement to the right raises an error""" @@ -202,7 +202,7 @@ def test_error_right_propagate_through_measurement(self): pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) with pytest.raises(SamplexBuildError, match="Cannot propagate through measure instruction"): - pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionMode.NONE, []) def test_error_left_propagate_through_measurement(self): """Test that propagation through measurement to the left raises an error""" @@ -210,7 +210,7 @@ def test_error_left_propagate_through_measurement(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0}) pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) - pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionMode.NONE, []) with pytest.raises(SamplexBuildError, match="Found an emission without a collector"): pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) @@ -220,7 +220,7 @@ def test_add_propagate_measurement(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0}) pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) - pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionMode.NONE, []) pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) @@ -235,15 +235,25 @@ def test_add_z2_collect(self): state = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}, cregs=[creg]) state.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) state.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) - state.add_propagate(CircuitInstruction(XGate(), [qreg[0]]), InstructionSpec()) + state.add_propagate(CircuitInstruction(XGate(), [qreg[0]]), InstructionMode.NONE, []) state.add_z2_collect(QubitPartition.from_elements(qreg), [0, 1]) subsys_idxs = QubitIndicesPartition.from_elements([0, 1]) assert state.graph.nodes()[-3] == PrePropagate( - QubitIndicesPartition.from_elements([0]), Direction.RIGHT, XGate(), [[0]], {} + QubitIndicesPartition.from_elements([0]), + Direction.RIGHT, + XGate(), + [[0]], + InstructionMode.NONE, + [], ) assert state.graph.nodes()[-2] == PrePropagate( - QubitIndicesPartition.from_elements([0]), Direction.LEFT, XGate(), [[0]], {} + QubitIndicesPartition.from_elements([0]), + Direction.LEFT, + XGate(), + [[0]], + InstructionMode.NONE, + [], ) assert state.graph.nodes()[-1] == PreZ2Collect( subsys_idxs, {creg.name: [0, 1]}, {creg.name: [0, 1]} @@ -334,7 +344,7 @@ def test_finalize_prunes_unreachable_nodes(self): pre_samplex = PreSamplex(qubit_map={qubit: idx for idx, qubit in enumerate(qreg)}) pre_samplex.add_collect(subsystems, RzSxSynth(), np.array([[0, 1, 2], [3, 4, 5]])) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) pre_samplex.finalize() assert not pre_samplex.graph.nodes() @@ -342,7 +352,7 @@ def test_finalize_prunes_unreachable_nodes(self): pre_samplex.add_collect(subsystems, RzSxSynth(), np.array([[0, 1, 2], [3, 4, 5]])) pre_samplex.add_emit_twirl(subsystems, VirtualType.PAULI) pre_samplex.add_collect(subsystems, RzSxSynth(), np.array([[0, 1, 2], [3, 4, 5]])) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) assert len(pre_samplex.graph) == 4 assert len(pre_samplex.graph.edges()) == 3 @@ -366,7 +376,7 @@ def test_nodes_clustered(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3, 4, 5]) # noqa: SLF001 @@ -381,8 +391,8 @@ def test_nodes_different_modes(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) - pre_samplex.add_propagate(circ[0], InstructionSpec(mode=InstructionMode.MULTIPLY)) - pre_samplex.add_propagate(circ[1], InstructionSpec(mode=InstructionMode.PROPAGATE)) + pre_samplex.add_propagate(circ[0], InstructionMode.MULTIPLY, []) + pre_samplex.add_propagate(circ[1], InstructionMode.PROPAGATE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3]) # noqa: SLF001 @@ -401,7 +411,7 @@ def test_nodes_different_predecessors(self): QubitPartition(1, ((circ.qubits[2],), (circ.qubits[3],))), RzSxSynth(), [] ) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl( QubitPartition(1, ((circ.qubits[0],), (circ.qubits[1],))), PauliRegister ) @@ -422,7 +432,7 @@ def test_nodes_different_operation(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3]) # noqa: SLF001 @@ -438,7 +448,7 @@ def test_nodes_overlaping_qubits(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3]) # noqa: SLF001 @@ -458,10 +468,10 @@ def test_pre_samplex_with_mergeable_pre_propagates(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_collect(subsystems, RzSxSynth(), []) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -491,10 +501,10 @@ def test_pre_samplex_with_no_mergeable_pre_propagates(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_collect(subsystems, RzSxSynth(), []) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -538,7 +548,7 @@ def test_pre_samplex_with_mergeable_pre_propagates_in_series(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box1.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box1: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -575,7 +585,7 @@ def test_partitions_are_rescaled_correctly(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -598,9 +608,7 @@ def test_unsupported_propagate_error(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circuit.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), np.arange(6).reshape(2, 3)) - pre_samplex.add_propagate( - next(iter(circuit)), InstructionSpec(mode=InstructionMode.MULTIPLY) - ) + pre_samplex.add_propagate(next(iter(circuit)), InstructionMode.MULTIPLY, []) pre_samplex.add_emit_twirl(subsystems, VirtualType.PAULI) pre_samplex.add_collect(subsystems, RzSxSynth(), np.arange(6).reshape(2, 3)) @@ -625,10 +633,10 @@ def test_draw(self, save_plot): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, VirtualType.PAULI) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_collect(subsystems, RzSxSynth(), []) save_plot(pre_samplex.draw()) From 5a21cd242e5049c2f777029167ef997450d3d1fa Mon Sep 17 00:00:00 2001 From: joshuasn Date: Tue, 14 Oct 2025 20:34:19 -0230 Subject: [PATCH 17/22] Removing support for measurements in right twirled boxes. This does work conceptually but as it stands, measurements are not propagators. Instead, they terminate boxes. Will need to revisit. --- samplomatic/builders/box_builder.py | 58 ++++----------- samplomatic/builders/get_builder.py | 9 +-- samplomatic/builders/passthrough_builder.py | 17 ++--- samplomatic/builders/specs.py | 36 +-------- samplomatic/pre_samplex/graph_data.py | 11 ++- samplomatic/pre_samplex/pre_samplex.py | 31 ++++---- samplomatic/pre_samplex/utils.py | 2 +- test/integration/test_measurement_twirling.py | 18 ----- .../test_builders_pre_samplex.py | 4 +- test/unit/test_builders/test_utils.py | 26 ++++--- .../unit/test_pre_samplex/test_pre_samplex.py | 74 ++++++++++--------- 11 files changed, 107 insertions(+), 179 deletions(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index 08944fe5..938f0655 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -18,11 +18,11 @@ from qiskit.circuit import Barrier from ..aliases import CircuitInstruction, ParamIndices -from ..exceptions import SamplexBuildError, TemplateBuildError +from ..exceptions import BuildError, SamplexBuildError, TemplateBuildError from ..partition import QubitPartition from ..pre_samplex import PreSamplex from .builder import Builder -from .specs import CollectionSpec, EmissionSpec, InstructionMode, InstructionSpec, VirtualType +from .specs import CollectionSpec, EmissionSpec, InstructionMode, VirtualType from .template_state import TemplateState @@ -85,9 +85,6 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): self.clbit_idxs = [] def parse(self, instr: CircuitInstruction): - name = instr.operation.name - param_idxs = np.empty((0, 0), dtype=np.intp) - if (name := instr.operation.name) == "barrier": self.template_state.append_remapped_gate(instr) return @@ -97,9 +94,7 @@ def parse(self, instr: CircuitInstruction): if (qubit,) not in self.measured_qubits: self.measured_qubits.add((qubit,)) else: - raise SamplexBuildError( - "Cannot measure the same qubit twice in a twirling box." - ) + raise SamplexBuildError("Cannot measure the same qubit twice in a dressed box.") self.template_state.append_remapped_gate(instr) self.clbit_idxs.extend( [self.template_state.template.find_bit(clbit)[0] for clbit in instr.clbits] @@ -117,20 +112,15 @@ def parse(self, instr: CircuitInstruction): "Cannot handle single-qubit gate to the right of entangler when dressing=left." ) # the action of this single-qubit gate will be absorbed into the dressing + mode = InstructionMode.MULTIPLY params = [] if instr.operation.is_parameterized(): params.extend((None, param) for param in instr.operation.params) - spec = InstructionSpec( - params=params, mode=InstructionMode.MULTIPLY, param_idxs=param_idxs - ) elif num_qubits > 1: self.entangled_qubits.update(instr.qubits) - spec = InstructionSpec( - params=self.template_state.append_remapped_gate(instr), - mode=InstructionMode.PROPAGATE, - param_idxs=param_idxs, - ) + params = self.template_state.append_remapped_gate(instr) + mode = InstructionMode.PROPAGATE else: raise RuntimeError(f"Instruction {instr} could not be parsed.") @@ -138,9 +128,9 @@ def parse(self, instr: CircuitInstruction): # TODO: What about delays? barriers? raise SamplexBuildError( f"Instruction {instr} happens after a measurement. No operations allowed " - "after a measurement in a measurement twirling box." + "after a measurement in a dressed box." ) - self.samplex_state.add_propagate(instr, spec) + self.samplex_state.add_propagate(instr, mode, params) def lhs(self): self._append_barrier("L") @@ -180,22 +170,11 @@ def __init__(self, collection: CollectionSpec, emission: EmissionSpec): def parse(self, instr: CircuitInstruction): if (name := instr.operation.name).startswith("barrier"): - spec = InstructionSpec(params=self.template_state.append_remapped_gate(instr)) + params = self.template_state.append_remapped_gate(instr) return if name.startswith("meas"): - for qubit in instr.qubits: - if (qubit,) not in self.measured_qubits: - self.measured_qubits.add((qubit,)) - else: - raise SamplexBuildError( - "Cannot measure the same qubit twice in a twirling box." - ) - self.template_state.append_remapped_gate(instr) - self.clbit_idxs.extend( - [self.template_state.template.find_bit(clbit)[0] for clbit in instr.clbits] - ) - return + raise BuildError("Cannot measure the same qubit twice in a dressed box.") elif (num_qubits := instr.operation.num_qubits) == 1: self.entangled_qubits.update(instr.qubits) @@ -203,21 +182,19 @@ def parse(self, instr: CircuitInstruction): params = [] if instr.operation.is_parameterized(): params.extend((None, param) for param in instr.operation.params) - spec = InstructionSpec(mode=InstructionMode.MULTIPLY, params=params) + mode = InstructionMode.MULTIPLY elif num_qubits > 1: if not self.entangled_qubits.isdisjoint(instr.qubits): raise RuntimeError( "Cannot handle single-qubit gate to the left of entangler when dressing=right." ) - spec = InstructionSpec( - params=self.template_state.append_remapped_gate(instr), - mode=InstructionMode.PROPAGATE, - ) + params = self.template_state.append_remapped_gate(instr) + mode = InstructionMode.PROPAGATE else: raise RuntimeError(f"Instruction {instr} could not be parsed.") - self.samplex_state.add_propagate(instr, spec) + self.samplex_state.add_propagate(instr, mode, params) def lhs(self): self._append_barrier("L") @@ -238,12 +215,5 @@ def lhs(self): def rhs(self): self._append_barrier("M") param_idxs = self._append_dressed_layer() - if twirl_type := self.emission.twirl_register_type: - if len(self.measured_qubits) != 0: - if twirl_type != VirtualType.PAULI: - raise SamplexBuildError( - f"Cannot use {twirl_type.value} twirl in a box with measurements." - ) - self.samplex_state.add_z2_collect(self.measured_qubits, self.clbit_idxs) self.samplex_state.add_collect(self.collection.qubits, self.collection.synth, param_idxs) self._append_barrier("R") diff --git a/samplomatic/builders/get_builder.py b/samplomatic/builders/get_builder.py index 90cbadf5..b9f4c742 100644 --- a/samplomatic/builders/get_builder.py +++ b/samplomatic/builders/get_builder.py @@ -18,7 +18,7 @@ from qiskit.circuit import Annotation, Qubit -from ..aliases import CircuitInstruction, TypeAlias +from ..aliases import CircuitInstruction from ..annotations import ( BasisTransform, BasisTransformMode, @@ -29,16 +29,11 @@ ) from ..exceptions import BuildError from ..partition import QubitPartition -from ..pre_samplex import PreSamplex from ..synths import get_synth from .box_builder import LeftBoxBuilder, RightBoxBuilder from .builder import Builder from .passthrough_builder import PassthroughBuilder -from .specs import CollectionSpec, EmissionSpec, InstructionSpec -from .template_state import TemplateState - -SamplexBuilder: TypeAlias = Builder[PreSamplex, None] -TemplateBuilder: TypeAlias = Builder[TemplateState, InstructionSpec] +from .specs import CollectionSpec, EmissionSpec def get_builder(instr: CircuitInstruction | None, qubits: Sequence[Qubit]) -> Builder: diff --git a/samplomatic/builders/passthrough_builder.py b/samplomatic/builders/passthrough_builder.py index 16481f89..974bd54b 100644 --- a/samplomatic/builders/passthrough_builder.py +++ b/samplomatic/builders/passthrough_builder.py @@ -17,7 +17,7 @@ from ..aliases import CircuitInstruction from ..pre_samplex import PreSamplex from .builder import Builder -from .specs import InstructionMode, InstructionSpec +from .specs import InstructionMode from .template_state import TemplateState @@ -52,12 +52,9 @@ def parse(self, instr: CircuitInstruction): ) self.samplex_state.passthrough_params.extend(true_params + false_params) else: - spec = InstructionSpec( - params=self.template_state.append_remapped_gate(instr), - mode=InstructionMode.PROPAGATE, - ) - - self.samplex_state.add_propagate(instr, spec) + mode = InstructionMode.PROPAGATE + params = self.template_state.append_remapped_gate(instr) + self.samplex_state.add_propagate(instr, mode, params) def _append_barrier(self, label: str): if self.template_state.scope_idx: @@ -66,10 +63,8 @@ def _append_barrier(self, label: str): barrier = CircuitInstruction(Barrier(len(all_qubits), label), all_qubits) self.template_state.template.append(barrier) - def lhs(self) -> InstructionSpec: + def lhs(self): self._append_barrier("L") - return InstructionSpec() - def rhs(self) -> InstructionSpec: + def rhs(self): self._append_barrier("R") - return InstructionSpec() diff --git a/samplomatic/builders/specs.py b/samplomatic/builders/specs.py index e3ba57e4..79b3b20f 100644 --- a/samplomatic/builders/specs.py +++ b/samplomatic/builders/specs.py @@ -15,16 +15,13 @@ from __future__ import annotations import enum -from dataclasses import dataclass, field +from dataclasses import dataclass import numpy as np from ..aliases import ( CircuitInstruction, - ClbitIndex, Parameter, - ParamIndices, - ParamSpec, Qubit, StrRef, ) @@ -96,34 +93,3 @@ class CollectionSpec: synth: Synth[Qubit, Parameter, CircuitInstruction] | None = None """How to synthesize collection gates.""" - - -@dataclass -class InstructionSpec: - """Specification of an instruction.""" - - params: ParamSpec = field(default_factory=list) - """A list of tuples of parameter indices in the template circuit and corresponding expressions. - - An index of ``None`` indicates that the expression is not added to the template.""" - - param_idxs: ParamIndices = field(default_factory=lambda: EMPTY_IDXS) - """A matrix of parameter indices specifing virtual gate synthesis locations in the template. - - The first axis is over subsystems, the second over parameters in a synthesizer decomposition. - """ - - mode: InstructionMode = InstructionMode.NONE - """The mode of an added instruction.""" - - clbit_idxs: list[ClbitIndex] = field(default_factory=list) - """The mode of an added instruction.""" - - if_else: tuple[list[InstructionSpec], ParamIndices, list[InstructionSpec], ParamIndices] = ( - field(default_factory=lambda: ([], EMPTY_IDXS, [], EMPTY_IDXS)) - ) - """The specs for an `IfElseOp`. - - Each branch of the operation is represented by two fields: a list of `InstructionSpec` for - the branch's instructions, followed by `ParamIndices` for the collectors of the branch. The - true branch is first in the tuple, and the else branch is second.""" diff --git a/samplomatic/pre_samplex/graph_data.py b/samplomatic/pre_samplex/graph_data.py index 8791107f..ab287c06 100644 --- a/samplomatic/pre_samplex/graph_data.py +++ b/samplomatic/pre_samplex/graph_data.py @@ -18,9 +18,9 @@ import numpy as np from qiskit.circuit.gate import Gate -from ..aliases import ClbitIndex, OutputIndex, ParamIndices, StrRef, SubsystemIndex +from ..aliases import ClbitIndex, OutputIndex, ParamIndices, ParamSpec, StrRef, SubsystemIndex from ..annotations import VirtualType -from ..builders.specs import InstructionMode, InstructionSpec +from ..builders.specs import InstructionMode from ..constants import Direction from ..partition import QubitIndicesPartition, SubsystemIndicesPartition from ..synths import Synth @@ -183,8 +183,11 @@ class PrePropagate(PreNode): each other. """ - spec: InstructionSpec - """Specification for how the operation acts on virtual gates.""" + mode: InstructionMode + """How the operation acts on virtual gates.""" + + params: ParamSpec + """The parameters required by the node.""" def get_style(self): return ( diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 67b0013f..96f292c6 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -45,7 +45,7 @@ StrRef, ) from ..annotations import VirtualType -from ..builders.specs import InstructionMode, InstructionSpec +from ..builders.specs import InstructionMode from ..constants import Direction from ..distributions import Distribution, HaarU2, UniformPauli from ..exceptions import SamplexBuildError @@ -733,7 +733,7 @@ def add_emit_prep_basis_transform(self, qubits: QubitPartition, basis_ref: StrRe node = PreBasisTransform(subsystems, Direction.RIGHT, VirtualType.U2, basis_ref) return self._add_emit_right(node) - def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): + def add_propagate(self, instr: CircuitInstruction, mode: InstructionMode, params: ParamSpec): """Add a node that propagates virtual gates through an operation. This method deduces which direction to propagate virtual gates by inspecting the previous @@ -741,7 +741,8 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): Args: instr: The circuit instruction to propagate through. - spec: The specification for how to propagate with the instruction. + mode: What mode to use for propagation. + params: The parameters of the instruction. Raises: SamplexBuildError: If the qubits of ``instr`` have partial overlap with dangling qubits @@ -765,8 +766,8 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): # These parameters appear in the template (InstructionMode.PROPAGATE) and must # be accounted for during sampling, even though they might not take part in # virtual gate propagation. - if spec.mode == InstructionMode.PROPAGATE and spec.params is not None: - self.passthrough_params.extend(spec.params) + if mode == InstructionMode.PROPAGATE and params is not None: + self.passthrough_params.extend(params) # recall that this is indexing out of `subsystems`, not qubits num_qubits = instr.operation.num_qubits @@ -774,7 +775,7 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): # time ordering: (emit> | propagate>) --> new propagate> rightward_node_candidate = NodeCandidate( - self.graph, PrePropagate(subsystems, Direction.RIGHT, op, partition, spec) + self.graph, PrePropagate(subsystems, Direction.RIGHT, op, partition, mode, params) ) match = DanglerMatch(node_types=(PreEmit, PrePropagate), direction=Direction.RIGHT) all_found_qubit_idxs = set() @@ -795,7 +796,7 @@ def add_propagate(self, instr: CircuitInstruction, spec: InstructionSpec): # time ordering: (collect< | propagate<) <-- new propagate< leftward_node_candidate = NodeCandidate( - self.graph, PrePropagate(subsystems, Direction.LEFT, op, partition, spec) + self.graph, PrePropagate(subsystems, Direction.LEFT, op, partition, mode, params) ) all_found_qubit_idxs = set() match = DanglerMatch(node_types=(PreCollect, PrePropagate), direction=Direction.LEFT) @@ -851,7 +852,7 @@ def merge_parallel_pre_propagate_nodes(self): ], ) - params = [param for node in nodes for param in node.spec.params] + params = [param for node in nodes for param in node.params] # This merges the node but not the edges new_node_idx = replace_nodes_with_one_node( @@ -862,7 +863,8 @@ def merge_parallel_pre_propagate_nodes(self): self.graph[node_idxs[0]].direction, # all nodes have same direction self.graph[node_idxs[0]].operation, # all nodes have same operation combined_partition, - InstructionSpec(params=params, mode=nodes[0].spec.mode), + nodes[0].mode, + params, ), ) @@ -882,7 +884,7 @@ def _cluster_pre_propagate_nodes(self, generation: list[NodeIndex]) -> list[list node = self.graph[node_idx] if isinstance(node, PrePropagate): cluster_type_key = PrePropagateKey( - mode=node.spec.mode, + mode=node.mode, operation_name=node.operation.name, direction=node.direction, ) @@ -1002,8 +1004,8 @@ def max_param_idx(self) -> int | None: for node in self.graph.nodes(): if isinstance(node, PreCollect) and node.param_idxs.size: max_value = max(max_value or 0, node.param_idxs.max()) - elif isinstance(node, PrePropagate) and node.spec.params: - max_value = max(max_value or 0, max(idx or 0 for idx, _ in node.spec.params)) + elif isinstance(node, PrePropagate) and node.params: + max_value = max(max_value or 0, max(idx or 0 for idx, _ in node.params)) return max_value def finalize(self): @@ -1349,7 +1351,7 @@ def add_propagate_node( combined_register_name = f"{prefix}_{order[pre_propagate_idx]}" op_name = pre_propagate.operation.name - mode = pre_propagate.spec.mode + mode = pre_propagate.mode incoming = set() for predecssor_idx in self.graph.predecessor_indices(pre_propagate_idx): incoming.add(samplex.graph[pre_nodes_to_nodes[predecssor_idx]].outgoing_register_type) @@ -1357,8 +1359,7 @@ def add_propagate_node( if pre_propagate.operation.is_parameterized(): combined_register_type = VirtualType.U2 param_idxs = [ - samplex.append_parameter_expression(param) - for _, param in pre_propagate.spec.params + samplex.append_parameter_expression(param) for _, param in pre_propagate.params ] if pre_propagate.direction is Direction.LEFT: propagate_node = RightU2ParametricMultiplicationNode( diff --git a/samplomatic/pre_samplex/utils.py b/samplomatic/pre_samplex/utils.py index 6c45b2be..56ab7d3b 100644 --- a/samplomatic/pre_samplex/utils.py +++ b/samplomatic/pre_samplex/utils.py @@ -41,7 +41,7 @@ def pre_propagate_nodes_are_mergeable( are_mergeable = ( isinstance(node_a, PrePropagate) and isinstance(node_b, PrePropagate) - and node_a.spec.mode is node_b.spec.mode + and node_a.mode is node_b.mode and node_a.direction == node_b.direction and node_a.operation.name == node_b.operation.name and not node_a.subsystems.overlaps_with(node_b.subsystems.all_elements) diff --git a/test/integration/test_measurement_twirling.py b/test/integration/test_measurement_twirling.py index e60d0969..ef33d5b7 100644 --- a/test/integration/test_measurement_twirling.py +++ b/test/integration/test_measurement_twirling.py @@ -54,14 +54,6 @@ def test_measure_all(self, save_plot): circuit.measure_all() sample_simulate_and_compare_counts(circuit, save_plot) - def test_measure_all_right(self, save_plot): - circuit = QuantumCircuit(3) - with circuit.box([Twirl()]): - circuit.noop(*circuit.qubits) - with circuit.box([Twirl(dressing="right")]): - circuit.measure_all() - sample_simulate_and_compare_counts(circuit, save_plot) - def test_gates_and_measure_all(self, save_plot): circuit = QuantumCircuit(3) with circuit.box([Twirl(dressing="left")]): @@ -83,16 +75,6 @@ def test_separate_measures(self, save_plot): sample_simulate_and_compare_counts(circuit, save_plot) - def test_measure_with_different_dressings(self, save_plot): - circuit = QuantumCircuit(QuantumRegister(size=2), ClassicalRegister(name="meas", size=2)) - with circuit.box([Twirl(dressing="left")]): - circuit.measure(0, 1) - circuit.x(1) - with circuit.box([Twirl(dressing="right")]): - circuit.measure(1, 0) - - sample_simulate_and_compare_counts(circuit, save_plot) - @pytest.mark.skip(reason="QiskitAer bug #2367") def test_separate_measure_boxes(self, save_plot): """Test separate measurement boxes, with non-standard cbit associations""" diff --git a/test/unit/test_builders/test_builders_pre_samplex.py b/test/unit/test_builders/test_builders_pre_samplex.py index 42b7c472..4cfdbfaf 100644 --- a/test/unit/test_builders/test_builders_pre_samplex.py +++ b/test/unit/test_builders/test_builders_pre_samplex.py @@ -33,7 +33,7 @@ class TestBoxBuilder: """Test Box Builders""" def get_builder(self, qreg, creg=None): - """Helper function to return left box builder with empty PreSamplex.""" + """Return left box builder with empty PreSamplex.""" creg = ClassicalRegister(len(qreg)) if creg is None else creg qubit_map = {q: idx for idx, q in enumerate(qreg)} template_state = TemplateState(QuantumCircuit(qreg, creg), qubit_map, ParamIter(), [0]) @@ -125,6 +125,6 @@ def test_two_measurements_on_the_same_qubit_error(self): builder.parse(CircuitInstruction(Measure(), qreg, creg)) with pytest.raises( - SamplexBuildError, match="Cannot measure the same qubit twice in a twirling box" + SamplexBuildError, match="Cannot measure the same qubit twice in a dressed box" ): builder.parse(CircuitInstruction(Measure(), qreg, creg)) diff --git a/test/unit/test_builders/test_utils.py b/test/unit/test_builders/test_utils.py index b03c0583..3474fad5 100644 --- a/test/unit/test_builders/test_utils.py +++ b/test/unit/test_builders/test_utils.py @@ -15,7 +15,7 @@ from qiskit.circuit.library import HGate, SXGate from rustworkx import PyDiGraph -from samplomatic.builders.specs import InstructionSpec +from samplomatic.builders.specs import InstructionMode from samplomatic.constants import Direction from samplomatic.exceptions import SamplexConstructionError from samplomatic.partition import QubitIndicesPartition, SubsystemIndicesPartition @@ -39,7 +39,8 @@ def test_two_connected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -48,7 +49,8 @@ def test_two_connected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) @@ -76,7 +78,8 @@ def test_two_disconnected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -88,7 +91,8 @@ def test_two_disconnected_pre_propagates(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) @@ -109,7 +113,8 @@ def test_two_connected_pre_propagates_with_different_directions(self): Direction.RIGHT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -118,7 +123,8 @@ def test_two_connected_pre_propagates_with_different_directions(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) @@ -139,7 +145,8 @@ def test_two_connected_pre_propagates_with_different_operations(self): Direction.LEFT, SXGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) n2 = graph.add_node( @@ -148,7 +155,8 @@ def test_two_connected_pre_propagates_with_different_operations(self): Direction.LEFT, HGate(), SubsystemIndicesPartition(1, [(0,)]), - InstructionSpec(), + InstructionMode.NONE, + [], ) ) diff --git a/test/unit/test_pre_samplex/test_pre_samplex.py b/test/unit/test_pre_samplex/test_pre_samplex.py index c340c643..7f6903c5 100644 --- a/test/unit/test_pre_samplex/test_pre_samplex.py +++ b/test/unit/test_pre_samplex/test_pre_samplex.py @@ -19,7 +19,7 @@ from rustworkx import topological_sort from samplomatic.annotations import VirtualType -from samplomatic.builders.specs import InstructionMode, InstructionSpec +from samplomatic.builders.specs import InstructionMode from samplomatic.constants import Direction from samplomatic.exceptions import SamplexBuildError from samplomatic.optionals import HAS_PLOTLY @@ -143,14 +143,14 @@ def test_propagate_preceded_by_collect(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) subsys_idxs = QubitIndicesPartition.from_elements([0, 1]) assert pre_samplex.graph.nodes()[0] == PreCollect( subsys_idxs, Direction.BOTH, RzSxSynth(), [] ) assert pre_samplex.graph.nodes()[1] == PrePropagate( - subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], {} + subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], InstructionMode.NONE, [] ) assert len(pre_samplex.graph.edges()) == 1 @@ -163,7 +163,7 @@ def test_propagate_preceded_by_emit(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) subsys_idxs = QubitIndicesPartition.from_elements([0, 1]) assert pre_samplex.graph.nodes()[0] == PreCollect( @@ -171,10 +171,10 @@ def test_propagate_preceded_by_emit(self): ) assert pre_samplex.graph.nodes()[1] == PreEmit(subsys_idxs, Direction.BOTH, PauliRegister) assert pre_samplex.graph.nodes()[2] == PrePropagate( - subsys_idxs, Direction.RIGHT, CXGate(), [[0, 1]], {} + subsys_idxs, Direction.RIGHT, CXGate(), [[0, 1]], InstructionMode.NONE, [] ) assert pre_samplex.graph.nodes()[3] == PrePropagate( - subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], {} + subsys_idxs, Direction.LEFT, CXGate(), [[0, 1]], InstructionMode.NONE, [] ) assert len(pre_samplex.graph.edges()) == 3 @@ -187,12 +187,12 @@ def test_propagate_errors_when_not_all_qubits_are_dangling(self): pre_samplex.add_collect(QubitPartition.from_elements(qreg[:1]), RzSxSynth(), []) pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg[:1]), PauliRegister) with pytest.raises(SamplexBuildError, match="overlaps partially with .* left-to-right"): - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) pre_samplex = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}) pre_samplex.add_collect(QubitPartition.from_elements(qreg[:1]), RzSxSynth(), []) with pytest.raises(SamplexBuildError, match="overlaps partially with .* collectors.* left"): - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) def test_error_right_propagate_through_measurement(self): """Test that propagation through measurement to the right raises an error""" @@ -202,7 +202,7 @@ def test_error_right_propagate_through_measurement(self): pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) with pytest.raises(SamplexBuildError, match="Cannot propagate through measure instruction"): - pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionMode.NONE, []) def test_error_left_propagate_through_measurement(self): """Test that propagation through measurement to the left raises an error""" @@ -210,7 +210,7 @@ def test_error_left_propagate_through_measurement(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0}) pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) - pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionMode.NONE, []) with pytest.raises(SamplexBuildError, match="Found an emission without a collector"): pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) @@ -220,7 +220,7 @@ def test_add_propagate_measurement(self): pre_samplex = PreSamplex(qubit_map={qreg[0]: 0}) pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) - pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(Measure(), qreg), InstructionMode.NONE, []) pre_samplex.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) pre_samplex.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) @@ -235,15 +235,25 @@ def test_add_z2_collect(self): state = PreSamplex(qubit_map={qreg[0]: 0, qreg[1]: 1}, cregs=[creg]) state.add_collect(QubitPartition.from_elements(qreg), RzSxSynth(), []) state.add_emit_twirl(QubitPartition.from_elements(qreg), PauliRegister) - state.add_propagate(CircuitInstruction(XGate(), [qreg[0]]), InstructionSpec()) + state.add_propagate(CircuitInstruction(XGate(), [qreg[0]]), InstructionMode.NONE, []) state.add_z2_collect(QubitPartition.from_elements(qreg), [0, 1]) subsys_idxs = QubitIndicesPartition.from_elements([0, 1]) assert state.graph.nodes()[-3] == PrePropagate( - QubitIndicesPartition.from_elements([0]), Direction.RIGHT, XGate(), [[0]], {} + QubitIndicesPartition.from_elements([0]), + Direction.RIGHT, + XGate(), + [[0]], + InstructionMode.NONE, + [], ) assert state.graph.nodes()[-2] == PrePropagate( - QubitIndicesPartition.from_elements([0]), Direction.LEFT, XGate(), [[0]], {} + QubitIndicesPartition.from_elements([0]), + Direction.LEFT, + XGate(), + [[0]], + InstructionMode.NONE, + [], ) assert state.graph.nodes()[-1] == PreZ2Collect( subsys_idxs, {creg.name: [0, 1]}, {creg.name: [0, 1]} @@ -334,7 +344,7 @@ def test_finalize_prunes_unreachable_nodes(self): pre_samplex = PreSamplex(qubit_map={qubit: idx for idx, qubit in enumerate(qreg)}) pre_samplex.add_collect(subsystems, RzSxSynth(), np.array([[0, 1, 2], [3, 4, 5]])) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) pre_samplex.finalize() assert not pre_samplex.graph.nodes() @@ -342,7 +352,7 @@ def test_finalize_prunes_unreachable_nodes(self): pre_samplex.add_collect(subsystems, RzSxSynth(), np.array([[0, 1, 2], [3, 4, 5]])) pre_samplex.add_emit_twirl(subsystems, VirtualType.PAULI) pre_samplex.add_collect(subsystems, RzSxSynth(), np.array([[0, 1, 2], [3, 4, 5]])) - pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionSpec()) + pre_samplex.add_propagate(CircuitInstruction(CXGate(), qreg), InstructionMode.NONE, []) assert len(pre_samplex.graph) == 4 assert len(pre_samplex.graph.edges()) == 3 @@ -366,7 +376,7 @@ def test_nodes_clustered(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3, 4, 5]) # noqa: SLF001 @@ -381,8 +391,8 @@ def test_nodes_different_modes(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) - pre_samplex.add_propagate(circ[0], InstructionSpec(mode=InstructionMode.MULTIPLY)) - pre_samplex.add_propagate(circ[1], InstructionSpec(mode=InstructionMode.PROPAGATE)) + pre_samplex.add_propagate(circ[0], InstructionMode.MULTIPLY, []) + pre_samplex.add_propagate(circ[1], InstructionMode.PROPAGATE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3]) # noqa: SLF001 @@ -401,7 +411,7 @@ def test_nodes_different_predecessors(self): QubitPartition(1, ((circ.qubits[2],), (circ.qubits[3],))), RzSxSynth(), [] ) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl( QubitPartition(1, ((circ.qubits[0],), (circ.qubits[1],))), PauliRegister ) @@ -422,7 +432,7 @@ def test_nodes_different_operation(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3]) # noqa: SLF001 @@ -438,7 +448,7 @@ def test_nodes_overlaping_qubits(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circ.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in circ: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) clusters = pre_samplex._cluster_pre_propagate_nodes([0, 1, 2, 3]) # noqa: SLF001 @@ -458,10 +468,10 @@ def test_pre_samplex_with_mergeable_pre_propagates(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_collect(subsystems, RzSxSynth(), []) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -491,10 +501,10 @@ def test_pre_samplex_with_no_mergeable_pre_propagates(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_collect(subsystems, RzSxSynth(), []) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -538,7 +548,7 @@ def test_pre_samplex_with_mergeable_pre_propagates_in_series(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box1.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box1: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -575,7 +585,7 @@ def test_partitions_are_rescaled_correctly(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, PauliRegister) pre_samplex.prune_prenodes_unreachable_from_emission() @@ -598,9 +608,7 @@ def test_unsupported_propagate_error(self): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(circuit.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), np.arange(6).reshape(2, 3)) - pre_samplex.add_propagate( - next(iter(circuit)), InstructionSpec(mode=InstructionMode.MULTIPLY) - ) + pre_samplex.add_propagate(next(iter(circuit)), InstructionMode.MULTIPLY, []) pre_samplex.add_emit_twirl(subsystems, VirtualType.PAULI) pre_samplex.add_collect(subsystems, RzSxSynth(), np.arange(6).reshape(2, 3)) @@ -652,10 +660,10 @@ def test_draw(self, save_plot): pre_samplex = PreSamplex(qubit_map={q: idx for idx, q in enumerate(box.qregs[0])}) pre_samplex.add_collect(subsystems, RzSxSynth(), []) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_emit_twirl(subsystems, VirtualType.PAULI) for instr in box: - pre_samplex.add_propagate(instr, InstructionSpec()) + pre_samplex.add_propagate(instr, InstructionMode.NONE, []) pre_samplex.add_collect(subsystems, RzSxSynth(), []) save_plot(pre_samplex.draw()) From 33444145a77853148075cfed8f02d9edb2951c00 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Wed, 15 Oct 2025 11:05:22 -0230 Subject: [PATCH 18/22] Make errors in BoxBuilders BuildError. --- samplomatic/builders/box_builder.py | 43 ++++++++++--------- .../test_builders_pre_samplex.py | 8 ++-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index 938f0655..7236eb71 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -18,7 +18,7 @@ from qiskit.circuit import Barrier from ..aliases import CircuitInstruction, ParamIndices -from ..exceptions import BuildError, SamplexBuildError, TemplateBuildError +from ..exceptions import BuildError from ..partition import QubitPartition from ..pre_samplex import PreSamplex from .builder import Builder @@ -51,7 +51,7 @@ def _append_dressed_layer(self) -> ParamIndices: for qubit in subsys if qubit not in self.template_state.qubit_map } - raise TemplateBuildError( + raise BuildError( f"The qubits '{not_found}' could not be found when recursing into a box of the " "input circuit." ) from KeyError @@ -94,7 +94,9 @@ def parse(self, instr: CircuitInstruction): if (qubit,) not in self.measured_qubits: self.measured_qubits.add((qubit,)) else: - raise SamplexBuildError("Cannot measure the same qubit twice in a dressed box.") + raise BuildError( + "Cannot measure the same qubit more than once in a dressed box." + ) self.template_state.append_remapped_gate(instr) self.clbit_idxs.extend( [self.template_state.template.find_bit(clbit)[0] for clbit in instr.clbits] @@ -103,13 +105,14 @@ def parse(self, instr: CircuitInstruction): if (num_qubits := instr.operation.num_qubits) == 1: if self.measured_qubits.overlaps_with(instr.qubits): - raise RuntimeError( - "Cannot handle single-qubit gate to the right of measurements when " - "dressing=left." + raise BuildError( + "Cannot handle single-qubit gate to the right of a measurement in a " + "left-dressed box. " ) if not self.entangled_qubits.isdisjoint(instr.qubits): - raise RuntimeError( - "Cannot handle single-qubit gate to the right of entangler when dressing=left." + raise BuildError( + "Cannot handle single-qubit gate to the right of an entangler in a " + "left-dressed box." ) # the action of this single-qubit gate will be absorbed into the dressing mode = InstructionMode.MULTIPLY @@ -118,18 +121,17 @@ def parse(self, instr: CircuitInstruction): params.extend((None, param) for param in instr.operation.params) elif num_qubits > 1: + if self.measured_qubits.overlaps_with(instr.qubits): + raise BuildError( + f"Cannot handle instruction {name} to the right of a measurement in a " + "left-dressed box." + ) self.entangled_qubits.update(instr.qubits) params = self.template_state.append_remapped_gate(instr) mode = InstructionMode.PROPAGATE else: - raise RuntimeError(f"Instruction {instr} could not be parsed.") + raise BuildError(f"Instruction {instr} could not be parsed.") - if self.measured_qubits.overlaps_with(instr.qubits): - # TODO: What about delays? barriers? - raise SamplexBuildError( - f"Instruction {instr} happens after a measurement. No operations allowed " - "after a measurement in a dressed box." - ) self.samplex_state.add_propagate(instr, mode, params) def lhs(self): @@ -153,7 +155,7 @@ def rhs(self): self.samplex_state.add_emit_twirl(self.emission.qubits, twirl_type) if len(self.measured_qubits) != 0: if twirl_type != VirtualType.PAULI: - raise SamplexBuildError( + raise BuildError( f"Cannot use {twirl_type.value} twirl in a box with measurements." ) self.samplex_state.add_z2_collect(self.measured_qubits, self.clbit_idxs) @@ -174,7 +176,7 @@ def parse(self, instr: CircuitInstruction): return if name.startswith("meas"): - raise BuildError("Cannot measure the same qubit twice in a dressed box.") + raise BuildError("Measurements are not currently supported in right-dressed boxes.") elif (num_qubits := instr.operation.num_qubits) == 1: self.entangled_qubits.update(instr.qubits) @@ -186,13 +188,14 @@ def parse(self, instr: CircuitInstruction): elif num_qubits > 1: if not self.entangled_qubits.isdisjoint(instr.qubits): - raise RuntimeError( - "Cannot handle single-qubit gate to the left of entangler when dressing=right." + raise BuildError( + "Cannot handle single-qubit gate to the left of an entangler in a " + "right-dressed box." ) params = self.template_state.append_remapped_gate(instr) mode = InstructionMode.PROPAGATE else: - raise RuntimeError(f"Instruction {instr} could not be parsed.") + raise BuildError(f"Instruction {instr} could not be parsed.") self.samplex_state.add_propagate(instr, mode, params) diff --git a/test/unit/test_builders/test_builders_pre_samplex.py b/test/unit/test_builders/test_builders_pre_samplex.py index 4cfdbfaf..a758b9ca 100644 --- a/test/unit/test_builders/test_builders_pre_samplex.py +++ b/test/unit/test_builders/test_builders_pre_samplex.py @@ -22,7 +22,7 @@ from samplomatic.builders.specs import CollectionSpec, EmissionSpec from samplomatic.builders.template_state import TemplateState from samplomatic.constants import Direction -from samplomatic.exceptions import SamplexBuildError +from samplomatic.exceptions import BuildError from samplomatic.partition import QubitIndicesPartition, QubitPartition from samplomatic.pre_samplex import PreSamplex from samplomatic.pre_samplex.graph_data import PreCollect, PreEmit, PreZ2Collect @@ -111,9 +111,7 @@ def test_wrong_twirl_type_for_measurement(self): builder.lhs() builder.parse(CircuitInstruction(Measure(), qreg, creg)) - with pytest.raises( - SamplexBuildError, match="Cannot use u2 twirl in a box with measurements" - ): + with pytest.raises(BuildError, match="Cannot use u2 twirl in a box with measurements"): builder.rhs() def test_two_measurements_on_the_same_qubit_error(self): @@ -125,6 +123,6 @@ def test_two_measurements_on_the_same_qubit_error(self): builder.parse(CircuitInstruction(Measure(), qreg, creg)) with pytest.raises( - SamplexBuildError, match="Cannot measure the same qubit twice in a dressed box" + BuildError, match="Cannot measure the same qubit more than once in a dressed box" ): builder.parse(CircuitInstruction(Measure(), qreg, creg)) From cf73fcb57c185523d19a3cf65ffc475a19ac9103 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Wed, 15 Oct 2025 17:47:12 -0230 Subject: [PATCH 19/22] Actually working. --- samplomatic/builders/box_builder.py | 14 ++++++++------ samplomatic/builders/dynamic_builder.py | 6 +++--- samplomatic/pre_samplex/pre_samplex.py | 4 ++-- test/integration/test_dynamic_circuits.py | 22 +++++++++------------- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/samplomatic/builders/box_builder.py b/samplomatic/builders/box_builder.py index cf69f891..ef303341 100644 --- a/samplomatic/builders/box_builder.py +++ b/samplomatic/builders/box_builder.py @@ -206,7 +206,15 @@ def parse(self, instr: CircuitInstruction): if name.startswith("meas"): raise BuildError("Cannot measure the same qubit twice in a dressed box.") + if not self.collection.dynamic_qubits.all_elements.isdisjoint(instr.qubits): + raise RuntimeError( + "Cannot handle a dynamic instruction and another instruction on " + f"qubits {instr.qubits} in the same dressed box." + ) + if name.startswith("if_else"): + for q in instr.qubits: + self.collection.dynamic_qubits.add((q,)) self._validate_if_else(instr.operation) builder = BoxRightIfElseBuilder( instr, self.samplex_state, self.collection.synth, self.template_state.param_iter @@ -216,12 +224,6 @@ def parse(self, instr: CircuitInstruction): self.template_state.template.append(if_else, new_qubits, instr.clbits) return - elif not self.collection.dynamic_qubits.all_elements.isdisjoint(instr.qubits): - raise RuntimeError( - "Cannot handle a dynamic instruction and another instruction on " - f"qubits {instr.qubits} in the same dressed box." - ) - if (num_qubits := instr.operation.num_qubits) == 1: self.entangled_qubits.update(instr.qubits) # the action of this single-qubit gate will be absorbed into the dressing diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 9ca2b005..4247c99e 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -61,7 +61,7 @@ def append_propagate(self, block: QuantumCircuit, new_block: QuantumCircuit): new_op = type(instr.operation)(*params) if params else instr.operation new_block.append(new_op, instr.qubits, instr.clbits) mode = InstructionMode.PROPAGATE - self.pre_samplex.add_propagate(instr, params=params, mode=mode) + self.pre_samplex.add_propagate(instr, mode=mode, params=params) def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> ParamIndices: start = self.param_iter.idx @@ -88,7 +88,7 @@ def build_block(self, block): qubits = QubitPartition.from_elements(new_block.qubits) subsystems = pre_samplex.qubits_to_indices(qubits) - dangler_match = DanglerMatch(node_types=(PreEmit, PrePropagate)) + dangler_match = DanglerMatch(Direction.RIGHT, node_types=(PreEmit, PrePropagate)) new_danglers = [] for node_idx, partition in pre_samplex.find_then_remove_danglers(dangler_match, subsystems): @@ -137,7 +137,7 @@ def build_block(self, block): pre_samplex = self.pre_samplex.remap(qubit_map=self.block_qubit_map(block)) qubits = QubitPartition.from_elements(new_block.qubits) subsystems = pre_samplex.qubits_to_indices(qubits) - dangler_match = DanglerMatch(node_types=(PreCollect, PrePropagate)) + dangler_match = DanglerMatch(Direction.LEFT, node_types=(PreCollect, PrePropagate)) list(pre_samplex.find_then_remove_danglers(dangler_match, subsystems)) pre_samplex.add_collect(qubits, self.synth, params) diff --git a/samplomatic/pre_samplex/pre_samplex.py b/samplomatic/pre_samplex/pre_samplex.py index 250f7acc..7a0f83f2 100644 --- a/samplomatic/pre_samplex/pre_samplex.py +++ b/samplomatic/pre_samplex/pre_samplex.py @@ -1192,12 +1192,12 @@ def add_copy_node( pre_nodes_to_nodes, order, register_names, - f"combine_{reg_idx}", + combine_name := f"combine_{reg_idx}", virtual_type, ) copy_node = CopyNode( - register_name, + combine_name, copy_name := f"copy_{reg_idx}", virtual_type, len(self.graph[pre_copy_idx].subsystems), diff --git a/test/integration/test_dynamic_circuits.py b/test/integration/test_dynamic_circuits.py index 9915ab86..e413a6dd 100644 --- a/test/integration/test_dynamic_circuits.py +++ b/test/integration/test_dynamic_circuits.py @@ -68,7 +68,7 @@ def test_right_dressed_twirled_conditional(self, save_plot): circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1) + circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): with circuit.if_test((circuit.clbits[0], 1)) as _else: circuit.cx(0, 1) @@ -88,9 +88,8 @@ def test_right_dressed_twirled_conditional_no_else(self, save_plot): circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1) - with circuit.box([Twirl(dressing="right")]): circuit.cx(0, 1) + with circuit.box([Twirl(dressing="right")]): with circuit.if_test((circuit.clbits[0], 1)): circuit.cx(0, 1) circuit.x(0) @@ -107,10 +106,10 @@ def test_right_dressed_parametric_twirled_conditional(self, save_plot): circuit.measure(0, 0) with circuit.box([Twirl(dressing="left")]): - circuit.noop(0, 1, 2) - with circuit.box([Twirl(dressing="right")]): circuit.noop(2) circuit.cx(0, 1) + with circuit.box([Twirl(dressing="right")]): + circuit.noop(2) with circuit.if_test((circuit.clbits[0], 1)) as _else: circuit.cx(0, 1) circuit.x(0) @@ -138,9 +137,8 @@ def test_left_dressed_twirled_conditional(self, save_plot): circuit.sx(0) circuit.cx(1, 0) circuit.x(2) - with circuit.box([Twirl()]): - circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): + circuit.cx(0, 1) circuit.h(1) circuit.noop(0, 2) @@ -150,7 +148,7 @@ def test_left_dressed_twirled_conditional(self, save_plot): def test_left_dressed_twirled_conditional_no_else(self, save_plot): """Test a conditional without else clause in a left-dressed twirl box.""" - circuit = QuantumCircuit(3, 2) + circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.measure(0, 0) @@ -158,11 +156,10 @@ def test_left_dressed_twirled_conditional_no_else(self, save_plot): with circuit.if_test((circuit.clbits[0], 1)): circuit.x(0) circuit.cx(0, 1) - with circuit.box([Twirl()]): - circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): - circuit.noop(0) + circuit.cx(0, 1) circuit.h(1) + circuit.noop(0) circuit.measure_all() @@ -185,9 +182,8 @@ def test_left_dressed_parametric_twirled_conditional(self, save_plot): circuit.rx(2 * p, 0) circuit.cx(1, 0) circuit.x(2) - with circuit.box([Twirl()]): - circuit.cx(0, 1) with circuit.box([Twirl(dressing="right")]): + circuit.cx(0, 1) circuit.h(1) circuit.noop(0, 2) From eb870781ebccc3c064a4e7ccb79f38f5aa42503f Mon Sep 17 00:00:00 2001 From: joshuasn Date: Thu, 16 Oct 2025 16:13:29 -0230 Subject: [PATCH 20/22] CopyNode tests and docs. --- samplomatic/samplex/nodes/copy_node.py | 26 +++++++++++--- .../test_samplex/test_nodes/test_copy_node.py | 35 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 test/unit/test_samplex/test_nodes/test_copy_node.py diff --git a/samplomatic/samplex/nodes/copy_node.py b/samplomatic/samplex/nodes/copy_node.py index e6114e4a..3a9459bb 100644 --- a/samplomatic/samplex/nodes/copy_node.py +++ b/samplomatic/samplex/nodes/copy_node.py @@ -22,10 +22,10 @@ class CopyNode(EvaluationNode): """Copies a register. Args: - regoster_name: - output_name: - register_type: - num_subsystems: + regoster_name: The name of the register to copy. + output_name: The name of the copy register. + register_type: The type of register to copy. + num_subsystems: The number of subsystems the register contains. """ def __init__( @@ -40,6 +40,24 @@ def __init__( self._output_type = output_type self._num_subsystems = num_subsystems + def _to_json_dict(self) -> dict[str, str]: + return { + "node_type": "13", + "register_name": self._register_name, + "output_name": self._output_name, + "output_type": self._output_type, + "num_subsystems": self._num_subsystems, + } + + @classmethod + def _from_json_dict(cls, data: dict[str, str]) -> "CopyNode": + return cls( + data["register_name"], + data["output_name"], + VirtualType(data["output_type"]), + int(data["num_subsystems"]), + ) + @property def outgoing_register_type(self): return self._output_type diff --git a/test/unit/test_samplex/test_nodes/test_copy_node.py b/test/unit/test_samplex/test_nodes/test_copy_node.py new file mode 100644 index 00000000..344741aa --- /dev/null +++ b/test/unit/test_samplex/test_nodes/test_copy_node.py @@ -0,0 +1,35 @@ +# This code is a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +from samplomatic.annotations import VirtualType +from samplomatic.samplex.nodes import CopyNode +from samplomatic.virtual_registers import PauliRegister + + +def test_construction(): + """Test construction and basic attributes.""" + node = CopyNode("existing", "new", VirtualType.PAULI, 5) + assert node.instantiates() == {"new": (5, VirtualType.PAULI)} + assert node.reads_from() == {"existing": (set(range(5)), VirtualType.PAULI)} + assert not node.removes() + assert not node.writes_to() + assert node.outgoing_register_type is VirtualType.PAULI + + +def test_evaluate(): + """Test the evaluation method.""" + node = CopyNode("existing", "new", VirtualType.PAULI, 5) + registers = {"existing": PauliRegister.identity(5, 3)} + node.evaluate(registers) + assert registers["new"] == registers["existing"] + assert registers["new"] is not registers["existing"] From 1c41b881e027ed47273c22a2a9bf17f99c73b7e3 Mon Sep 17 00:00:00 2001 From: joshuasn Date: Thu, 16 Oct 2025 16:50:32 -0230 Subject: [PATCH 21/22] Documenting dynamic builder. --- samplomatic/builders/dynamic_builder.py | 173 +++++++++++++++--------- 1 file changed, 108 insertions(+), 65 deletions(-) diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 4247c99e..292ad10a 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -10,14 +10,16 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""BoxIfElseBuilder""" +"""DynamicBuilder""" +import abc from copy import deepcopy import numpy as np -from qiskit.circuit import IfElseOp, QuantumCircuit +from qiskit.circuit import ControlFlowOp, IfElseOp, QuantumCircuit -from ..aliases import ParamIndices, Qubit, QubitIndex +from ..aliases import CircuitInstruction, ParamIndices, Qubit, QubitIndex +from ..exceptions import BuildError from ..partition import QubitPartition from ..pre_samplex import DanglerMatch, PreSamplex from ..pre_samplex.graph_data import Direction, PreCollect, PreCopy, PreEdge, PreEmit, PrePropagate @@ -26,10 +28,23 @@ from .specs import InstructionMode -class BoxIfElseBuilder: +class DynamicBuilder(abc.ABC): + """Base class for building dressed conditional operations. + + This class does not inherit from :class:`~.Builder` as it does not add the operations to the + template. Instead, it constructs an operation of the same type while adding nodes to the + corresponding samplex. + + Args: + op: The control flow operation to build + pre_samplex: The pre-samplex to use. + synth: The synthesizer to use for the dressing. + param_iter: An iterator over parameters to use in the circuit being built. + """ + def __init__( self, - op, + op: ControlFlowOp, pre_samplex: PreSamplex, synth: Synth, param_iter: ParamIter, @@ -39,42 +54,48 @@ def __init__( self.synth = synth self.param_iter = param_iter - def block_qubit_map(self, block) -> dict[Qubit, QubitIndex]: + def _block_qubit_map(self, block) -> dict[Qubit, QubitIndex]: block_map = {i_q: b_q for i_q, b_q in zip(self.op.qubits, block.qubits)} qubit_map = self.pre_samplex.qubit_map return {block_map[i_q]: qubit_map[i_q] for i_q in qubit_map if i_q in block_map} - def append_propagate(self, block: QuantumCircuit, new_block: QuantumCircuit): - for instr in block: - if len(instr.qubits) == 1: - mode = InstructionMode.MULTIPLY - params = [] - if instr.operation.is_parameterized(): - params.extend((None, param) for param in instr.operation.params) - else: - params = [] - param_mapping = [] - for param in instr.operation.params: - param_mapping.append([self.param_iter.idx, param]) - params.append(next(self.param_iter)) - - new_op = type(instr.operation)(*params) if params else instr.operation - new_block.append(new_op, instr.qubits, instr.clbits) - mode = InstructionMode.PROPAGATE - self.pre_samplex.add_propagate(instr, mode=mode, params=params) - - def append_template(self, block: QuantumCircuit, new_block: QuantumCircuit) -> ParamIndices: + def _parse_mq_gate(self, instr: CircuitInstruction, new_block: QuantumCircuit): + new_params = [] + param_mapping = [] + for param in instr.operation.params: + param_mapping.append([self.param_iter.idx, param]) + new_params.append(next(self.param_iter)) + + new_op = type(instr.operation)(*new_params) if new_params else instr.operation + new_block.append(new_op, instr.qubits, instr.clbits) + self.pre_samplex.add_propagate(instr, mode=InstructionMode.PROPAGATE, params=param_mapping) + + def _parse_sq_gate(self, instr: CircuitInstruction): + new_params = [] + if instr.operation.is_parameterized(): + new_params.extend((None, param) for param in instr.operation.params) + self.pre_samplex.add_propagate(instr, mode=InstructionMode.MULTIPLY, params=new_params) + + def _append_dressed_layer(self, new_block: QuantumCircuit) -> ParamIndices: start = self.param_iter.idx - num_params = len(block.qubits) * self.synth.num_params + num_params = len(new_block.qubits) * self.synth.num_params params = np.arange(start, start + num_params, dtype=np.intp) for qubit in new_block.qubits: for instr in self.synth.make_template([qubit], self.param_iter): new_block.append(instr) - return params.reshape(len(block.qubits), -1) + return params.reshape(len(new_block.qubits), -1) + + @abc.abstractmethod + def build_block(self) -> QuantumCircuit: + """Build a block of a control flow operation.""" + @abc.abstractmethod + def build(self) -> ControlFlowOp: + """Build the operation.""" -class BoxRightIfElseBuilder(BoxIfElseBuilder): - def build_block(self, block): + +class BoxLeftIfElseBuilder(DynamicBuilder): + def build_block(self, block) -> QuantumCircuit: block = ( block if block is not None @@ -84,35 +105,44 @@ def build_block(self, block): ) new_block = QuantumCircuit(block.qubits, block.clbits) - pre_samplex = self.pre_samplex.remap(qubit_map=self.block_qubit_map(block)) + params = self._append_dressed_layer(new_block) + pre_samplex = self.pre_samplex.remap(qubit_map=self._block_qubit_map(block)) qubits = QubitPartition.from_elements(new_block.qubits) subsystems = pre_samplex.qubits_to_indices(qubits) - dangler_match = DanglerMatch(Direction.RIGHT, node_types=(PreEmit, PrePropagate)) + dangler_match = DanglerMatch(Direction.LEFT, node_types=(PreCollect, PrePropagate)) - new_danglers = [] - for node_idx, partition in pre_samplex.find_then_remove_danglers(dangler_match, subsystems): - copy_idx = pre_samplex.graph.add_node(PreCopy(partition, Direction.RIGHT)) - edge = PreEdge(partition, Direction.RIGHT) - pre_samplex.graph.add_edge(node_idx, copy_idx, edge) - new_danglers.append((copy_idx, partition)) + list(pre_samplex.find_then_remove_danglers(dangler_match, subsystems)) + pre_samplex.add_collect(qubits, self.synth, params) - for node_idx, partition in new_danglers: - pre_samplex.add_dangler(partition.all_elements, node_idx) + entangled_qubits = set() + for instr in block: + if len(instr.qubits) == 1: + if not entangled_qubits.isdisjoint(instr.qubits): + raise BuildError( + "Cannot have entanglers before single-qubit gates in a left-dressed box." + ) + self._parse_sq_gate(instr) + else: + entangled_qubits.update(instr.qubits) + self._parse_mq_gate(instr, new_block) - self.append_propagate(block, new_block) - params = self.append_template(block, new_block) - collect_idx = pre_samplex.add_collect(qubits, self.synth, params) + copy_idxs = [] + for node_idx, partition in pre_samplex.find_then_remove_danglers(dangler_match, subsystems): + copy_idx = pre_samplex.graph.add_node(PreCopy(partition, Direction.LEFT)) + copy_idxs.append((copy_idx, partition)) + edge = PreEdge(partition, Direction.LEFT) + pre_samplex.graph.add_edge(copy_idx, node_idx, edge) - return new_block, (collect_idx, subsystems) + return new_block, copy_idxs def build(self): original_danglers = deepcopy(self.pre_samplex.get_all_danglers()) - if_block, if_dangler = self.build_block(self.op.params[0]) + if_block, if_danglers = self.build_block(self.op.params[0]) self.pre_samplex.set_all_danglers(*original_danglers) - else_block, else_dangler = self.build_block(self.op.params[1]) + else_block, else_danglers = self.build_block(self.op.params[1]) - for node_idx, subsystems in [if_dangler, else_dangler]: + for node_idx, subsystems in [*if_danglers, *else_danglers]: self.pre_samplex.add_dangler( subsystems.all_elements, node_idx, @@ -121,8 +151,8 @@ def build(self): return IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) -class BoxLeftIfElseBuilder(BoxIfElseBuilder): - def build_block(self, block): +class BoxRightIfElseBuilder(DynamicBuilder): + def build_block(self, block) -> QuantumCircuit: block = ( block if block is not None @@ -132,33 +162,46 @@ def build_block(self, block): ) new_block = QuantumCircuit(block.qubits, block.clbits) - params = self.append_template(block, new_block) + pre_samplex = self.pre_samplex.remap(qubit_map=self._block_qubit_map(block)) - pre_samplex = self.pre_samplex.remap(qubit_map=self.block_qubit_map(block)) qubits = QubitPartition.from_elements(new_block.qubits) subsystems = pre_samplex.qubits_to_indices(qubits) - dangler_match = DanglerMatch(Direction.LEFT, node_types=(PreCollect, PrePropagate)) - - list(pre_samplex.find_then_remove_danglers(dangler_match, subsystems)) - pre_samplex.add_collect(qubits, self.synth, params) - self.append_propagate(block, new_block) + dangler_match = DanglerMatch(Direction.RIGHT, node_types=(PreEmit, PrePropagate)) - copy_idxs = [] + new_danglers = [] for node_idx, partition in pre_samplex.find_then_remove_danglers(dangler_match, subsystems): - copy_idx = pre_samplex.graph.add_node(PreCopy(partition, Direction.LEFT)) - copy_idxs.append((copy_idx, partition)) - edge = PreEdge(partition, Direction.LEFT) - pre_samplex.graph.add_edge(copy_idx, node_idx, edge) + copy_idx = pre_samplex.graph.add_node(PreCopy(partition, Direction.RIGHT)) + edge = PreEdge(partition, Direction.RIGHT) + pre_samplex.graph.add_edge(node_idx, copy_idx, edge) + new_danglers.append((copy_idx, partition)) - return new_block, copy_idxs + for node_idx, partition in new_danglers: + pre_samplex.add_dangler(partition.all_elements, node_idx) + + unentangled_qubits = set() + for instr in block: + if len(instr.qubits) == 1: + unentangled_qubits.update(instr.qubits) + self._parse_sq_gate(instr) + else: + if not unentangled_qubits.isdisjoint(instr.qubits): + raise BuildError( + "Cannot have entanglers after single-qubit gates in a right-dressed box." + ) + self._parse_mq_gate(instr, new_block) + + params = self._append_dressed_layer(new_block) + collect_idx = pre_samplex.add_collect(qubits, self.synth, params) + + return new_block, (collect_idx, subsystems) def build(self): original_danglers = deepcopy(self.pre_samplex.get_all_danglers()) - if_block, if_danglers = self.build_block(self.op.params[0]) + if_block, if_dangler = self.build_block(self.op.params[0]) self.pre_samplex.set_all_danglers(*original_danglers) - else_block, else_danglers = self.build_block(self.op.params[1]) + else_block, else_dangler = self.build_block(self.op.params[1]) - for node_idx, subsystems in [*if_danglers, *else_danglers]: + for node_idx, subsystems in [if_dangler, else_dangler]: self.pre_samplex.add_dangler( subsystems.all_elements, node_idx, From dac86f7dfe64d74119ce5f048548bcb91ec4e5ff Mon Sep 17 00:00:00 2001 From: joshuasn Date: Thu, 16 Oct 2025 17:01:56 -0230 Subject: [PATCH 22/22] Generic. --- samplomatic/builders/dynamic_builder.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/samplomatic/builders/dynamic_builder.py b/samplomatic/builders/dynamic_builder.py index 292ad10a..ae39918b 100644 --- a/samplomatic/builders/dynamic_builder.py +++ b/samplomatic/builders/dynamic_builder.py @@ -14,9 +14,10 @@ import abc from copy import deepcopy +from typing import Generic, TypeVar import numpy as np -from qiskit.circuit import ControlFlowOp, IfElseOp, QuantumCircuit +from qiskit.circuit import IfElseOp, QuantumCircuit from ..aliases import CircuitInstruction, ParamIndices, Qubit, QubitIndex from ..exceptions import BuildError @@ -27,8 +28,10 @@ from .param_iter import ParamIter from .specs import InstructionMode +T = TypeVar("T") -class DynamicBuilder(abc.ABC): + +class DynamicBuilder(abc.ABC, Generic[T]): """Base class for building dressed conditional operations. This class does not inherit from :class:`~.Builder` as it does not add the operations to the @@ -36,7 +39,7 @@ class DynamicBuilder(abc.ABC): corresponding samplex. Args: - op: The control flow operation to build + op: The control flow operation to build. pre_samplex: The pre-samplex to use. synth: The synthesizer to use for the dressing. param_iter: An iterator over parameters to use in the circuit being built. @@ -44,7 +47,7 @@ class DynamicBuilder(abc.ABC): def __init__( self, - op: ControlFlowOp, + op: T, pre_samplex: PreSamplex, synth: Synth, param_iter: ParamIter, @@ -90,11 +93,11 @@ def build_block(self) -> QuantumCircuit: """Build a block of a control flow operation.""" @abc.abstractmethod - def build(self) -> ControlFlowOp: + def build(self) -> T: """Build the operation.""" -class BoxLeftIfElseBuilder(DynamicBuilder): +class BoxLeftIfElseBuilder(DynamicBuilder[IfElseOp]): def build_block(self, block) -> QuantumCircuit: block = ( block @@ -151,7 +154,7 @@ def build(self): return IfElseOp(self.op.operation.condition, if_block, else_block, self.op.label) -class BoxRightIfElseBuilder(DynamicBuilder): +class BoxRightIfElseBuilder(DynamicBuilder[IfElseOp]): def build_block(self, block) -> QuantumCircuit: block = ( block