@@ -0,0 +1,229 @@ | |||||
""" | |||||
This program computes lookup tables and stores them as tables.py and tables.js | |||||
# TODO: clifford naming discrepancy | |||||
""" | |||||
import numpy as np | |||||
from tqdm import tqdm | |||||
import itertools as it | |||||
from functools import reduce | |||||
from os.path import dirname, join, split | |||||
import json | |||||
import qi, clifford | |||||
DECOMPOSITIONS = ( | |||||
"xxxx", "xx", "zzxx", "zz", "zxx", "z", "zzz", "xxz", "xzx", "xzxxx", "xzzzx", | |||||
"xxxzx", "xzz", "zzx", "xxx", "x", "zzzx", "xxzx", "zx", "zxxx", "xxxz", "xzzz", "xz", "xzxx") | |||||
JS_TEMPLATE = """\ | |||||
var tables = {{ | |||||
decompositions : {decompositions}, | |||||
conjugation_table : {conjugation_table}, | |||||
times_table : {times_table}, | |||||
cz_table : {cz_table}, | |||||
clifford : {by_name}, | |||||
measurement_table_real : {measurement_table_real}, | |||||
measurement_table_imag : {measurement_table_imag} | |||||
}}; | |||||
""" | |||||
PY_TEMPLATE = """\ | |||||
import numpy as np | |||||
# Define lookup tables | |||||
ir2 = 1/np.sqrt(2) | |||||
decompositions = {decompositions} | |||||
conjugation_table = np.array({conjugation_table}, dtype=int) | |||||
times_table = np.array({times_table}, dtype=int) | |||||
cz_table = np.array({cz_table}, dtype=int) | |||||
by_name = {by_name} | |||||
measurement_table_real = np.array({measurement_table_real}, dtype=complex) | |||||
measurement_table_imag = np.array({measurement_table_imag}, dtype=complex) | |||||
unitaries_real = np.array({unitaries_real}, dtype=complex) | |||||
unitaries_imag = np.array({unitaries_imag}, dtype=complex) | |||||
# Reconstruct | |||||
measurement_table = measurement_table_real + 1j*measurement_table_imag | |||||
unitaries = unitaries_real + 1j*unitaries_imag | |||||
""" | |||||
def find_clifford(needle, haystack): | |||||
""" Find the index of a given u within a list of unitaries, up to a global phase """ | |||||
needle = qi.normalize_global_phase(needle) | |||||
for i, t in enumerate(haystack): | |||||
if np.allclose(t, needle): | |||||
return i | |||||
raise IndexError | |||||
def find_cz(bond, c1, c2, commuters, state_table): | |||||
""" Find the output of a CZ operation """ | |||||
# Figure out the target state | |||||
target = qi.cz.dot(state_table[bond, c1, c2]) | |||||
target = qi.normalize_global_phase(target) | |||||
# Choose the sets to search over | |||||
s1 = commuters if c1 in commuters else xrange(24) | |||||
s2 = commuters if c2 in commuters else xrange(24) | |||||
# Find a match | |||||
for bondp, c1p, c2p in it.product([0, 1], s1, s2): | |||||
if np.allclose(target, state_table[bondp, c1p, c2p]): | |||||
return bondp, c1p, c2p | |||||
# Didn't find anything - this should never happen | |||||
raise IndexError | |||||
def compose_u(decomposition): | |||||
""" Get the unitary representation of a particular decomposition """ | |||||
matrices = ({"x": qi.msqx, "z": qi.sqz}[c] for c in decomposition) | |||||
output = reduce(np.dot, matrices, np.eye(2, dtype=complex)) | |||||
return qi.normalize_global_phase(output) | |||||
def get_unitaries(): | |||||
""" The Clifford group """ | |||||
return [compose_u(d) for d in DECOMPOSITIONS] | |||||
def get_by_name(unitaries): | |||||
""" Get a lookup table of cliffords by name """ | |||||
a = {name: find_clifford(u, unitaries) | |||||
for name, u in qi.by_name.items()} | |||||
a.update({clifford.get_name(i): i for i in range(24)}) | |||||
a.update({i: i for i in range(24)}) | |||||
return a | |||||
def get_conjugation_table(unitaries): | |||||
""" Construct the conjugation table """ | |||||
return np.array([find_clifford(qi.hermitian_conjugate(u), unitaries) for u in unitaries], dtype=int) | |||||
def get_times_table(unitaries): | |||||
""" Construct the times-table """ | |||||
return np.array([[find_clifford(u.dot(v), unitaries) for v in unitaries] | |||||
for u in tqdm(unitaries, desc="Building times-table")], dtype=int) | |||||
def get_state_table(unitaries): | |||||
""" Cache a table of state to speed up a little bit """ | |||||
state_table = np.zeros((2, 24, 24, 4), dtype=complex) | |||||
params = list(it.product([0, 1], range(24), range(24))) | |||||
for bond, i, j in tqdm(params, desc="Building state table"): | |||||
state = qi.bond if bond else qi.nobond | |||||
kp = np.kron(unitaries[i], unitaries[j]) | |||||
state_table[bond, i, j, :] = qi.normalize_global_phase( | |||||
np.dot(kp, state).T) | |||||
return state_table | |||||
def get_measurement_entry(operator, unitary): | |||||
""" | |||||
Any Clifford group unitary will map an operator A in {I, X, Y, Z} | |||||
to an operator B in +-{I, X, Y, Z}. This finds that mapping. | |||||
""" | |||||
matrices = ({"x": qi.msqx, "z": qi.sqz}[c] | |||||
for c in DECOMPOSITIONS[unitary]) | |||||
unitary = reduce(np.dot, matrices, np.eye(2, dtype=complex)) | |||||
operator = qi.operators[operator] | |||||
new_operator = reduce(np.dot, | |||||
(unitary, operator, qi.hermitian_conjugate(unitary))) | |||||
for i, o in enumerate(qi.operators): | |||||
if np.allclose(o, new_operator): | |||||
return i, 1 | |||||
elif np.allclose(o, -new_operator): | |||||
return i, -1 | |||||
raise IndexError | |||||
def get_measurement_table(): | |||||
""" | |||||
Compute a table of transform * operation * transform^dagger | |||||
This is pretty unintelligible right now, we should probably compute the phase from unitaries instead | |||||
""" | |||||
measurement_table = np.zeros((4, 24, 2), dtype=complex) | |||||
for operator, unitary in it.product(range(4), range(24)): | |||||
measurement_table[operator, unitary] = get_measurement_entry( | |||||
operator, unitary) | |||||
return measurement_table | |||||
def get_commuters(unitaries): | |||||
""" Get the indeces of gates which commute with CZ """ | |||||
commuters = (qi.id, qi.pz, qi.ph, qi.hermitian_conjugate(qi.ph)) | |||||
return [find_clifford(u, unitaries) for u in commuters] | |||||
def get_cz_table(unitaries): | |||||
""" Compute the lookup table for the CZ (A&B eq. 9) """ | |||||
# Get a cached state table and a list of gates which commute with CZ | |||||
commuters = get_commuters(unitaries) | |||||
state_table = get_state_table(unitaries) | |||||
# And now build the CZ table | |||||
cz_table = np.zeros((2, 24, 24, 3), dtype=int) | |||||
rows = list( | |||||
it.product([0, 1], it.combinations_with_replacement(range(24), 2))) | |||||
# CZ is symmetric so we only need combinations | |||||
for bond, (c1, c2) in tqdm(rows, desc="Building CZ table"): | |||||
newbond, c1p, c2p = find_cz( | |||||
bond, c1, c2, commuters, state_table) | |||||
cz_table[bond, c1, c2] = [newbond, c1p, c2p] | |||||
cz_table[bond, c2, c1] = [newbond, c2p, c1p] | |||||
return cz_table | |||||
def compute_everything(): | |||||
""" Compute all lookup tables """ | |||||
unitaries = get_unitaries() | |||||
return {"decompositions": DECOMPOSITIONS, | |||||
"unitaries": unitaries, | |||||
"by_name": get_by_name(unitaries), | |||||
"conjugation_table": get_conjugation_table(unitaries), | |||||
"times_table": get_times_table(unitaries), | |||||
"cz_table": get_cz_table(unitaries), | |||||
"measurement_table": get_measurement_table()} | |||||
def human_readable(data): | |||||
""" Format the data """ | |||||
unitaries = np.array(data["unitaries"]) | |||||
return {"decompositions": json.dumps(DECOMPOSITIONS), | |||||
"unitaries_real": json.dumps(unitaries.real.round(5).tolist()).replace("0.70711", "ir2"), | |||||
"unitaries_imag": json.dumps(unitaries.imag.round(5).tolist()).replace("0.70711", "ir2"), | |||||
"conjugation_table": json.dumps(data["conjugation_table"].tolist()), | |||||
"times_table": json.dumps(data["times_table"].tolist()), | |||||
"cz_table": json.dumps(data["cz_table"].tolist()), | |||||
"by_name": json.dumps(data["by_name"]), | |||||
"measurement_table_real": json.dumps(data["measurement_table"].real.tolist()), | |||||
"measurement_table_imag": json.dumps(data["measurement_table"].imag.tolist())} | |||||
def write_python(data): | |||||
""" Write the tables to a python module """ | |||||
path = join(dirname(__file__), "tables.py") | |||||
content = PY_TEMPLATE.format(**data) | |||||
with open(path, "w") as f: | |||||
f.write(content) | |||||
def write_javascript(data): | |||||
""" Write the tables to javascript files for consumption in the browser """ | |||||
path = join(split(dirname(__file__))[0], "static/scripts/tables.js") | |||||
content = JS_TEMPLATE.format(**data) | |||||
with open(path, "w") as f: | |||||
f.write(content) | |||||
if __name__ == '__main__': | |||||
data = compute_everything() | |||||
data = human_readable(data) | |||||
write_python(data) | |||||
write_javascript(data) |
@@ -6,236 +6,23 @@ It provides tables for Clifford group multiplication and conjugation, | |||||
as well as CZ and decompositions of the 2x2 Cliffords. | as well as CZ and decompositions of the 2x2 Cliffords. | ||||
""" | """ | ||||
import os, json, tempfile, json | |||||
from functools import reduce | |||||
import itertools as it | |||||
import numpy as np | |||||
from tqdm import tqdm | |||||
import qi | |||||
decompositions = ("xxxx", "xx", "zzxx", "zz", "zxx", "z", "zzz", "xxz", | |||||
"xzx", "xzxxx", "xzzzx", "xxxzx", "xzz", "zzx", "xxx", "x", | |||||
"zzzx", "xxzx", "zx", "zxxx", "xxxz", "xzzz", "xz", "xzxx") | |||||
from tables import * | |||||
def conjugate(operator, unitary): | def conjugate(operator, unitary): | ||||
""" Returns transform * vop * transform^dagger and a phase in {+1, -1} """ | """ Returns transform * vop * transform^dagger and a phase in {+1, -1} """ | ||||
return measurement_table[operator, unitary] | return measurement_table[operator, unitary] | ||||
def use_old_cz(): | def use_old_cz(): | ||||
""" Use the CZ table from A&B's code """ | """ Use the CZ table from A&B's code """ | ||||
global cz_table | global cz_table | ||||
from anders_cz import cz_table | from anders_cz import cz_table | ||||
def get_name(i): | def get_name(i): | ||||
""" Get the human-readable name of this clifford """ | """ Get the human-readable name of this clifford """ | ||||
return "IXYZ"[i & 0x03] + "ABCDEF"[i / 4] | return "IXYZ"[i & 0x03] + "ABCDEF"[i / 4] | ||||
def find_clifford(needle, haystack): | |||||
""" Find the index of a given u within a list of unitaries, up to a global phase """ | |||||
needle = qi.normalize_global_phase(needle) | |||||
for i, t in enumerate(haystack): | |||||
if np.allclose(t, needle): | |||||
return i | |||||
raise IndexError | |||||
def find_cz(bond, c1, c2, commuters, state_table): | |||||
""" Find the output of a CZ operation """ | |||||
# Figure out the target state | |||||
target = qi.cz.dot(state_table[bond, c1, c2]) | |||||
target = qi.normalize_global_phase(target) | |||||
# Choose the sets to search over | |||||
s1 = commuters if c1 in commuters else xrange(24) | |||||
s2 = commuters if c2 in commuters else xrange(24) | |||||
# Find a match | |||||
for bondp, c1p, c2p in it.product([0, 1], s1, s2): | |||||
if np.allclose(target, state_table[bondp, c1p, c2p]): | |||||
return bondp, c1p, c2p | |||||
# Didn't find anything - this should never happen | |||||
raise IndexError | |||||
def compose_u(decomposition): | |||||
""" Get the unitary representation of a particular decomposition """ | |||||
matrices = ({"x": qi.msqx, "z": qi.sqz}[c] for c in decomposition) | |||||
output = reduce(np.dot, matrices, np.eye(2, dtype=complex)) | |||||
return qi.normalize_global_phase(output) | |||||
def get_unitaries(): | |||||
""" The Clifford group """ | |||||
return [compose_u(d) for d in decompositions] | |||||
def get_by_name(unitaries): | |||||
""" Get a lookup table of cliffords by name """ | |||||
a = {name: find_clifford(u, unitaries) | |||||
for name, u in qi.by_name.items()} | |||||
a.update({get_name(i): i for i in range(24)}) | |||||
a.update({i: i for i in range(24)}) | |||||
return a | |||||
def get_conjugation_table(unitaries): | |||||
""" Construct the conjugation table """ | |||||
return np.array([find_clifford(qi.hermitian_conjugate(u), unitaries) for u in unitaries], dtype=int) | |||||
def get_times_table(unitaries): | |||||
""" Construct the times-table """ | |||||
return np.array([[find_clifford(u.dot(v), unitaries) for v in unitaries] | |||||
for u in tqdm(unitaries, desc="Building times-table")], dtype=int) | |||||
def get_state_table(unitaries): | |||||
""" Cache a table of state to speed up a little bit """ | |||||
state_table = np.zeros((2, 24, 24, 4), dtype=complex) | |||||
params = list(it.product([0, 1], range(24), range(24))) | |||||
for bond, i, j in tqdm(params, desc="Building state table"): | |||||
state = qi.bond if bond else qi.nobond | |||||
kp = np.kron(unitaries[i], unitaries[j]) | |||||
state_table[bond, i, j, :] = qi.normalize_global_phase( | |||||
np.dot(kp, state).T) | |||||
return state_table | |||||
def get_measurement_entry(operator, unitary): | |||||
""" | |||||
Any Clifford group unitary will map an operator A in {I, X, Y, Z} | |||||
to an operator B in +-{I, X, Y, Z}. This finds that mapping. | |||||
""" | |||||
matrices = ({"x": qi.msqx, "z": qi.sqz}[c] | |||||
for c in decompositions[unitary]) | |||||
unitary = reduce(np.dot, matrices, np.eye(2, dtype=complex)) | |||||
operator = qi.operators[operator] | |||||
new_operator = reduce( | |||||
np.dot, (unitary, operator, qi.hermitian_conjugate(unitary))) | |||||
for i, o in enumerate(qi.operators): | |||||
if np.allclose(o, new_operator): | |||||
return i, 1 | |||||
elif np.allclose(o, -new_operator): | |||||
return i, -1 | |||||
raise IndexError | |||||
def get_measurement_table(): | |||||
""" | |||||
Compute a table of transform * operation * transform^dagger | |||||
This is pretty unintelligible right now, we should probably compute the phase from unitaries instead | |||||
""" | |||||
measurement_table = np.zeros((4, 24, 2), dtype=complex) | |||||
for operator, unitary in it.product(range(4), range(24)): | |||||
measurement_table[operator, unitary] = get_measurement_entry( | |||||
operator, unitary) | |||||
return measurement_table | |||||
def get_commuters(unitaries): | |||||
""" Get the indeces of gates which commute with CZ """ | |||||
commuters = (qi.id, qi.pz, qi.ph, qi.hermitian_conjugate(qi.ph)) | |||||
return [find_clifford(u, unitaries) for u in commuters] | |||||
def get_cz_table(unitaries): | |||||
""" Compute the lookup table for the CZ (A&B eq. 9) """ | |||||
# Get a cached state table and a list of gates which commute with CZ | |||||
commuters = get_commuters(unitaries) | |||||
state_table = get_state_table(unitaries) | |||||
# And now build the CZ table | |||||
cz_table = np.zeros((2, 24, 24, 3), dtype=int) | |||||
rows = list( | |||||
it.product([0, 1], it.combinations_with_replacement(range(24), 2))) | |||||
# CZ is symmetric so we only need combinations | |||||
for bond, (c1, c2) in tqdm(rows, desc="Building CZ table"): | |||||
newbond, c1p, c2p = find_cz( | |||||
bond, c1, c2, commuters, state_table) | |||||
cz_table[bond, c1, c2] = [newbond, c1p, c2p] | |||||
cz_table[bond, c2, c1] = [newbond, c2p, c1p] | |||||
return cz_table | |||||
def write_javascript_tables(): | |||||
""" Write the tables to javascript files for consumption in the browser """ | |||||
path = os.path.dirname(__file__) | |||||
path = os.path.split(path)[0] | |||||
with open(os.path.join(path, "static/scripts/tables.js"), "w") as f: | |||||
f.write("var tables = {\n") | |||||
f.write("\tdecompositions : {},\n" | |||||
.format(json.dumps(decompositions))) | |||||
f.write("\tconjugation_table : {},\n" | |||||
.format(json.dumps(conjugation_table.tolist()))) | |||||
f.write("\ttimes_table : {},\n" | |||||
.format(json.dumps(times_table.tolist()))) | |||||
f.write("\tcz_table : {},\n" | |||||
.format(json.dumps(cz_table.tolist()))) | |||||
f.write("\tclifford : {}\n" | |||||
.format(json.dumps(by_name))) | |||||
f.write("};") | |||||
def temp(filename): | |||||
""" Get a temporary path """ | |||||
# TODO: this STILL fucking fails sometimes. WHY | |||||
tempdir = tempfile.gettempdir() | |||||
return os.path.join(tempdir, filename) | |||||
def compute_everything(): | |||||
""" Compute all lookup tables """ | |||||
global unitaries, by_name, conjugation_table, times_table, cz_table, measurement_table | |||||
unitaries = get_unitaries() | |||||
by_name = get_by_name(unitaries) | |||||
conjugation_table = get_conjugation_table(unitaries) | |||||
times_table = get_times_table(unitaries) | |||||
cz_table = get_cz_table(unitaries) | |||||
measurement_table = get_measurement_table() | |||||
def save_to_disk(): | |||||
""" Save all tables to disk """ | |||||
global unitaries, by_name, conjugation_table, times_table, cz_table, measurement_table | |||||
np.save(temp("unitaries.npy"), unitaries) | |||||
np.save(temp("conjugation_table.npy"), conjugation_table) | |||||
np.save(temp("times_table.npy"), times_table) | |||||
np.save(temp("cz_table.npy"), cz_table) | |||||
np.save(temp("measurement_table.npy"), measurement_table) | |||||
write_javascript_tables() | |||||
with open(temp("by_name.json"), "wb") as f: | |||||
json.dump(by_name, f) | |||||
def load_from_disk(): | |||||
""" Load all the tables from disk """ | |||||
global unitaries, by_name, conjugation_table, times_table, cz_table, measurement_table | |||||
unitaries = np.load(temp("unitaries.npy")) | |||||
conjugation_table = np.load(temp("conjugation_table.npy")) | |||||
times_table = np.load(temp("times_table.npy")) | |||||
measurement_table = np.load(temp("measurement_table.npy")) | |||||
cz_table = np.load(temp("cz_table.npy")) | |||||
with open(temp("by_name.json")) as f: | |||||
by_name = json.load(f) | |||||
def is_diagonal(v): | def is_diagonal(v): | ||||
""" TODO: remove this. Checks if a VOP is diagonal or not """ | """ TODO: remove this. Checks if a VOP is diagonal or not """ | ||||
return v in {0, 3, 5, 6} | return v in {0, 3, 5, 6} | ||||
if __name__ == "__main__": | |||||
compute_everything() | |||||
save_to_disk() | |||||
else: | |||||
try: | |||||
load_from_disk() | |||||
except IOError: | |||||
compute_everything() | |||||
save_to_disk() |
@@ -101,7 +101,7 @@ def test_with_cphase_gates_hadamard_only(N=10): | |||||
assert_equal(a, b) | assert_equal(a, b) | ||||
def _test_cz_hadamard(N=3): | |||||
def test_cz_hadamard(N=3): | |||||
""" Test CZs and Hadamards at random """ | """ Test CZs and Hadamards at random """ | ||||
clifford.use_old_cz() | clifford.use_old_cz() | ||||
@@ -2,6 +2,7 @@ from numpy import * | |||||
from tqdm import tqdm | from tqdm import tqdm | ||||
import itertools as it | import itertools as it | ||||
from abp import clifford | from abp import clifford | ||||
from abp import build_tables | |||||
from abp import qi | from abp import qi | ||||
from nose.tools import raises | from nose.tools import raises | ||||
@@ -16,14 +17,14 @@ def identify_pauli(m): | |||||
def test_find_clifford(): | def test_find_clifford(): | ||||
""" Test that slightly suspicious function """ | """ Test that slightly suspicious function """ | ||||
assert clifford.find_clifford(qi.id, clifford.unitaries) == 0 | |||||
assert clifford.find_clifford(qi.px, clifford.unitaries) == 1 | |||||
assert build_tables.find_clifford(qi.id, clifford.unitaries) == 0 | |||||
assert build_tables.find_clifford(qi.px, clifford.unitaries) == 1 | |||||
@raises(IndexError) | @raises(IndexError) | ||||
def test_find_non_clifford(): | def test_find_non_clifford(): | ||||
""" Test that looking for a non-Clifford gate fails """ | """ Test that looking for a non-Clifford gate fails """ | ||||
clifford.find_clifford(qi.t, clifford.unitaries) | |||||
build_tables.find_clifford(qi.t, clifford.unitaries) | |||||
def get_action(u): | def get_action(u): | ||||
@@ -44,14 +45,14 @@ def test_we_have_24_matrices(): | |||||
def test_we_have_all_useful_gates(): | def test_we_have_all_useful_gates(): | ||||
""" Check that all the interesting gates are included up to a global phase """ | """ Check that all the interesting gates are included up to a global phase """ | ||||
for name, u in qi.by_name.items(): | for name, u in qi.by_name.items(): | ||||
clifford.find_clifford(u, clifford.unitaries) | |||||
build_tables.find_clifford(u, clifford.unitaries) | |||||
def test_group(): | def test_group(): | ||||
""" Test we are really in a group """ | """ Test we are really in a group """ | ||||
matches = set() | matches = set() | ||||
for a, b in tqdm(it.combinations(clifford.unitaries, 2), "Testing this is a group"): | for a, b in tqdm(it.combinations(clifford.unitaries, 2), "Testing this is a group"): | ||||
i = clifford.find_clifford(a.dot(b), clifford.unitaries) | |||||
i = build_tables.find_clifford(a.dot(b), clifford.unitaries) | |||||
matches.add(i) | matches.add(i) | ||||
assert len(matches) == 24 | assert len(matches) == 24 | ||||
@@ -76,4 +77,4 @@ def test_cz_table_makes_sense(): | |||||
def test_commuters(): | def test_commuters(): | ||||
""" Test that commutation is good """ | """ Test that commutation is good """ | ||||
assert len(clifford.get_commuters(clifford.unitaries)) == 4 | |||||
assert len(build_tables.get_commuters(clifford.unitaries)) == 4 |
@@ -4,11 +4,11 @@ import sys | |||||
import os | import os | ||||
import itertools as it | import itertools as it | ||||
from string import maketrans | from string import maketrans | ||||
from abp import clifford, qi, anders_cz | |||||
from abp import clifford, qi, anders_cz, build_tables | |||||
def test_cz_table(): | def test_cz_table(): | ||||
""" Does our clifford code work with anders & briegel's table? """ | """ Does our clifford code work with anders & briegel's table? """ | ||||
state_table = clifford.get_state_table(clifford.unitaries) | |||||
state_table = build_tables.get_state_table(clifford.unitaries) | |||||
ab_cz_table = anders_cz.cz_table | ab_cz_table = anders_cz.cz_table | ||||
rows = it.product([0, 1], it.combinations_with_replacement(range(24), 2)) | rows = it.product([0, 1], it.combinations_with_replacement(range(24), 2)) | ||||
@@ -1,10 +1,10 @@ | |||||
import numpy as np | import numpy as np | ||||
from abp import clifford, qi | |||||
from abp import clifford, qi, build_tables | |||||
import itertools as it | import itertools as it | ||||
def test_cz_table(): | def test_cz_table(): | ||||
""" Does the CZ code work good? """ | """ Does the CZ code work good? """ | ||||
state_table = clifford.get_state_table(clifford.unitaries) | |||||
state_table = build_tables.get_state_table(clifford.unitaries) | |||||
rows = it.product([0, 1], it.combinations_with_replacement(range(24), 2)) | rows = it.product([0, 1], it.combinations_with_replacement(range(24), 2)) | ||||