Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/apidocs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This package contains functionality which is meant to supplement workflows invol
qiskit_addon_utils.coloring
qiskit_addon_utils.noise_management
qiskit_addon_utils.noise_management.post_selection.transpiler.passes
qiskit_addon_utils.noise_management.pre_selection.transpiler.passes
qiskit_addon_utils.problem_generators
qiskit_addon_utils.slicing
qiskit_addon_utils.slicing.transpiler.passes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
==============================================================================================================
pre_selection_transpiler_passes (:mod:`qiskit_addon_utils.noise_management.pre_selection.transpiler.passes`)
==============================================================================================================

.. automodule:: qiskit_addon_utils.noise_management.pre_selection.transpiler.passes
:no-members:
:no-inherited-members:
:no-special-members:

.. currentmodule:: qiskit_addon_utils.noise_management.pre_selection.transpiler.passes

.. autosummary::
:toctree: ../stubs/
:nosignatures:

AddPreSelectionMeasures
AddSpectatorMeasuresPreSelection

.. Made with Bob
2 changes: 2 additions & 0 deletions docs/apidocs/qiskit_addon_utils.noise_management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ noise_management (:mod:`qiskit_addon_utils.noise_management`)
.. autofunction:: gamma_from_noisy_boxes
.. autoclass:: PostSelectionSummary
.. autoclass:: PostSelector
.. autoclass:: PreSelectionSummary
.. autoclass:: PreSelector
9 changes: 5 additions & 4 deletions qiskit_addon_utils/exp_vals/expectation_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def executor_expectation_values(

Optionally allows averaging over additional axes of `bool_array`, as when twirling.

Optionally supports measurement twirling, PEC, and postselection.
Optionally supports measurement twirling, PEC, pre-selection, and post-selection.

Args:
bool_array: Boolean array, presumably representing data from measured qubits.
Expand All @@ -69,8 +69,9 @@ def executor_expectation_values(
Data processing will use the result of `xor`ing this array with `bool_array`. Must be same shape as `bool_array`.
pauli_signs: Optional boolean array used with probabilistic error cancellation (PEC). Final axis is assumed to index all noisy boxes in circuit. Value of `True` indicates an overall sign of `-1` should be associated with the noisy box, typically because an odd number of inverse-noise errors were inserted in that box for the specified circuit randomization. The final axis is immediately collapsed as a sum mod 2 to obtain the overall sign associated with each circuit randomization.
Remaining shape must be `pauli_signs.shape[:-1] == bool_array.shape[:-2]`. Note this array does not have a shots axis.
postselect_mask: Optional boolean array used for postselection. `True` (`False`) indicates a shot accepted (rejected) by postselection.
Shape must be `bool_array.shape[:-1]`.
postselect_mask: Optional boolean array or sequence of boolean arrays used for post-selection. `True` (`False`) indicates a shot accepted (rejected) by post-selection.
Shape must be `bool_array.shape[:-1]`. Can be obtained from ``PostSelector.compute_mask()`` or ``PreSelector.compute_mask()``.
If multiple masks are provided (e.g., from both pre-selection and post-selection), they should be combined with a logical AND operation before passing to this function.
gamma_factor: Rescaling factor gamma to be applied to PEC mitigated expectation values. If `None`, rescaling factors will be computed as the
number of positive samples minus the number of negative samples, computed as `1/(np.sum(~pauli_signs, axis=avg_axis) - np.sum(pauli_signs, axis=avg_axis))`.
This can fail due to division by zero if there are an equal number of positive and negative samples. Also note this rescales each expectation value
Expand Down Expand Up @@ -145,7 +146,7 @@ def executor_expectation_values(
basis_dict_[basis] = diag_obs_list
basis_dict = basis_dict_

##### POSTSELECTION:
##### POST-SELECTION:
if postselect_mask is not None:
bool_array, basis_dict, num_shots_kept = _apply_postselect_mask(
bool_array, basis_dict, postselect_mask
Expand Down
3 changes: 3 additions & 0 deletions qiskit_addon_utils/noise_management/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

from .gamma_factor import gamma_from_noisy_boxes
from .post_selection import PostSelectionSummary, PostSelector
from .pre_selection import PreSelectionSummary, PreSelector
from .trex_factors import trex_factors

__all__ = [
"PostSelectionSummary",
"PostSelector",
"PreSelectionSummary",
"PreSelector",
"gamma_from_noisy_boxes",
"trex_factors",
]
10 changes: 10 additions & 0 deletions qiskit_addon_utils/noise_management/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@
"""
The default name of the classical register used for measuring spectator qubits.
"""

DEFAULT_PRE_SELECTION_SUFFIX = "_pre"
"""
The default suffix to append to the names of the classical registers used for pre selection measurements.
"""

DEFAULT_SPECTATOR_PRE_CREG_NAME = "spec_pre"
"""
The default name of the classical register used for measuring spectator qubits in pre-selection.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,12 @@ def _get_primary_and_ps_cregs(
cregs: The dictionary of registers.
post_selection_suffix: The suffix of the post selection registers.
"""
# Exclude pre-selection registers (ending with _pre) from primary registers
# that need post-selection validation, since pre-selection happens before the circuit
primary_cregs = {
name: creg for name, creg in cregs.items() if not name.endswith(post_selection_suffix)
name: creg
for name, creg in cregs.items()
if not name.endswith(post_selection_suffix) and not name.endswith("_pre")
}

ps_cregs = {name: creg for name, creg in cregs.items() if name.endswith(post_selection_suffix)}
Expand Down Expand Up @@ -213,8 +217,8 @@ def _get_measure_maps(
measure_map[qubit_map[node.qargs[0]]] = clbit
elif clbit_ps := clbit_map_ps.get(node.cargs[0]):
measure_map_ps[qubit_map[node.qargs[0]]] = clbit_ps
else: # pragma: no cover
raise ValueError(f"Clbit {node.cargs[0]} does not belong to any valid register.")
# Skip measurements into pre-selection registers (ending with _pre)
# as they are not relevant for post-selection analysis

return measure_map, measure_map_ps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,30 @@ def __init__(

def run(self, dag: DAGCircuit): # noqa: D102
# Find what qubits have a terminal measurement
terminal_measurements: dict[Qubit, Clbit] = {
qubit: clbit for qubit, clbit in self._find_terminal_measurements(dag).items() if clbit
}
if not terminal_measurements:
return dag
all_terminal_measurements = self._find_terminal_measurements(dag)

# Add the new registers and create a map between the original clbit and the new ones
# Skip pre-selection registers (those ending with _pre) as they don't need post-selection
clbits_map = {}
for name, creg in dag.cregs.items():
if name.endswith("_pre"):
# Skip pre-selection registers - they don't get post-selection
continue
dag.add_creg(
new_creg := ClassicalRegister(creg.size, name + self.post_selection_suffix)
)
clbits_map.update({clbit: clbit_ps for clbit, clbit_ps in zip(creg, new_creg)})

# Filter terminal measurements to only include those with clbits in clbits_map
# This excludes measurements into pre-selection registers
terminal_measurements: dict[Qubit, Clbit] = {
qubit: clbit
for qubit, clbit in all_terminal_measurements.items()
if clbit and clbit in clbits_map
}
if not terminal_measurements:
return dag

# Add a barrier to separate the post selection measurements from the rest of the circuit
qubits = tuple(terminal_measurements)
dag.apply_operation_back(Barrier(len(qubits)), qubits)
Expand Down Expand Up @@ -122,7 +132,7 @@ def _find_terminal_measurements(self, dag: DAGCircuit) -> dict[Qubit, Clbit]:
for node in dag.topological_op_nodes():
validate_op_is_supported(node)

if node.is_standard_gate():
if node.is_standard_gate() or (name := node.op.name) == "xslow":
for qarg in node.qargs:
terminal_measurements[qarg] = None
elif (name := node.op.name) == "barrier":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,62 @@ def _find_active_and_terminated_qubits(self, dag: DAGCircuit) -> tuple[set[Qubit
# The qubits whose last action is a measurement
terminated_qubits: set[Qubit] = set()

# Track measurements into pre-selection registers to handle them specially
preselection_measurements: dict[Qubit, bool] = {}

for node in dag.topological_op_nodes():
validate_op_is_supported(node)

if node.is_standard_gate():
active_qubits.update(node.qargs)
terminated_qubits.difference_update(node.qargs)
# Skip xslow and rx gates - they are part of pre/post-selection protocol
if ("xslow" in node.op.name) or ("rx" in node.op.name):
continue
elif node.is_standard_gate():
# Check if this is an X gate that's part of a pre-selection sequence
# (X gate immediately before a measurement into a _pre register)
if node.op.name == "x" and len(node.qargs) == 1:
# Look ahead to see if next operation on this qubit is a measurement into _pre
qubit = node.qargs[0]
successors = list(dag.successors(node))
is_preselection_x = False
for succ in successors:
if (
hasattr(succ, "op")
and hasattr(succ.op, "name")
and succ.op.name == "measure"
and len(succ.qargs) == 1
and succ.qargs[0] == qubit
and len(succ.cargs) == 1
):
# Check if measuring into a pre-selection register
clbit = succ.cargs[0]
for creg in dag.cregs.values():
if clbit in creg and creg.name.endswith("_pre"):
is_preselection_x = True
break
break

if not is_preselection_x:
active_qubits.update(node.qargs)
terminated_qubits.difference_update(node.qargs)
else:
active_qubits.update(node.qargs)
terminated_qubits.difference_update(node.qargs)
elif (name := node.op.name) == "barrier":
continue
elif name == "measure":
active_qubits.add(node.qargs[0])
terminated_qubits.add(node.qargs[0])
# Check if this is a measurement into a pre-selection register
if len(node.cargs) == 1:
clbit = node.cargs[0]
is_preselection = False
for creg in dag.cregs.values():
if clbit in creg and creg.name.endswith("_pre"):
is_preselection = True
preselection_measurements[node.qargs[0]] = True
break

if not is_preselection:
active_qubits.add(node.qargs[0])
terminated_qubits.add(node.qargs[0])
elif isinstance(node.op, ControlFlowOp):
# The qubits whose last action is a measurement, block by block
all_terminated_qubits: list[set[Qubit]] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def validate_op_is_supported(node: DAGOpNode):
"""
if (
node.is_standard_gate()
or node.op.name in ["barrier", "measure"]
or node.op.name in ["barrier", "measure", "xslow"]
or isinstance(node.op, ControlFlowOp)
):
return
Expand Down
25 changes: 25 additions & 0 deletions qiskit_addon_utils/noise_management/pre_selection/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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.

# Reminder: update the RST file in docs/apidocs when adding new interfaces.
"""Functions and classes to perform Pre Selection."""

from .pre_selection_summary import PreSelectionSummary
from .pre_selector import PreSelectionStrategy, PreSelector

__all__ = [
"PreSelectionStrategy",
"PreSelectionSummary",
"PreSelector",
]

# Made with Bob
Loading
Loading