Hi @umutsahin
Thanks for the fast replies and the quick PR!
Now I am getting a new weird behaviour. I noticed that and/or operations between two encrypted values are actually implemented already (which I thought at first they were not). So actually I can rewrite my code to this (without lookup tables for and/or):
@fhe.compiler({"x": "encrypted"})
def hasHighBloodpressure2(x):
isNormal = (x[0] < 120) | (x[1] < 80)
isPrehypertension = ((120 >= x[0]) & (x[0] <= 139)) | ( (80 >= x[1]) & (x[1] <= 89) )
isHypertension = (x[0] >= 140) | (x[1] >= 90)
return fhe.array([isNormal, isPrehypertension, isHypertension])
inputset = [np.random.randint(0, 200, size=(3, )) for _ in range(13)]
circuit = hasHighBloodpressure.compile(inputset, configuration=configuration)
sample = np.array([140, 90, 0])
print(circuit.encrypt_run_decrypt(sample))
This will take very long (~8min) to run for the first time after circuit compilation but does the right thing:
[0 0 1] # correct output
CPU times: user 1h 57min 19s, sys: 2.41 s, total: 1h 57min 21s
Wall time: 7min 48s
Here is the circuit:
Circuit 1 with & and | operators
%0 = x # EncryptedTensor<uint8, shape=(3,)> β [0, 197]
%1 = %0[0] # EncryptedScalar β [0, 197]
%2 = 120 # ClearScalar β [120, 120]
%3 = less(%1, %2) # EncryptedScalar β [0, 1]
%4 = %0[1] # EncryptedScalar β [0, 197]
%5 = 80 # ClearScalar β [80, 80]
%6 = less(%4, %5) # EncryptedScalar β [0, 1]
%7 = bitwise_or(%3, %6) # EncryptedScalar β [0, 1]
%8 = %0[0] # EncryptedScalar β [0, 197]
%9 = 120 # ClearScalar β [120, 120]
%10 = less_equal(%8, %9) # EncryptedScalar β [0, 1]
%11 = %0[0] # EncryptedScalar β [0, 197]
%12 = 139 # ClearScalar β [139, 139]
%13 = less_equal(%11, %12) # EncryptedScalar β [0, 1]
%14 = bitwise_and(%10, %13) # EncryptedScalar β [0, 1]
%15 = %0[1] # EncryptedScalar β [0, 197]
%16 = 80 # ClearScalar β [80, 80]
%17 = less_equal(%15, %16) # EncryptedScalar β [0, 1]
%18 = %0[1] # EncryptedScalar β [0, 197]
%19 = 89 # ClearScalar β [89, 89]
%20 = less_equal(%18, %19) # EncryptedScalar β [0, 1]
%21 = bitwise_and(%17, %20) # EncryptedScalar β [0, 1]
%22 = bitwise_or(%14, %21) # EncryptedScalar β [0, 1]
%23 = %0[0] # EncryptedScalar β [0, 197]
%24 = 140 # ClearScalar β [140, 140]
%25 = greater_equal(%23, %24) # EncryptedScalar β [0, 1]
%26 = %0[1] # EncryptedScalar β [0, 197]
%27 = 90 # ClearScalar β [90, 90]
%28 = greater_equal(%26, %27) # EncryptedScalar β [0, 1]
%29 = bitwise_or(%25, %28) # EncryptedScalar β [0, 1]
%30 = array([%7, %22, %29]) # EncryptedTensor<uint1, shape=(3,)> β [0, 1]
return %30
However: With the LookupTable
approach for and/or, you suggested above:
or_table = fhe.LookupTable([0, 1, 1])
def or_bits(x, y):
return or_table[x + y + 0] # added bool->int conversion here
and_table = fhe.LookupTable([0, 0, 1])
def and_bits(x, y):
return and_table[x + y + 0] # added bool->int conversion here
@fhe.compiler({"x": "encrypted"})
def hasHighBloodpressure(x):
isNormal = or_bits((x[0] < 120), (x[1] < 80))
isPrehypertension = or_bits( and_bits((120 >= x[0]), (x[0] <= 139)), and_bits((80 >= x[1]), (x[1] <= 89)) )
isHypertension = or_bits( (x[0] >= 140), (x[1] >= 90) )
return fhe.array([isNormal, isPrehypertension, isHypertension])
inputset = [np.random.randint(0, 200, size=(3, )) for _ in range(13)]
circuit = hasHighBloodpressure.compile(inputset, configuration=configuration)
sample = np.array([140, 90, 0])
print(circuit.encrypt_run_decrypt(sample))
it prints a wrong output, and I donβt seem to find the reason for that. Since the LUT just replace the normal & and | operators. It is also faster than the approach without LookupTables, which seems weird.
[225 56 0] # wrong output
CPU times: user 1min 26s, sys: 180 ms, total: 1min 27s
Wall time: 12 s
Here is the circuit:
Circuit 2 without & and | operators but with LUTs
%0 = x # EncryptedTensor<uint8, shape=(3,)> β [0, 196]
%1 = %0[0] # EncryptedScalar β [0, 196]
%2 = 120 # ClearScalar β [120, 120]
%3 = less(%1, %2) # EncryptedScalar β [0, 1]
%4 = %0[1] # EncryptedScalar β [0, 166]
%5 = 80 # ClearScalar β [80, 80]
%6 = less(%4, %5) # EncryptedScalar β [0, 1]
%7 = add(%3, %6) # EncryptedScalar β [0, 1]
%8 = 0 # ClearScalar β [0, 0]
%9 = add(%7, %8) # EncryptedScalar β [0, 1]
%10 = tlu(%9, table=[0 1 1]) # EncryptedScalar β [0, 1]
%11 = %0[0] # EncryptedScalar β [0, 196]
%12 = 120 # ClearScalar β [120, 120]
%13 = less_equal(%11, %12) # EncryptedScalar β [0, 1]
%14 = %0[0] # EncryptedScalar β [0, 196]
%15 = 139 # ClearScalar β [139, 139]
%16 = less_equal(%14, %15) # EncryptedScalar β [0, 1]
%17 = add(%13, %16) # EncryptedScalar β [0, 1]
%18 = 0 # ClearScalar β [0, 0]
%19 = add(%17, %18) # EncryptedScalar β [0, 1]
%20 = tlu(%19, table=[0 0 1]) # EncryptedScalar β [0, 0]
%21 = %0[1] # EncryptedScalar β [0, 166]
%22 = 80 # ClearScalar β [80, 80]
%23 = less_equal(%21, %22) # EncryptedScalar β [0, 1]
%24 = %0[1] # EncryptedScalar β [0, 166]
%25 = 89 # ClearScalar β [89, 89]
%26 = less_equal(%24, %25) # EncryptedScalar β [0, 1]
%27 = add(%23, %26) # EncryptedScalar β [0, 1]
%28 = 0 # ClearScalar β [0, 0]
%29 = add(%27, %28) # EncryptedScalar β [0, 1]
%30 = tlu(%29, table=[0 0 1]) # EncryptedScalar β [0, 0]
%31 = add(%20, %30) # EncryptedScalar β [0, 0]
%32 = 0 # ClearScalar β [0, 0]
%33 = add(%31, %32) # EncryptedScalar β [0, 0]
%34 = tlu(%33, table=[0 1 1]) # EncryptedScalar β [0, 0]
%35 = %0[0] # EncryptedScalar β [0, 196]
%36 = 140 # ClearScalar β [140, 140]
%37 = greater_equal(%35, %36) # EncryptedScalar β [0, 1]
%38 = %0[1] # EncryptedScalar β [0, 166]
%39 = 90 # ClearScalar β [90, 90]
%40 = greater_equal(%38, %39) # EncryptedScalar β [0, 1]
%41 = add(%37, %40) # EncryptedScalar β [0, 1]
%42 = 0 # ClearScalar β [0, 0]
%43 = add(%41, %42) # EncryptedScalar β [0, 1]
%44 = tlu(%43, table=[0 1 1]) # EncryptedScalar β [0, 1]
%45 = array([%10, %34, %44]) # EncryptedTensor<uint1, shape=(3,)> β [0, 1]
return %45
Let me know if I can provide you with more details.