<jupyter_start><jupyter_text>QC Mentorship program Screening Task 2 The bit-flip code and the sign-flip code (you can find a description of both [here](https://en.wikipedia.org/wiki/Quantum_error_correction)) are two very simple circuits able to detect and fix the bit-flip and the sign-flip errors, respectively.>_Note: I am going to use [Qiskit](https://qiskit.org/) as the Quantum Development Framework_ First ExerciseBuild the following simple circuit to prepare the Bell state:**Code:**<jupyter_code>from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.tools.visualization import *
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_state_qsphere
from qiskit import execute, Aer
from qiskit.extensions import UnitaryGate
# Use Aer's qasm_simulator
simulator = Aer.get_backend('qasm_simulator')
stateVectorSimulator = Aer.get_backend('statevector_simulator')

# Create 2 qbits circuit
circuitExerciseOne = QuantumCircuit(2)
# By default circuit start with |0>, but we can also explicitely reset it with |0> state
circuitExerciseOne.reset(0)
circuitExerciseOne.reset(1)
# Add a H gate on qubit 0
circuitExerciseOne.h(0)
# Add a CX (CNOT) gate on control qubit 0 and target qubit 1
circuitExerciseOne.cx(0, 1)
# Draw the circuit
circuitExerciseOne.draw('mpl')
# Print State Vector from Exercise one circuit: NO error and previous to measurement
statevectorExerciseOne = execute(circuitExerciseOne, backend=stateVectorSimulator).result().get_statevector(circuitExerciseOne)
plot_state_city(statevectorExerciseOne, title='Base Circuit')
plot_state_qsphere(Statevector.from_instruction(circuitExerciseOne))<jupyter_output><empty_output><jupyter_text>Result Analysis$$\newcommand{\ket}[1]{\left|{1}\right\rangle}$$With the plot state city and from the Bloch Qsphere from `Base Circuit` we show that the circuit creates the Bell State:  $\ket{\Phi^+} = \frac{|00>+|11>}{\sqrt{2}}$ .  Second ExerciseNow add, right before the CNOT gate and for each of the two qubits, an arbitrary “error gate”. By error gate we mean that with a certain probability (that you can decide but must be non-zero for all the choices) you have a 1 qubit unitary which can be either the identity, or the X gate (bit-flip error) or the Z gate (sign-flip error).<jupyter_code>def initTwoRandomBits() -> str: 
    """ Creates a random two-bit sequence by the resulting of a one-time execution of a 
        two qubit Quantum Circuit with a Hadamard Gate on each one and performing a Z-measurement.
    """
    # Create 2 qbits circuit with 2 classical registers
    internalCircuit = QuantumCircuit(2,2)
    # Adding Hadamard gates on each qubit
    internalCircuit.h(0)
    internalCircuit.h(1)
    # Map the quantum measurement to the classical bits
    internalCircuit.measure([0,1], [0,1])
    # Execute the circuit on the qasm simulator or just ONE execution
    resultOneExecution = execute(internalCircuit, simulator, shots=1).result()
    # Grab the result in dictionary format
    counts = resultOneExecution.get_counts(internalCircuit)
    # Return the two bit string
    return list(counts.keys())[0]
def randomErrorGate() -> UnitaryGate:
    """ 
        Creates an Unitary Gate, either the identity, X or Z gate with a random probability.
        The probability is computed measuring from a new internal two-qubit circuit qubits, 
        to which it has applied a Hadamard gate to in a superposition state and then to 
        generate an actual random probability. 
        Each qubit is then measured with a Z-measurement and the applied final gate to 
        the input circuit is based on the following output decision table:

        q0 q1 final gate
        -- -- ----------
         0  0  Identity
         0  1  X (bit-flip error)
         1  0  Z (sign-flip error)
         1  1  XZ (both bit & sign-flip error)

         The probability decision table gives a 25% of being the Identity, and each one of the errors
    """
    from qiskit.circuit.library import IGate, XGate, ZGate

    twoRandomBits = initTwoRandomBits()
    if (twoRandomBits == "00"):
        return UnitaryGate(IGate(), label="Error Gate (I)")
    if (twoRandomBits == "01"):
        return UnitaryGate(XGate(), label="Error Gate (X)")
    if (twoRandomBits == "10"):
        return UnitaryGate(ZGate(), label="Error Gate (Z)")
    if (twoRandomBits == "11"):
        circ = QuantumCircuit(1)
        circ.x(0)
        circ.h(0)
        return circ.to_gate(label="Error Gate (XZ)")
    
def createBaseCircuitWithErrorGates() -> QuantumCircuit:
    """ Creates the requested circuit for Exercise 2
        We put it as a function, so it can be recreated many times with different error gates,
        as required in Exercise 4. 
    """
    # Create 2 qbits circuit
    qreg_q = QuantumRegister(2, 'q')
    circuit = QuantumCircuit(qreg_q)
    # By default circuit start with |0>, but we can also explicitely reset it with |0> state
    circuit.reset(0)
    circuit.reset(1)
    # Add a H gate on qubit 0
    circuit.h(0)
    # Add an error gate on each qubit
    circuit.append(randomErrorGate(), [qreg_q[0]])
    circuit.append(randomErrorGate(), [qreg_q[1]])
    # Add a CX (CNOT) gate on control qubit 0 and target qubit 1
    circuit.cx(0, 1)
    return circuit<jupyter_output><empty_output><jupyter_text>Plotting the circuit several times to show that the circuit generate contains different Error Gate.<jupyter_code># Draw the circuit
circuitExerciseTwo = createBaseCircuitWithErrorGates()
display(circuitExerciseTwo.draw('mpl'))
display(createBaseCircuitWithErrorGates().draw('mpl'))
display(createBaseCircuitWithErrorGates().draw('mpl'))
display(createBaseCircuitWithErrorGates().draw('mpl'))
# Print State Vector from Exercise two circuit: WITH error and previous to measurement
statevectorExerciseTwo = execute(circuitExerciseTwo, backend=stateVectorSimulator).result().get_statevector(circuitExerciseTwo)
plot_state_city(statevectorExerciseTwo, title='Base Circuit with Random Gate Error')
plot_state_qsphere(Statevector.from_instruction(circuitExerciseTwo))<jupyter_output><empty_output><jupyter_text>Result AnalysisWith the plot state city and from the Bloch Sphere from `Base Circuit with Random Gate Error` we show that the circuit has altered the Bell State $\ket{\Phi^+}$ expected output and differs from previous results.> Note: we have selected one circuit that does introduce error. As the gate creation is random, some combinations do not add an actual error to the Bell State.  Third ExerciseEncode each of the two qubits with a sign-flip or a bit-flip code, in such a way that **all the possible** choices for the error gates described in 2), occurring on the logical qubits, can be detected and fixed. Motivate your choice. This is the most non-trivial part of the problem, so do it with a lot of care!  Proposed solutionTo solve this part, I am going to use `The Shor code` to fix both possible phase & bit-flip errors on each of the two logical qbits being $\phi$ and $\psi$ in this code. The steps to do so are as follows:1. Define a function that creates a 9 qubit circuit that contains the random Gate Error generated on Exercise 2 applied to the first qubit, corresponding to the one that we want to apply the error correction2. Define a function that creates a 9 qubit circuit that implements the Shor code and includes the random Gate Error3. Define a function that creates the final circuit required for Exercise 3, which implements a Bell State circuit adding a Random Error Gate for each logical qubit and applies error correction4. Create the Exercise 3 circuit and plot two versions of the circuit: a circuit with opaque gates and the same one with gates decomposed (just one level)<jupyter_code>def globalErrorGateCircuit() -> QuantumCircuit:
    """ set a 9-qubit circuit with a random Error Gate applied to the first qubit """
    errorCircuit = QuantumCircuit(9, name='  Error(Phi) ')
    errorCircuit.append(randomErrorGate(), [0])
    return errorCircuit
def shorCodeCircuitWithRandomErrorGate() -> QuantumCircuit:
    """  set a 9-qbuit circuit implementing the Shor Code that fixes both bit-flip and sign-flip errors.
         The Shor code can correct arbitrary 1-qubit error, which will be always applied to the phi0 qubit
         with the random error gate, created on the previous exercise. """
    shorCode = QuantumCircuit(9, name='Shor code with Error gate')
    shorCode.cx(0, 3)
    shorCode.cx(0, 6)
    shorCode.h(0)
    shorCode.h(3)
    shorCode.h(6)
    shorCode.cx(0, 1)
    shorCode.cx(0, 2)
    shorCode.cx(3, 4)
    shorCode.cx(3, 5)
    shorCode.cx(6, 7)
    shorCode.cx(6, 8)
    shorCode.append(globalErrorGateCircuit(), range(9))
    shorCode.cx(0, 1)
    shorCode.cx(0, 2)
    shorCode.cx(3, 4)
    shorCode.cx(3, 5)
    shorCode.cx(6, 7)
    shorCode.cx(6, 8)
    shorCode.ccx(2,1,0)
    shorCode.ccx(5,4,3)
    shorCode.ccx(8,7,6)
    shorCode.h(0)
    shorCode.h(3)
    shorCode.h(6)
    shorCode.cx(0, 3)
    shorCode.cx(0, 6)
    shorCode.ccx(6,3,0)

    return shorCode
# Printing the Shor code circuit with a random Error Gate
shorCodeCircuitWithRandomErrorGate().draw('mpl')<jupyter_output><empty_output><jupyter_text>Writing the final circuit The circuit acts on the 2 logical qubits ($\phi$ and $\psi$), which for each one we add 8 additional qubits that implement the Shor code to correct both bit-flip and sign-flip, resulting into a 18 qubit circuit. The initial circuit gates (Hadamard and CNOT) will be applied only to the logical qubits $\phi$ and $\psi$.<jupyter_code>def createBaseCircuitWithErrorCorrection() -> QuantumCircuit:
    phi_qreg_q = QuantumRegister(1, 'phi')
    codePhi_qreg_q = QuantumRegister(8, 'codePhi')
    psi_qreg_q = QuantumRegister(1, 'psi')
    codePsi_qreg_q = QuantumRegister(8, 'codePsi')
    circuit = QuantumCircuit(phi_qreg_q)
    circuit.add_register(codePhi_qreg_q)
    circuit.add_register(psi_qreg_q)
    circuit.add_register(codePsi_qreg_q)
    # By default circuit start with |0>, but we can also explicitely reset it with |0> state
    circuit.reset(0)
    circuit.reset(9)
    # Add a H gate on qubit phi
    circuit.h(0)
    # Add shor code with random error Gate to both logical qbutis phi and psi
    circuit.append(shorCodeCircuitWithRandomErrorGate(), range(9))
    circuit.append(shorCodeCircuitWithRandomErrorGate(), range(9,18))
    # Add a CX (CNOT) gate on control qubit phi and target qubit psi
    circuit.cx(0, 9)
    return circuit

# Draw the circuit
circuitExerciseThree = createBaseCircuitWithErrorCorrection()
circuitExerciseThree.draw('mpl')
# Printing the decomposed circuit to understand the inner gates
circuitExerciseThree.decompose().draw('mpl')<jupyter_output><empty_output><jupyter_text>Fourth ExerciseTest your solution by making many measurements over the final state and testing that the results are in line with the expectations. For testing the coded solution, we will be using IBMQ qasm simulator, as it can run our 18-qubit circuit. The steps to test is as follows:1. Define a function to add measurements to the second and third exercise circuits, a circuit that introduces a random error gate and a circuit that uses the Shor code to correct the possible errors respectively.2. Add measurements to the initial base circuit that creates the Bell State $\ket{\Phi^+}$3. Display those 3 circuits4. Run 1024 simulations on each of the 500 generated circuits so we have a high probability that we test all possible error gate combinations and check that our final circuit detects and corrects all of them, and also on the base circuit5. Define a function to filter the different outcomes found on all simulation for each circuit type, returning a list of the average probabilites found for each bitstring outcome: `00`, `01`, `10` and `11`.6. Plot an histogram with the different results measured on each circuit. We expect to find only `00` or `11` for the Base circuit (Bell State), to find all bitstrings for the circuit with Random Gate Error, and only `00` and `11` results for the circuit with error correction, proofing that our circuit successfully fixes all detected errors.<jupyter_code># using ibm_qasm_simulator that simulates 32 bits to run it on the cloud server rather than locally
from qiskit import IBMQ
import time
import numpy as np

provider = IBMQ.load_account()
chip_name = 'ibmq_qasm_simulator'
backendIBMQSimulator = provider.get_backend(chip_name)
def addMeasureGatesToCircuits(baseCircuitWithErrorGate, baseCircuitWithErrorCorrection):
    baseCircuitWithErrorGateMeasure = QuantumCircuit.copy(baseCircuitWithErrorGate)
    baseCircuitWithErrorGateMeasure.measure_all()
    # Add measure gate to the logical qubits phi and psi
    classicalRegisters = ClassicalRegister(2, 'c')
    baseCircuitWithErrorCorrectionMeasure = QuantumCircuit.copy(baseCircuitWithErrorCorrection)
    baseCircuitWithErrorCorrectionMeasure.add_register(classicalRegisters)
    baseCircuitWithErrorCorrectionMeasure.measure([0,9], classicalRegisters)

    return baseCircuitWithErrorGateMeasure, baseCircuitWithErrorCorrectionMeasure
# Add measurement to Base Circuit
baseCircuitMeasure = QuantumCircuit.copy(circuitExerciseOne)
baseCircuitMeasure.measure_all()
# Add measurement to Base Circuit with Error Gate and Base Circuit with Error Correction
baseCircuitWithErrorGateMeasure, baseCircuitWithErrorCorrectionMeasure = addMeasureGatesToCircuits(circuitExerciseTwo, circuitExerciseThree)
# Printing circuits
display(baseCircuitMeasure.draw('mpl'))
display(baseCircuitWithErrorGateMeasure.draw('mpl'))
display(baseCircuitWithErrorCorrectionMeasure.draw('mpl'))
totalCountsBaseCircuit=[]
totalCountsBaseCircuitWithErrorGate=[]
totalCountsBaseCircuitWithErrorCorrection=[]

numberMeasurements = 500

program_start_time = time.time()
print("Starting the execution")
for indexMeasurement in range(numberMeasurements):
    start_time = time.time()
    if (indexMeasurement % 10 == 0):
        print("Entering to Measurement nº: ", indexMeasurement)
    baseCircuitWithErrorGateMeasure, baseCircuitWithErrorCorrectionMeasure = addMeasureGatesToCircuits(
        baseCircuitWithErrorGate = createBaseCircuitWithErrorGates(), 
        baseCircuitWithErrorCorrection = createBaseCircuitWithErrorCorrection() )
    countsExerciseOne = execute(baseCircuitMeasure, backend=simulator, shots=1024).result().get_counts()
    countsBaseCircuitWithErrorGate = execute(baseCircuitWithErrorGateMeasure, backend=simulator, shots=1024).result().get_counts()
    countsBaseCircuitWithErrorCorrection = execute(baseCircuitWithErrorCorrectionMeasure, backend=backendIBMQSimulator, shots=1024).result().get_counts()
    totalCountsBaseCircuit.append(countsExerciseOne)
    totalCountsBaseCircuitWithErrorGate.append(countsBaseCircuitWithErrorGate)
    totalCountsBaseCircuitWithErrorCorrection.append(countsBaseCircuitWithErrorCorrection)
    end_time = time.time()
    if (indexMeasurement % 10 == 0):
        print("total minutes taken so far: ", np.round((end_time - program_start_time)/60))

print("Total minutes to compute all measurements: ", np.round((end_time - program_start_time)/60))
def filterUniqueResults(simulationResultCounts):
    """ filter the simulation results finding the different bitstring found and averaging the counts for each bitstring """

    uniqueCounts = {
        "00": {
            "totalCounts": 0,
            "nOccurrences": 0
        },
        "01": {
            "totalCounts": 0,
            "nOccurrences": 0
        },
        "10": {
            "totalCounts": 0,
            "nOccurrences": 0
        },
        "11": {
            "totalCounts": 0,
            "nOccurrences": 0
        }
    }
    # loop through all simulations
    for singleSimulationResultCounts in simulationResultCounts:
        # for each result add the counts found and increase its occurrence
        for bitsResult, counts in singleSimulationResultCounts.items():
            uniqueCounts[bitsResult]["totalCounts"] += counts
            uniqueCounts[bitsResult]["nOccurrences"] += 1
    
    result = {
        "00": 0,
        "01": 0,
        "10": 0,
        "11": 0
    }
    # compute the average counts
    for bitsResult, counts in uniqueCounts.items():
        if uniqueCounts[bitsResult]["nOccurrences"] != 0:
            result[bitsResult] = np.round(uniqueCounts[bitsResult]["totalCounts"]/uniqueCounts[bitsResult]["nOccurrences"])
        
    return result
plot_histogram([filterUniqueResults(totalCountsBaseCircuit), filterUniqueResults(totalCountsBaseCircuitWithErrorGate), filterUniqueResults(totalCountsBaseCircuitWithErrorCorrection)], title='Probabilites for the unique values measured on each circuit', legend=["Base (no error)", "With Random Error Gate", "With Error Correction"])<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0788534728829686, 'fraction_numerical': 0.014926260704091341, 'mean_word_length': 4.428340865074241, 'pattern_counts': {'":': 16, '<': 25, '<?xml version=': 0, '>': 41, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '537135', 'license': 'MIT', 'n_tokens_mistral': 5072, 'n_tokens_neox': 4677, 'n_words': 1713, 'path': 'qosf-error-correction-task.ipynb', 'repo_name': 'iamtxena/qc-mentorship-program', 'size': '16816'}