Source code for scalerqec.Clifford.clifford

import stim
import numpy as np




oneQGate_ = ["H", "P", "X", "Y", "Z"]
oneQGateindices={"H":0, "P":1, "X":2, "Y":3, "Z":4}


twoQGate_ = ["CNOT", "CZ"]
twoQGateindices={"CNOT":0, "CZ":1}

pauliNoise_ = ["I","X", "Y", "Z"]
pauliNoiseindices={"I":0,"X":1, "Y":2, "Z":3}


[docs] class SingleQGate: def __init__(self, gateindex, qubitindex): self._name = oneQGate_[gateindex] self._qubitindex = qubitindex @property def qubitindex(self): return self._qubitindex @property def name(self): return self._name def __str__(self): return self._name + "[" + str(self._qubitindex) + "]"
[docs] class TwoQGate: def __init__(self, gateindex, control, target): self._name = twoQGate_[gateindex] self._control = control self._target = target @property def control(self): return self._control @property def target(self): return self._target @property def name(self): return self._name def __str__(self): return self._name + "[" + str(self._control) + "," + str(self._target)+ "]"
[docs] class pauliNoise: def __init__(self, noiseindex, qubitindex): self._name="n"+str(noiseindex) self._noiseindex= noiseindex self._qubitindex = qubitindex self._noisetype=0 @property def noisetype(self): return self._noisetype @noisetype.setter def noisetype(self, noisetype): self._noisetype=noisetype def __str__(self): return self._name +"("+pauliNoise_[self._noisetype] +")" +"[" + str(self._qubitindex) + "]"
[docs] class Measurement: def __init__(self,measureindex ,qubitindex): self._name="M"+str(measureindex) self._qubitindex = qubitindex self._measureindex=measureindex @property def qubitindex(self): return self._qubitindex def __str__(self): return self._name + "[" + str(self._qubitindex) + "]"
[docs] class Reset: def __init__(self, qubitindex): self._name="R" self._qubitindex = qubitindex @property def qubitindex(self): return self._qubitindex def __str__(self): return self._name + "[" + str(self._qubitindex) + "]"
#Class: CliffordCircuit
[docs] class CliffordCircuit: def __init__(self, qubit_num): self._qubit_num = qubit_num self._totalnoise=0 self._totalMeas=0 self._totalgates=0 self._gatelists=[] self._error_rate=0 self._index_to_noise={} self._index_to_measurement={} #self._index_to_measurement={} self._shownoise=False self._syndromeErrorTable={} #Store the repeat match group #For example, if we require M0=M1, M2=M3, then the match group is [[0,1],[2,3]] self._parityMatchGroup=[] self._observable=[] self._measIdx_to_parityIdx={} self._stim_str=None self._stimcircuit=stim.Circuit() #self._error_channel @property def gatelists(self): return self._gatelists @property def qubitnum(self): return self._qubit_num @qubitnum.setter def qubitnum(self, qubit_num): self._qubit_num = qubit_num
[docs] def get_measIdx_to_parityIdx(self,measIdx): return self._measIdx_to_parityIdx[measIdx]
@property def stim_str(self): return self._stim_str @stim_str.setter def stim_str(self, stim_str): self._stim_str=stim_str @property def error_rate(self): return self._error_rate @error_rate.setter def error_rate(self, error_rate): self._error_rate=error_rate @property def stimcircuit(self): return self._stimcircuit @stimcircuit.setter def stimcircuit(self, stim_circuit): if isinstance(stim_circuit, str): self._stim_str=stim_circuit self._stimcircuit= stim.Circuit(stim_circuit) elif isinstance(stim_circuit, stim.Circuit): self._stimcircuit=stim_circuit self._stim_str= str(stim_circuit) @property def observable(self): return self._observable @observable.setter def observable(self, observablemeasurements): self._observable=observablemeasurements @property def parityMatchGroup(self): return self._parityMatchGroup @parityMatchGroup.setter def parityMatchGroup(self, parityMatchGroup): self._parityMatchGroup=parityMatchGroup @property def qubit_num(self): return self._qubit_num @property def totalnoise(self): return self._totalnoise @property def totalMeas(self): return self._totalMeas ''' Read the circuit from a file Example of the file: NumberOfQubit 6 cnot 1 2 cnot 1 3 cnot 1 0 M 0 cnot 1 4 cnot 2 4 M 4 cnot 2 5 cnot 3 5 M 5 R 4 R 5 cnot 1 4 cnot 2 4 M 4 cnot 2 5 cnot 3 5 M 5 '''
[docs] def read_circuit_from_file(self, filename): with open(filename, 'r') as file: for line in file: line = line.strip() if not line: continue # Skip empty lines if line.startswith("NumberOfQubit"): # Extract the number of qubits self._qubit_num = int(line.split()[1]) else: # Parse the gate operation parts = line.split() gate_type = parts[0] qubits = list(map(int, parts[1:])) if gate_type == "cnot": self.add_cnot(qubits[0], qubits[1]) elif gate_type == "M": self.add_measurement(qubits[0]) elif gate_type == "R": self.add_reset(qubits[0]) elif gate_type == "H": self.add_hadamard(qubits[0]) elif gate_type == "P": self.add_phase(qubits[0]) elif gate_type == "CZ": self.add_cz(qubits[0], qubits[1]) elif gate_type == "X": self.add_paulix(qubits[0]) elif gate_type == "Y": self.add_pauliy(qubits[0]) elif gate_type == "Z": self.add_pauliz(qubits[0]) else: raise ValueError(f"Unknown gate type: {gate_type}")
''' Compile from a stim circuit string. '''
[docs] def compile_from_stim_circuit_str(self, stim_str): #self._totalnoise=0 self._totalnoise=0 self._totalMeas=0 self._totalgates=0 lines = stim_str.splitlines() output_lines = [] maxum_q_index=0 ''' First, read and compute the parity match group and the observable ''' parityMatchGroup=[] observable=[] measure_index_to_line={} measure_line_to_measure_index={} current_line_index=0 current_measure_index=0 for line in lines: stripped_line = line.strip() if not stripped_line: # Skip empty lines (optional: you could also preserve them) current_line_index+=1 continue # Keep lines that we do NOT want to split if (stripped_line.startswith("TICK") or stripped_line.startswith("DETECTOR(") or stripped_line.startswith("QUBIT_COORDS(") or stripped_line.startswith("OBSERVABLE_INCLUDE(")): current_line_index+=1 continue tokens = stripped_line.split() gate = tokens[0] if gate == "M": measure_index_to_line[current_measure_index]=current_line_index measure_line_to_measure_index[current_line_index]=current_measure_index current_measure_index+=1 current_line_index+=1 current_line_index=0 measure_stack=[] for line in lines: stripped_line = line.strip() if stripped_line.startswith("DETECTOR("): meas_index = [token.strip() for token in stripped_line.split() if token.strip().startswith("rec")] meas_index = [int(x[4:-1]) for x in meas_index] parityMatchGroup.append([measure_line_to_measure_index[measure_stack[x]] for x in meas_index]) current_line_index+=1 continue elif stripped_line.startswith("OBSERVABLE_INCLUDE("): meas_index = [token.strip() for token in stripped_line.split() if token.strip().startswith("rec")] meas_index = [int(x[4:-1]) for x in meas_index] observable=[measure_line_to_measure_index[measure_stack[x]] for x in meas_index] current_line_index+=1 continue tokens = stripped_line.split() gate = tokens[0] if gate == "M": measure_stack.append(current_line_index) current_line_index+=1 ''' Insert gates ''' for line in lines: stripped_line = line.strip() if not stripped_line: # Skip empty lines (optional: you could also preserve them) continue # Keep lines that we do NOT want to split if (stripped_line.startswith("TICK") or stripped_line.startswith("DETECTOR(") or stripped_line.startswith("QUBIT_COORDS(") or stripped_line.startswith("OBSERVABLE_INCLUDE(")): output_lines.append(stripped_line) continue tokens = stripped_line.split() gate = tokens[0] if gate == "CX": control = int(tokens[1]) maxum_q_index=maxum_q_index if maxum_q_index>control else control target = int(tokens[2]) maxum_q_index=maxum_q_index if maxum_q_index>target else target self.add_depolarize(control) self.add_depolarize(target) self.add_cnot(control, target) elif gate == "M": qubit = int(tokens[1]) maxum_q_index=maxum_q_index if maxum_q_index>qubit else qubit self.add_depolarize(qubit) self.add_measurement(qubit) elif gate == "H": qubit = int(tokens[1]) maxum_q_index=maxum_q_index if maxum_q_index>qubit else qubit self.add_depolarize(qubit) self.add_hadamard(qubit) elif gate == "S": qubit = int(tokens[1]) maxum_q_index=maxum_q_index if maxum_q_index>qubit else qubit self.add_depolarize(qubit) self.add_phase(qubit) elif gate == "R": qubits = int(tokens[1]) maxum_q_index=maxum_q_index if maxum_q_index>qubits else qubits self.add_depolarize(qubits) self.add_reset(qubits) ''' Finally, compiler detector and observable ''' self._parityMatchGroup=parityMatchGroup self._observable=observable self._qubit_num=maxum_q_index+1 self.compile_detector_and_observable()
[docs] def save_circuit_to_file(self, filename): pass
[docs] def set_noise_type(self, noiseindex, noisetype): self._index_to_noise[noiseindex].set_noisetype(noisetype)
[docs] def reset_noise_type(self): for i in range(self._totalnoise): self._index_to_noise[i].set_noisetype(0)
[docs] def show_all_noise(self): for i in range(self._totalnoise): print(self._index_to_noise[i])
[docs] def add_xflip_noise(self, qubit): self._stimcircuit.append("X_ERROR", [qubit], self._error_rate) self._gatelists.append(pauliNoise(self._totalnoise, qubit)) self._index_to_noise[self._totalnoise]=self._gatelists[-1] self._totalnoise+=1
[docs] def add_depolarize(self, qubit): self._stimcircuit.append("DEPOLARIZE1", [qubit], self._error_rate) self._gatelists.append(pauliNoise(self._totalnoise, qubit)) self._index_to_noise[self._totalnoise]=self._gatelists[-1] self._totalnoise+=1
[docs] def add_cnot(self, control, target): self._gatelists.append(TwoQGate(twoQGateindices["CNOT"], control, target)) self._stimcircuit.append("CNOT", [control, target])
[docs] def add_hadamard(self, qubit): self._gatelists.append(SingleQGate(oneQGateindices["H"], qubit)) self._stimcircuit.append("H", [qubit])
[docs] def add_phase(self, qubit): self._gatelists.append(SingleQGate(oneQGateindices["P"], qubit)) self._stimcircuit.append("S", [qubit])
[docs] def add_cz(self, qubit1, qubit2): self._gatelists.append(TwoQGate(twoQGateindices["CZ"], qubit1, qubit2))
[docs] def add_paulix(self, qubit): self._gatelists.append(SingleQGate(oneQGateindices["X"], qubit)) self._stimcircuit.append("X", [qubit])
[docs] def add_pauliy(self, qubit): self._gatelists.append(SingleQGate(oneQGateindices["Y"], qubit)) self._stimcircuit.append("Y", [qubit])
[docs] def add_pauliz(self, qubit): self._gatelists.append(SingleQGate(oneQGateindices["Z"], qubit)) self._stimcircuit.append("Z", [qubit])
[docs] def add_measurement(self, qubit): self._gatelists.append(Measurement(self._totalMeas,qubit)) self._stimcircuit.append("M", [qubit]) #self._stimcircuit.append("DETECTOR", [stim.target_rec(-1)]) self._index_to_measurement[self._totalMeas]=self._gatelists[-1] self._measIdx_to_parityIdx[self._totalMeas]=[] self._totalMeas+=1
[docs] def compile_detector_and_observable(self): totalMeas=self._totalMeas #print(totalMeas) detectorIdx=0 for paritygroup in self._parityMatchGroup: #print(paritygroup) #print([k-totalMeas for k in paritygroup]) self._stimcircuit.append("DETECTOR", [stim.target_rec(k-totalMeas) for k in paritygroup]) for k in paritygroup: self._measIdx_to_parityIdx[k].append(detectorIdx) detectorIdx+=1 self._stimcircuit.append("OBSERVABLE_INCLUDE", [stim.target_rec(k-totalMeas) for k in self._observable], 0)
[docs] def add_reset(self, qubit): self._gatelists.append(Reset(qubit)) self._stimcircuit.append("R", [qubit])
[docs] def setShowNoise(self, show): self._shownoise=show
def __str__(self): str="" for gate in self._gatelists: if isinstance(gate, pauliNoise) and not self._shownoise: continue str+=gate.__str__()+"\n" return str
[docs] def get_yquant_latex(self): """ Convert the circuit (stored in self._gatelists) into a yquant LaTeX string. This version simply prints each gate (or noise box) in the order they appear, without grouping or any fancy logic. """ lines = [] # Begin the yquant environment lines.append("\\begin{yquant}") lines.append("") # Declare qubits and classical bits. # Note: Literal braces in the LaTeX code are escaped by doubling them. lines.append("% -- Qubits and classical bits --") lines.append("qubit {{$\\ket{{q_{{\\idx}}}}$}} q[{}];".format(self._qubit_num)) lines.append("cbit {{$c_{{\\idx}} = 0$}} c[{}];".format(self._totalMeas)) lines.append("") lines.append("% -- Circuit Operations --") # Process each gate in the order they were added. for gate in self._gatelists: if isinstance(gate, pauliNoise): # Print the noise box only if noise output is enabled. if self._shownoise: lines.append("[fill=red!80]") # The following format string produces, e.g.,: # "box {$n_{8}$} q[2];" lines.append("box {{$n_{{{}}}$}} q[{}];".format(gate._noiseindex, gate._qubitindex)) elif isinstance(gate, TwoQGate): # Two-qubit gate (e.g., CNOT or CZ). if gate._name == "CNOT": # Note: yquant syntax for a CNOT is: cnot q[target] | q[control]; line = "cnot q[{}] | q[{}];".format(gate._target, gate._control) elif gate._name == "CZ": line = "cz q[{}] | q[{}];".format(gate._target, gate._control) lines.append(line) elif isinstance(gate, SingleQGate): # Single-qubit gate. if gate._name == "H": line = "h q[{}];".format(gate._qubitindex) lines.append(line) elif isinstance(gate, Measurement): # Measurement is output as three separate lines. lines.append("measure q[{}];".format(gate._qubitindex)) lines.append("cnot c[{}] | q[{}];".format(gate._measureindex, gate._qubitindex)) lines.append("discard q[{}];".format(gate._qubitindex)) elif isinstance(gate, Reset): # Reset is output as an initialization command. lines.append("init {{$\\ket0$}} q[{}];".format(gate._qubitindex)) else: continue lines.append("") lines.append("\\end{yquant}") return "\n".join(lines)
def example(): circ= CliffordCircuit(3) circ.error_rate=0.1 circ.add_hadamard(0) circ.add_cnot(0,1) circ.add_cnot(0,2) circ.add_measurement(1) circ.add_measurement(2) #Convert scaler circuit to stim circuit stimcirc=circ.stimcircuit print(stimcirc) #print(circ)
[docs] def example(): circ= CliffordCircuit(3) circ.set_error_rate(0.1) circ.add_depolarize(0) circ.add_hadamard(0) circ.add_depolarize(0) circ.add_depolarize(1) circ.add_cnot(0,1) circ.add_cnot(0,2) circ.add_depolarize(1) circ.add_measurement(1) circ.add_measurement(2) #Convert scaler circuit to stim circuit stimcirc=circ.get_stim_circuit() print(stimcirc)
#print(circ) if __name__ == "__main__": example()