From 1d4ed796e0ac9d513e1ec0d201c4af8ea77cc81c Mon Sep 17 00:00:00 2001
From: Valentin Bruch <valentin.bruch@rwth-aachen.de>
Date: Fri, 24 Jun 2022 11:10:12 +0200
Subject: [PATCH] surface 17 dephasing: adapted for SS22

---
 exercises/surface17_exercise.py           | 130 +++++++++++++---------
 exercises/surface17_optimized_exercise.py | 111 ++++++++++++++++++
 exercises/surface17_optimized_solution.py | 111 ++++++++++++++++++
 exercises/surface17_solution.py           | 130 ++++++++++++----------
 4 files changed, 372 insertions(+), 110 deletions(-)
 create mode 100644 exercises/surface17_optimized_exercise.py
 create mode 100644 exercises/surface17_optimized_solution.py

diff --git a/exercises/surface17_exercise.py b/exercises/surface17_exercise.py
index bbbf8cc..723e7df 100644
--- a/exercises/surface17_exercise.py
+++ b/exercises/surface17_exercise.py
@@ -2,100 +2,124 @@
 import numpy as np
 import matplotlib.pyplot as plt
 
-# logical X in symplectic notation
-xL = 0b001010100
+# We need to define for logical operators and for stabilizers the set qubits on
+# which they act.
+# This can be done using binary representation of an integer, in which the last
+# bit indicates the physical qubit D1, and so on. For example, a logical
+# X operator that acts on qubits D1, D4, and D7 is defined as:
+xL = 0b001001001
+
 # X stabilizer generators in symplectic notation
-s1 = 0b000011011
-s2 = 0b000100100
-s3 = 0b001001000
-s4 = 0b110110000
-stabilizerGenerators = [s1,s2,s3,s4]
+# qubit:987654321
+s1 =  0b000000110
+s2 =  0b000011011
+s3 =  0b110110000
+s4 =  0b011000000
+stabilizerGenerators = [s1, s2, s3, s4]
+
+def getCommutator(a, b):
+	"""
+	Check whether two operators made of Pauli operators commute or anticommute.
+	a consists of Pauli X operators on a subset of the data qubits, b consists
+	of Pauli Z operators on a different subset of data qubits. The data qubits
+	are defined by the bits in the integers a and b.
+	E.g. a=0b0101 has Pauli X operators on data qubits 1 and 3.
+	Return 0 if a and b commute and 1 otherwise.
+	"""
+	# Because single qubit Paulis anticommute we just sum up (mod 2) the mutual
+	# ones in the bitstings that represent the multiqubit operators in
+	# symplectic notation
 
-def getCommutator(a,b):
-	# For two multiqubit Pauli X- or Z- operators we want to know
-	# whether they commute (0) or anticommute (1). Because single qubit Paulis
-	# anticommute we just sum up (mod 2) the mutual ones in the bitstings 
-	# that represent the multiqubit operators in symplectic notation
+	# In python 3.10 you can uncomment the following line to make the code run faster
+	#return (a & b).bit_count() % 2
 	return bin(a & b)[2:].count('1')%2
 
 def getSyndrome(configuration):
-	# commutation/anticommutation with a stabilizer is marked as 0/1 in syndrome
+	"""
+	Given a configuration of phase flip errors on the data qubits (defined by
+	the binary representation of the integer "configuration"), compute the
+	syndrome.
+	Commutation/anticommutation with stabilizer n is marked as 0/1 in bit n
+	of the syndrome. E.g. if an error anticommutes with stabilizers 1 and 3,
+	the syndrome will be 0b0101.
+	"""
 	syndrome = 0
-	for i in range(len(stabilizerGenerators)):
-		syndrome += getCommutator(stabilizerGenerators[i], configuration)*2**i
+	for i, s in enumerate(stabilizerGenerators):
+		syndrome += getCommutator(s, configuration)*2**i
 	return syndrome
 
 def getCorrection(syndrome):
+	"""
+	Given a syndrome, return the correction. The nth bit of the correction
+	is 1 if data qubit n should obtain a phase flip in the correction, and 0 if
+	data qubit n should not be altered.
+	"""
 	# Look Up Table: apply minimal weight correction based on syndrome information
 	correction = []
 	bin_correction = 0
-	if syndrome == 0b0001: correction = [0]
-	if syndrome == 0b1110: correction = [5,6]
-	if syndrome == 0b0010: correction = [2]
-	if syndrome == 0b1000: correction = [8]
-	if syndrome == 0b0100: correction = [6]
+	if syndrome == 0: return 0
 	# TASK: add the correct syndromes and corrections
-	if syndrome == ...: correction = [3]
-	if syndrome == ...: correction = [2,4]
-	if syndrome == 0b1101: correction = ...
+	if syndrome == 0b0001: correction = [3]
+	if syndrome == 0b0010: correction = [1]
+	if syndrome == 0b0011: correction = [2]
+	if syndrome == 0b0100: correction = [9]
+	if syndrome == 0b0101: correction = [3,9]
+	if syndrome == 0b0110: correction = ...
+	if syndrome == 0b0111: correction = [2,9]
+	if syndrome == 0b1000: correction = ...
 	if syndrome == 0b1001: correction = ...
-	if syndrome == 0b1010: correction = [5]
-	if syndrome == 0b1100: correction = [6,8]
-	if syndrome == 0b0111: correction = [5,6]
-	if syndrome == 0b0011: correction = [0,2]
-	if syndrome == 0b0110: correction = [2,6]
-	if syndrome == 0b1111: correction = [3,5]
+	if syndrome == 0b1010: correction = [1,7]
+	if syndrome == 0b1011: correction = [2,7]
+	if syndrome == 0b1100: correction = ...
+	if syndrome == 0b1101: correction = [3,8]
+	if syndrome == 0b1110: correction = [1,8]
+	if syndrome == 0b1111: correction = [2,8]
 	for i in correction:
-		bin_correction += 2**i
+		bin_correction += 2**(i-1)
 	return bin_correction
 
-# set range of physical error rates to MC sample
-pRange = np.logspace(-3,0,7)
+# set range of physical error rates to montecarlo sample
+pRange = np.logspace(-3, 0, 7)
 
 fail_log = []
 logical_failure_rate = []
-for p in range(len(pRange)):
-	
+for p in pRange:
 	fail_log.append([])
 	# sample a reasonable amount of times given the probability of errors
-	for _ in range(int(1000/pRange[p])):
-		
+	for _ in range(int(1000/p)):
+
 		# start out with clean state and randomly place Z-errors on data qubits
 		configuration = 0
 		for i in range(9):
-			if np.random.random() < pRange[p]: configuration += 2**i
-		
+			if np.random.random() < p:
+				configuration += 2**i
+
 		# TASK: add syndrome readout and apply correction operators according to look up table
-		syndrome = ... 
-		correction = ... 
-		after_correction = configuration^correction
+		syndrome = ...
+		correction = ...
+		after_correction = configuration ^ correction
 
 		# TASK: check if the resulting configuration has caused a logical Z error
 		if ... :
 			fail_log[-1].append(0)
 		else:
 			fail_log[-1].append(1)
-		
+
+		# You can uncomment these lines to debug your code
 		#print('configuration', format(configuration,'09b'))
 		#print('syndrome', format(syndrome,'04b'))
 		#print('correction', format(correction,'09b'))
 		#print('result', format(after_correction,'09b'))
 		#print('logical X', getCommutator(after_correction, xL))
-	
-	# calculate logical failure rate from samples
-	logical_failure_rate.append(np.sum(fail_log[-1])/len(fail_log[-1]))
 
-#print('physical error rates', pRange)
-#print('failure', logical_failure_rate)
-
-#plt.rcParams['text.usetex'] = True
-#plt.rcParams.update({'font.size': 18})
+	# calculate logical failure rate from samples
+	logical_failure_rate.append( np.sum(fail_log[-1]) / len(fail_log[-1]) )
 
+# Plot the results
 plt.grid()
 plt.xlabel('$p$')
 plt.ylabel('$p_L$')
 plt.title('Pseudothreshold of Surface-17')
-plt.loglog(pRange,logical_failure_rate,'-x')
-plt.loglog(np.logspace(-3,0,4),np.logspace(-3,0,4),'k:',alpha=0.5)
-plt.tight_layout()
+plt.loglog(pRange, logical_failure_rate, '-x')
+plt.loglog(pRange, pRange, 'k:', alpha=0.5)
 plt.show()
diff --git a/exercises/surface17_optimized_exercise.py b/exercises/surface17_optimized_exercise.py
new file mode 100644
index 0000000..12d92bf
--- /dev/null
+++ b/exercises/surface17_optimized_exercise.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+import numpy as np
+from random import random
+import matplotlib.pyplot as plt
+from numba import jit, prange
+
+# We need to define for logical operators and for stabilizers the set qubits on
+# which they act.
+# This can be done using binary representation of an integer, in which the last
+# bit indicates the physical qubit D1, and so on. For example, a logical
+# X operator that acts on qubits D1, D4, and D7 is defined as:
+xL = 0b001001001
+
+# X stabilizer generators in symplectic notation
+# qubit:987654321
+s1 =  0b000000110
+s2 =  0b000011011
+s3 =  0b110110000
+s4 =  0b011000000
+stabilizerGenerators = (s1, s2, s3, s4)
+
+# Lookup table of number of bits:
+# parity_table[x] = 1 if x has an odd number of ones, 0 otherwise
+# This implies getCommutator(a, b) == parity_table[a & b]
+parity_table = bytes(bin(x).count('1')%2 for x in range(1<<9))
+
+@jit(nopython=True)
+def getSyndrome(configuration):
+    """
+    Given a configuration of phase flip errors on the data qubits (defined by
+    the binary representation of the integer "configuration"), compute the
+    syndrome.
+    Commutation/anticommutation with stabilizer n is marked as 0/1 in bit n
+    of the syndrome. E.g. if an error anticommutes with stabilizers 1 and 3,
+    the syndrome will be 0b0101.
+    """
+    syndrome = 0
+    for i, s in enumerate(stabilizerGenerators):
+        syndrome |= parity_table[s & configuration] << i
+    return syndrome
+
+# Lookup table of corrections:
+# correction_table[syndrome] == getCorrection(syndrome)
+correction_table = (
+        # TASK: add the correct syndromes and corrections
+        0b000000000, # syndmome = 0, correction = []
+        0b000000100, # syndrome = 0b0001, correction = [3]
+        0b000000001, # syndrome = 0b0010, correction = [1]
+        0b000000010, # syndrome = 0b0011, correction = [2]
+        0b100000000, # syndrome = 0b0100, correction = [9]
+        0b100000100, # syndrome = 0b0101, correction = [3,9]
+        ...        , # syndrome = 0b0110, correction = ...
+        0b100000010, # syndrome = 0b0111, correction = [2,9]
+        ...        , # syndrome = 0b1000, correction = ...
+        ...        , # syndrome = 0b1001, correction = ...
+        0b001000001, # syndrome = 0b1010, correction = [1,7]
+        0b001000010, # syndrome = 0b1011, correction = [2,7]
+        ...        , # syndrome = 0b1100, correction = ...
+        0b010000100, # syndrome = 0b1101, correction = [3,8]
+        0b010000001, # syndrome = 0b1110, correction = [1,8]
+        0b010000010, # syndrome = 0b1111, correction = [2,8]
+    )
+
+
+@jit(nopython=True, cache=False, parallel=True)
+def simulate(p, runs):
+    """
+    Simulate n runs with error probability p.
+    Return logical failure rate.
+    """
+    failures = 0
+    # sample a reasonable amount of times given the probability of errors
+    for _ in prange(runs):
+
+        # start out with clean state and randomly place Z-errors on data qubits
+        configuration = 0
+        for i in range(9):
+            if random() < p:
+                configuration |= 1<<i
+
+        # TASK: add syndrome readout and apply correction operators according to look up table
+        syndrome = ...
+        correction = ...
+        after_correction = configuration ^ correction
+
+        # TASK: check if the resulting configuration has caused a logical Z error
+        if ... :
+            failures += 1
+
+    # calculate logical failure rate from samples
+    return failures / runs
+
+def main():
+    # set range of physical error rates to MC sample
+    pRange = np.logspace(-3, 0, 21)
+
+    # Run the simulation
+    logical_failure_rate = [simulate(p, int(5000/p)) for p in pRange]
+
+    # Plot the results
+    plt.grid()
+    plt.xlabel('$p$')
+    plt.ylabel('$p_L$')
+    plt.title('Pseudothreshold of Surface-17')
+    plt.loglog(pRange, logical_failure_rate, '-x')
+    plt.loglog(pRange, pRange, 'k:', alpha=0.5)
+    plt.show()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/exercises/surface17_optimized_solution.py b/exercises/surface17_optimized_solution.py
new file mode 100644
index 0000000..171b81f
--- /dev/null
+++ b/exercises/surface17_optimized_solution.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python3
+import numpy as np
+from random import random
+import matplotlib.pyplot as plt
+from numba import jit, prange
+
+# We need to define for logical operators and for stabilizers the set qubits on
+# which they act.
+# This can be done using binary representation of an integer, in which the last
+# bit indicates the physical qubit D1, and so on. For example, a logical
+# X operator that acts on qubits D1, D4, and D7 is defined as:
+xL = 0b001001001
+
+# X stabilizer generators in symplectic notation
+# qubit:987654321
+s1 =  0b000000110
+s2 =  0b000011011
+s3 =  0b110110000
+s4 =  0b011000000
+stabilizerGenerators = (s1, s2, s3, s4)
+
+# Lookup table of number of bits:
+# parity_table[x] = 1 if x has an odd number of ones, 0 otherwise
+# This implies getCommutator(a, b) == parity_table[a & b]
+parity_table = bytes(bin(x).count('1')%2 for x in range(1<<9))
+
+@jit(nopython=True)
+def getSyndrome(configuration):
+    """
+    Given a configuration of phase flip errors on the data qubits (defined by
+    the binary representation of the integer "configuration"), compute the
+    syndrome.
+    Commutation/anticommutation with stabilizer n is marked as 0/1 in bit n
+    of the syndrome. E.g. if an error anticommutes with stabilizers 1 and 3,
+    the syndrome will be 0b0101.
+    """
+    syndrome = 0
+    for i, s in enumerate(stabilizerGenerators):
+        syndrome |= parity_table[s & configuration] << i
+    return syndrome
+
+# Lookup table of corrections:
+# correction_table[syndrome] == getCorrection(syndrome)
+correction_table = (
+        # TASK: add the correct syndromes and corrections
+        0b000000000, # syndmome = 0, correction = []
+        0b000000100, # syndrome = 0b0001, correction = [3]
+        0b000000001, # syndrome = 0b0010, correction = [1]
+        0b000000010, # syndrome = 0b0011, correction = [2]
+        0b100000000, # syndrome = 0b0100, correction = [9]
+        0b100000100, # syndrome = 0b0101, correction = [3,9]
+        0b000010000, # syndrome = 0b0110, correction = [5]
+        0b100000010, # syndrome = 0b0111, correction = [2,9]
+        0b001000000, # syndrome = 0b1000, correction = [7]
+        0b001000100, # syndrome = 0b1001, correction = [3,7]
+        0b001000001, # syndrome = 0b1010, correction = [1,7]
+        0b001000010, # syndrome = 0b1011, correction = [2,7]
+        0b010000000, # syndrome = 0b1100, correction = [8]
+        0b010000100, # syndrome = 0b1101, correction = [3,8]
+        0b010000001, # syndrome = 0b1110, correction = [1,8]
+        0b010000010, # syndrome = 0b1111, correction = [2,8]
+    )
+
+
+@jit(nopython=True, cache=False, parallel=True)
+def simulate(p, runs):
+    """
+    Simulate n runs with error probability p.
+    Return logical failure rate.
+    """
+    failures = 0
+    # sample a reasonable amount of times given the probability of errors
+    for _ in prange(runs):
+
+        # start out with clean state and randomly place Z-errors on data qubits
+        configuration = 0
+        for i in range(9):
+            if random() < p:
+                configuration |= 1<<i
+
+        # TASK: add syndrome readout and apply correction operators according to look up table
+        syndrome = getSyndrome(configuration)
+        correction = correction_table[syndrome]
+        after_correction = configuration ^ correction
+
+        # TASK: check if the resulting configuration has caused a logical Z error
+        if parity_table[after_correction & xL]:
+            failures += 1
+
+    # calculate logical failure rate from samples
+    return failures / runs
+
+def main():
+    # set range of physical error rates to MC sample
+    pRange = np.logspace(-3, 0, 21)
+
+    # Run the simulation
+    logical_failure_rate = [simulate(p, int(5000/p)) for p in pRange]
+
+    # Plot the results
+    plt.grid()
+    plt.xlabel('$p$')
+    plt.ylabel('$p_L$')
+    plt.title('Pseudothreshold of Surface-17')
+    plt.loglog(pRange, logical_failure_rate, '-x')
+    plt.loglog(pRange, pRange, 'k:', alpha=0.5)
+    plt.show()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/exercises/surface17_solution.py b/exercises/surface17_solution.py
index fc3c5ef..a7f1527 100644
--- a/exercises/surface17_solution.py
+++ b/exercises/surface17_solution.py
@@ -1,83 +1,105 @@
 #!/usr/bin/env python3
-'''
-Solution for exercise 13.1f, Monte Carlo simulation of surface-17 error
-quantum error correction for dephasing noise.
-A more efficient implementation of this can be found in the solution for
-depolarizing noise.
-'''
 import numpy as np
 import matplotlib.pyplot as plt
 
-# logical X in symplectic notation
-xL = 0b001010100
+# We need to define for logical operators and for stabilizers the set qubits on
+# which they act.
+# This can be done using binary representation of an integer, in which the last
+# bit indicates the physical qubit D1, and so on. For example, a logical
+# X operator that acts on qubits D1, D4, and D7 is defined as:
+xL = 0b001001001
+
 # X stabilizer generators in symplectic notation
-s1 = 0b000011011
-s2 = 0b000100100
-s3 = 0b001001000
-s4 = 0b110110000
-stabilizerGenerators = [s1,s2,s3,s4]
+# qubit:987654321
+s1 =  0b000000110
+s2 =  0b000011011
+s3 =  0b110110000
+s4 =  0b011000000
+stabilizerGenerators = [s1, s2, s3, s4]
+
+def getCommutator(a, b):
+	"""
+	Check whether two operators made of Pauli operators commute or anticommute.
+	a consists of Pauli X operators on a subset of the data qubits, b consists
+	of Pauli Z operators on a different subset of data qubits. The data qubits
+	are defined by the bits in the integers a and b.
+	E.g. a=0b0101 has Pauli X operators on data qubits 1 and 3.
+	Return 0 if a and b commute and 1 otherwise.
+	"""
+	# Because single qubit Paulis anticommute we just sum up (mod 2) the mutual
+	# ones in the bitstings that represent the multiqubit operators in
+	# symplectic notation
 
-def getCommutator(a,b):
-	# For two multiqubit Pauli X- or Z- operators we want to know
-	# whether they commute (0) or anticommute (1). Because single qubit Paulis
-	# anticommute we just sum up (mod 2) the mutual ones in the bitstings
-	# that represent the multiqubit operators in symplectic notation
+	# In python 3.10 you can uncomment the following line to make the code run faster
+	#return (a & b).bit_count() % 2
 	return bin(a & b)[2:].count('1')%2
 
 def getSyndrome(configuration):
-	# commutation/anticommutation with a stabilizer is marked as 0/1 in syndrome
+	"""
+	Given a configuration of phase flip errors on the data qubits (defined by
+	the binary representation of the integer "configuration"), compute the
+	syndrome.
+	Commutation/anticommutation with stabilizer n is marked as 0/1 in bit n
+	of the syndrome. E.g. if an error anticommutes with stabilizers 1 and 3,
+	the syndrome will be 0b0101.
+	"""
 	syndrome = 0
-	for i in range(len(stabilizerGenerators)):
-		syndrome += getCommutator(stabilizerGenerators[i], configuration)*2**i
+	for i, s in enumerate(stabilizerGenerators):
+		syndrome += getCommutator(s, configuration)*2**i
 	return syndrome
 
 def getCorrection(syndrome):
+	"""
+	Given a syndrome, return the correction. The nth bit of the correction
+	is 1 if data qubit n should obtain a phase flip in the correction, and 0 if
+	data qubit n should not be altered.
+	"""
 	# Look Up Table: apply minimal weight correction based on syndrome information
 	correction = []
 	bin_correction = 0
-	if syndrome == 0b0001: correction = [0] # could also be [1]
-	if syndrome == 0b1110: correction = [5,6]
-	if syndrome == 0b0010: correction = [2]
-	if syndrome == 0b1000: correction = [8] # could also be [7]
-	if syndrome == 0b0100: correction = [6]
-	if syndrome == 0b0101: correction = [3]
-	if syndrome == 0b1011: correction = [2,4]
-	if syndrome == 0b1101: correction = [4,6] # could also be [3,8]
-	if syndrome == 0b1001: correction = [4]
-	if syndrome == 0b1010: correction = [5]
-	if syndrome == 0b1100: correction = [6,8]
-	if syndrome == 0b0111: correction = [5,6]
-	if syndrome == 0b0011: correction = [0,2]
-	if syndrome == 0b0110: correction = [2,6]
-	if syndrome == 0b1111: correction = [3,5]
+	if syndrome == 0: return 0
+	# TASK: add the correct syndromes and corrections
+	if syndrome == 0b0001: correction = [3]
+	if syndrome == 0b0010: correction = [1]
+	if syndrome == 0b0011: correction = [2]
+	if syndrome == 0b0100: correction = [9]
+	if syndrome == 0b0101: correction = [3,9]
+	if syndrome == 0b0110: correction = [5]
+	if syndrome == 0b0111: correction = [2,9]
+	if syndrome == 0b1000: correction = [7]
+	if syndrome == 0b1001: correction = [3,7]
+	if syndrome == 0b1010: correction = [1,7]
+	if syndrome == 0b1011: correction = [2,7]
+	if syndrome == 0b1100: correction = [8]
+	if syndrome == 0b1101: correction = [3,8]
+	if syndrome == 0b1110: correction = [1,8]
+	if syndrome == 0b1111: correction = [2,8]
 	for i in correction:
-		bin_correction += 2**i
+		bin_correction += 2**(i-1)
 	return bin_correction
 
-# set range of physical error rates to MC sample
-pRange = np.logspace(-3,0,7)
+# set range of physical error rates to montecarlo sample
+pRange = np.logspace(-3, 0, 7)
 
 fail_log = []
 logical_failure_rate = []
-for p in range(len(pRange)):
-
+for p in pRange:
 	fail_log.append([])
 	# sample a reasonable amount of times given the probability of errors
-	for _ in range(int(1000/pRange[p])):
+	for _ in range(int(1000/p)):
 
 		# start out with clean state and randomly place Z-errors on data qubits
 		configuration = 0
 		for i in range(9):
-			if np.random.random() < pRange[p]: configuration += 2**i
+			if np.random.random() < p:
+				configuration += 2**i
 
-		# read syndrome and apply correction operators according to look up table
+		# TASK: add syndrome readout and apply correction operators according to look up table
 		syndrome = getSyndrome(configuration)
 		correction = getCorrection(syndrome)
-		after_correction = configuration^correction
+		after_correction = configuration ^ correction
 
-		# check if the resulting configuration has caused a logical Z error
-		# we notice that we induced a logical Z by applying the correction
-		# by checking if the resulting multiqubit Z-operator commutes with logical X
+		# TASK: check if the resulting configuration has caused a logical Z error
 		if getCommutator(after_correction, xL) == 0:
 			fail_log[-1].append(0)
 		else:
@@ -92,17 +114,11 @@ for p in range(len(pRange)):
 	# calculate logical failure rate from samples
 	logical_failure_rate.append(np.sum(fail_log[-1])/len(fail_log[-1]))
 
-#print('physical error rates', pRange)
-#print('failure', logical_failure_rate)
-
-#plt.rcParams['text.usetex'] = True
-#plt.rcParams.update({'font.size': 18})
-
+# Plot the results
 plt.grid()
 plt.xlabel('$p$')
 plt.ylabel('$p_L$')
 plt.title('Pseudothreshold of Surface-17')
-plt.loglog(pRange,logical_failure_rate,'-x')
-plt.loglog(np.logspace(-3,0,4),np.logspace(-3,0,4),'k:',alpha=0.5)
-plt.tight_layout()
+plt.loglog(pRange, logical_failure_rate, '-x')
+plt.loglog(pRange, pRange, 'k:', alpha=0.5)
 plt.show()
-- 
GitLab