Source code for loom.validator.utilities

"""
Copyright (c) Entropica Labs Pte Ltd 2025.

Use, distribution and reproduction of this program in its source or compiled
form is prohibited without the express written consent of Entropica Labs Pte
Ltd.

"""

from functools import reduce
from itertools import product
import numpy as np

from ..eka import LogicalState
from ..eka.utilities import SignedPauliOp, is_tableau_valid, StabArray
from ..cliffordsim import Engine


[docs] def get_all_cliffordsim_registers_with_random_flags( cat_engine: Engine, ) -> dict[str, tuple[int, bool | None]]: """ Get all the classical registers in the given cliffordsim engine. Parameters ---------- cat_engine : Engine The cliffordsim engine that contains the classical registers. Returns ------- dict[str, tuple[int, bool | None]] A dictionary where the keys are the classical register names and the values are tuples containing the value of the register and a flag indicating whether the register is a result of a random measurement (True) or not (False). If the register is not a result of a measurement, the flag is set to None. """ # Get all the classical registers in the cat_engine # Set their random flags to None res_is_random_dict = { bit_id: (bit_value, None) for reg in cat_engine.registry.values() for bit_id, bit_value in reg.id_bit_reg.items() } # Look up whether the bit comes as a result of a random measurement # Flatten the measurement results to a dict of tuples (m_result, is_random) cat_mres_dict = cat_engine.data_store.measurements measurement_results = {} for time_step in cat_mres_dict["time_step"]: bit_id = list(cat_mres_dict[str(time_step)].keys())[0] meas_result = cat_mres_dict[str(time_step)][bit_id]["measurement_result"] meas_is_random = cat_mres_dict[str(time_step)][bit_id]["is_random"] measurement_results[bit_id] = (meas_result, meas_is_random) # Update the res_is_random_dict with the measurement results and return it res_is_random_dict.update(measurement_results) return res_is_random_dict
[docs] def get_parity_from_cbits(cat_engine: Engine, cbits: tuple[str | int, ...]) -> int: """ Get the parity of the cbits in the given list. Parameters ---------- cat_engine : Engine The cliffordsim engine that contains the runtime results. cbits : tuple[str | int, ...] The list of cbits to check. The cbits can be either strings or integers. The integers are treated as constant values (0 or 1) that can flip the expected parity. The strings are the labels of the classical channels whose values are to be retrieved at runtime. The register name is the first part of the label, e.g. c_(0_0)_0 -> c. Returns ------- int The parity of the cbits. The parity is calculated by XORing the values of all of the cbits. """ # Get all the classical registers in the cat_engine and their values cliffordsim_classical_registers = get_all_cliffordsim_registers_with_random_flags( cat_engine ) # Separate the int cbits from the c_reg values # cbit_int_values : Constant values (0 or 1) that can flip the expected parity # cbit_labels : Labels of the classical channels whose values are to be # retrieved at runtime cbit_int_values = [cbit for cbit in cbits if isinstance(cbit, int)] cbit_labels = [cbit for cbit in cbits if isinstance(cbit, str)] cbit_runtime_values = [] for cbit_label in cbit_labels: # Append the value of the cbit to the list cbit_runtime_values += [cliffordsim_classical_registers[cbit_label][0]] # Evaluate change in parity parity = reduce( lambda x, y: x ^ y, cbit_int_values + cbit_runtime_values, 0, ) if parity not in (0, 1): raise ValueError( "The parity of the cbits is not valid. The parity should be either 0 or 1." ) return parity
[docs] def logical_states_to_check(n_logical_qubits: int) -> list[LogicalState]: """ Returns the logical states to check for a given number of logical qubits. These logical states are constructed in such a way that if these are transformed correctly, then all the logical states should be transformed correctly for a Clifford gate operation. The selection is done such that the effect of the gate on each input logical operator is isolated. For example, to verify the effect on the input logical operators Z1, X1 (logical qubit 1) for a 3-logical-qubit system, the effect should be captured by checking the output of the logical states: {+Z0, +Z1, +Z2}, {+Z0, +X1, +Z2}, {+X0, +X1, +X2} and {+X0, +Z1, +X2}. We can see that the action of the gate on the logical qubit 1 is isolated by keeping the logical qubits 0 and 2 in the same state for the first two and the last two states while changing the state of the logical qubit 1. NOTE: The above has not been mathematically proven, but it is a reasonable assumption that hasn't been disproven yet. Parameters ---------- n_logical_qubits : int The number of logical qubits. Returns ------- list[LogicalState] The list of logical states to check. """ if n_logical_qubits == 1: # Return the |0> and |+> states return [LogicalState(["+Z0"]), LogicalState(["+X0"])] if n_logical_qubits == 2: # Return the |00>, |++>, |0+>, and |+0> states return [ LogicalState(["+Z0", "+Z1"]), LogicalState(["+X0", "+X1"]), LogicalState(["+Z0", "+X1"]), LogicalState(["+X0", "+Z1"]), ] return ( # |00...0> state [LogicalState([f"+Z{j}" for j in range(n_logical_qubits)])] # |++...+> state + [LogicalState([f"+X{j}" for j in range(n_logical_qubits)])] # |+0...0>, |0+0...0>, ..., |00...0+> states + [ LogicalState( [f"+Z{j}" if i != j else f"+X{j}" for j in range(n_logical_qubits)] ) for i in range(n_logical_qubits) ] # |0+...+>, |+0+...+>, ..., |++...0> states + [ LogicalState( [f"+X{j}" if i != j else f"+Z{j}" for j in range(n_logical_qubits)] ) for i in range(n_logical_qubits) ] )
[docs] def all_possible_pauli_strings(n_qubits: int) -> list[str]: """ Returns all possible sparse Pauli strings for a given number of qubits. The sparse Pauli strings are generated by iterating over all possible combinations of X and Z operators for each qubit. The list size scales exponentially with the number of qubits (as 2*4**n_qubits), so it is important to consider the performance implications when working with large number of (logical) qubits. Parameters ---------- n_qubits : int The number of qubits. Returns ------- list[str] The list of all possible sparse Pauli strings for the given number of qubits. """ if n_qubits < 1 or not isinstance(n_qubits, int): raise ValueError("The number of qubits must be at least 1.") paulis = ["_", "X", "Z", "Y"] signs = ["+", "-"] return [ f"{sign}{''.join(p)}" for sign, p in product(signs, product(paulis, repeat=n_qubits)) if any(c != "_" for c in p) # skip all-identity ] # Exclude the empty string (all I's)
# pylint:disable=anomalous-backslash-in-string, line-too-long
[docs] def logical_state_transformations_to_check( x_operators_sparse_pauli_map: list[str], z_operators_sparse_pauli_map: list[str] ) -> list[tuple[LogicalState, tuple[LogicalState]]]: """Returns the logical state transformations to check for a logical operation. The input is the sparse Pauli strings for the individual X and Z operators. The output is a list of tuples where each tuple contains the input and expected output logical states for the logical operation. Example: For a CNOT gate from qubit 0 to qubit 1 we know that: - X0 -> X0X1 - X1 -> X1 - Z0 -> Z0 - Z1 -> Z1Z0 Thus, we can find the logical state transformations for the CNOT gate by calling this function with the following inputs: - x_operators_sparse_pauli_map = ["X0X1", "X1"] - z_operators_sparse_pauli_map = ["Z0", "Z1Z0"] This will return the list of logical state transformations to check for the CNOT gate which will be a list containing the following tuples: - (LogicalState(('+Z0', '+Z1')), (LogicalState(('+Z0', '+Z0Z1')),)) :math:`\ket{00} -> \ket{00}` - (LogicalState(('+X0', '+X1')), (LogicalState(('+X0X1', '+X1')),)) :math:`\ket{++}-> \ket{++}` - (LogicalState(('+Z0', '+X1')), (LogicalState(('+Z0', '+X1')),)) :math:`\ket{0+} -> \ket{0+}` - (LogicalState(('+X0', '+Z1')), (LogicalState(('+X0X1', '+Z0Z1')),)) :math:`\ket{+0} ->` Bell pair The format of the tuples is such that they can be used as input for validator logical state transformation checks. Parameters ---------- x_operators_sparse_pauli_map : list[str] The list of sparse Pauli strings describing how each X operator is transformed. No sign is needed and the order matches the transformation of the logical operators X0, X1, ... z_operators_sparse_pauli_map : list[str] The list of sparse Pauli strings describing how each Z operator is transformed. No sign is needed and the order matches the transformation of the logical operators Z0, Z1, ... Returns ------- list[tuple[LogicalState, tuple[LogicalState]]] The list of logical state transformations to check. """ if len(z_operators_sparse_pauli_map) != len(x_operators_sparse_pauli_map): raise ValueError( "The number of X and Z operators should be the same for the logical state " "transformations." ) # Get the number of logical qubits n_logical_qubits = len(x_operators_sparse_pauli_map) # Convert the sparse Pauli strings to SignedPauliOp objects and store them in an # np.array for easy access x_signed_pauli_op_map = [ SignedPauliOp.from_sparse_string("+" + x, n_logical_qubits) for x in x_operators_sparse_pauli_map ] z_signed_pauliop_map = [ SignedPauliOp.from_sparse_string("+" + z, n_logical_qubits) for z in z_operators_sparse_pauli_map ] # For the LogicalStates to be valid, the transformed operators should generate # a valid tableau when stacked, similarly to how the X_i and Z_i operators generate # a valid tableau when stacked. tableau_to_check = np.vstack( [x.array for x in x_signed_pauli_op_map] + [z.array for z in z_signed_pauliop_map] ) if not is_tableau_valid(tableau_to_check): raise ValueError( "The transformed X and Z operators do not generate a valid tableau when " "stacked. Check the input sparse Pauli string maps." ) x_transform_stabarray = StabArray.from_signed_pauli_ops(x_signed_pauli_op_map) z_transform_stabarray = StabArray.from_signed_pauli_ops(z_signed_pauliop_map) # Get the input logical states to check input_states = logical_states_to_check(n_logical_qubits) def _multiply_signed_pauli_ops( signed_pauli_ops: list[SignedPauliOp], n_logical_qubits: int = n_logical_qubits, negative: bool = False, ) -> SignedPauliOp: """Multiply all of the Pauli operators together and return the result as a SignedPauliOp object. Need to specify the number of logical qubits and whether the starting sign is negative. """ return reduce( lambda x, y: x * y, signed_pauli_ops, SignedPauliOp.identity(n_logical_qubits, negative=negative), ) # Get the output logical states to check output_states = [ LogicalState( # The logical state needs to be defined as a list of sparse Pauli # strings which will be found by taking the product of the transformed # X and Z operators for each input operator [ # Take the product of the transformed X and Z operators that # are present in the input state # For example, for the CNOT gate and the input state |00>: # we have the input operators +Z0, +Z1 # which are transformed to +Z0, +Z0Z1 respectively _multiply_signed_pauli_ops( # Include all transformed X, Z operators that are present # in the input state (.x==1 or .z==1) x_transform_stabarray[input_pauli_op.x == 1] + z_transform_stabarray[input_pauli_op.z == 1], negative=input_pauli_op.sign == 1, ).as_sparse_string() # cast to sparse string such that it can be used to create a # LogicalState object for input_pauli_op in input_state.stabarray ] ) for input_state in input_states ] # Because the output states need to be tuples of LogicalState objects, we need to # convert the output states to a tuple of LogicalState objects output_states = [(output_state,) for output_state in output_states] # Return the input and output logical states as a list of tuples for validator checks return list(zip(input_states, output_states, strict=True))