Returning array after or/and operations not working correctly

I currently have this code to test if someone has high blood pressure. The input consists of an array of length 3, that holds two medical values and a 0 in the last digit (padding).

import numpy as np
from concrete import fhe

configuration = fhe.Configuration(dataflow_parallelize=True)

or_table = fhe.LookupTable([0, 1, 1])
def or_bits(x, y):
    return or_table[int(x) + int(y)]

and_table = fhe.LookupTable([0, 0, 1])
def and_bits(x, y):
    return and_table[int(x) + int(y)]

def hasHighBloodpressure(x):    
    isHypertension = or_bits(x[0] >= 140, x[1] >= 90)
    isPrehypertension = or_bits( and_bits(120 >= x[0], x[0] <= 139), and_bits(80 >= x[1], x[1] <= 89) )
    isNormal = or_bits(x[0] < 120, x[1] < 80)
            
    return [isNormal, isPrehypertension, isHypertension]

@fhe.compiler({"x": "encrypted"})
def f(x):
    return fhe.univariate(hasHighBloodpressure)(x)

inputset = [[150,150,0]]
circuit = f.compile(inputset, configuration=configuration)

Simulating it with: circuit.simulate([140, 90, 0]) yields array([0, 0, 1]) which is correct.

When actually running the encrypt_run_decrypt function I get something different:

sample = np.array([140, 90, 0])
print(circuit.encrypt_run_decrypt(sample))
# prints [0 1 0]  --> wrong

I also played around with the error prob. which has not changed anything, unfortunately.

Am I doing it too complicated or is there a mistake I am not seeing?

I am working on WSL with Ubuntu 22.04 and Python 3.9

As a first impression it seems your input set may be too small. The input set needs to be representative (have the same distribution) as the data that you will apply your encrypted function on.

What is the third cell in the input used for ?

Thus I would suggest you generate data with the distribution you are expecting for cells 0 and 1. The safest bet it to say the distribution is at worst uniform and use np.random.randint with the appropriate low and high range.

1 Like

Hi @G43beli

The actual problem is that univariate extension only works if each of the output elements correspond to a single input element, at the same location of the input. univariate is converted to a table lookup, and if you use input[0] and input[1], it cannot be converted properly.

Unfortunately, what you did is impossible to detect and raise error for. We can only call the function you provide, nothing more.

You can try to implement normally without univariate maybe :slightly_smiling_face:

Let us know if you have other questions!

1 Like

Thanks for the remark about my input set. I will increase the size of it (see code below)

I did not understand how I would return an array of length 3 as the output when I have an array of length 2 as the input. That’s why I added the third cell (always 0 in the input) as β€œpadding”. Is there better way to do that?

Idea:
Input: medicalValue1, medicalValue2
Ouput: one of the three states (has high blood pressure, β€œalmost” has high blood pressure, everything normal)

The states are determined with the two medical Values (Systolic vs. Diastolic Blood Pressure)

My approach:
Input: [medicalValue1, medicalValue2, 0] as array (maybe there is a better approach and I should input them as x and y (individual variables)?
Ouput: [isNormal, isPrehypertension, isHypertension] (bit array, for example [1, 0, 0] if normal)

The output could also just be 1 or 2 or 3, depending on the state.

Is my approach even feasible for that scenario? Sorry if my question is silly, but I am very new to FHE and Concrete, maybe my approach is way too complicated…

Do you mean just calling my function without the univariate extension like this:

@fhe.compiler({"x": "encrypted"})
def hasHighBloodpressure(x):
    isHypertension = or_bits(x[0] >= 140, x[1] >= 90)
    isPrehypertension = or_bits( and_bits(120 >= x[0], x[0] <= 139), and_bits(80 >= x[1], x[1] <= 89) )
    isNormal = or_bits(x[0] < 120, x[1] < 80)
            
    return np.array([isNormal, isPrehypertension, isHypertension])

inputset = [np.random.randint(0, 200, size=(3, )) for _ in range(15)]
circuit = hasHighBloodpressure.compile(inputset, configuration=configuration)

This will raise the error

function 'hasHighBloodpressure' returned '[
<concrete.fhe.tracing.tracer.Tracer object at 0x7fb8fb5fda80>
<concrete.fhe.tracing.tracer.Tracer object at 0x7fb8fb5fc550>
<concrete.fhe.tracing.tracer.Tracer object at 0x7fb8fb7ed9f0>
]', which is not supported

Maybe I have to restructure my input and output to enable this to work. I am completely open in how to design the input and the output, I just could not figure out how I would do it with Concrete so far :smiley:

Thanks for your help @umutsahin and @andrei-stoian-zama

Could you replace np.array([isNormal, isPrehypertension, isHypertension]) with cnp.array([isNormal, isPrehypertension, isHypertension]) :slight_smile:

1 Like

This results in: RuntimeError: Bound measurement using inputset[0] failed

This is the graph:

RuntimeError: Evaluation of the graph failed

%0 = x                              # EncryptedTensor<uint6, shape=(3,)>
 %1 = %0[0]                          # EncryptedScalar<uint6>
 %2 = 140                            # ClearScalar<uint8>
 %3 = greater_equal(%1, %2)          # EncryptedScalar<uint1>
 %4 = %0[1]                          # EncryptedScalar<uint6>
 %5 = 90                             # ClearScalar<uint7>
 %6 = greater_equal(%4, %5)          # EncryptedScalar<uint1>
 %7 = add(%3, %6)                    # EncryptedScalar<uint2>
 %8 = tlu(%7, table=[0 1 1])         # EncryptedScalar<uint1>
 %9 = %0[0]                          # EncryptedScalar<uint6>
%10 = 120                            # ClearScalar<uint7>
%11 = less_equal(%9, %10)            # EncryptedScalar<uint1>
%12 = %0[0]                          # EncryptedScalar<uint6>
%13 = 139                            # ClearScalar<uint8>
%14 = less_equal(%12, %13)           # EncryptedScalar<uint1>
%15 = add(%11, %14)                  # EncryptedScalar<uint2>
%16 = tlu(%15, table=[0 0 1])        # EncryptedScalar<uint1>
%17 = %0[1]                          # EncryptedScalar<uint6>
%18 = 80                             # ClearScalar<uint7>
%19 = less_equal(%17, %18)           # EncryptedScalar<uint1>
%20 = %0[1]                          # EncryptedScalar<uint6>
%21 = 89                             # ClearScalar<uint7>
%22 = less_equal(%20, %21)           # EncryptedScalar<uint1>
%23 = add(%19, %22)                  # EncryptedScalar<uint2>
%24 = tlu(%23, table=[0 0 1])        # EncryptedScalar<uint1>
%25 = add(%16, %24)                  # EncryptedScalar<uint2>
%26 = tlu(%25, table=[0 1 1])        # EncryptedScalar<uint1>
%27 = %0[0]                          # EncryptedScalar<uint6>
%28 = 120                            # ClearScalar<uint7>
%29 = less(%27, %28)                 # EncryptedScalar<uint1>
%30 = %0[1]                          # EncryptedScalar<uint6>
%31 = 80                             # ClearScalar<uint7>
%32 = less(%30, %31)                 # EncryptedScalar<uint1>
%33 = add(%29, %32)                  # EncryptedScalar<uint2>
%34 = tlu(%33, table=[0 1 1])        # EncryptedScalar<uint1>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of this node failed
%35 = array([%34, %26, %8])          # EncryptedTensor<uint1, shape=(3,)>
return %35

For reference, this is my input set:

inputset = [np.random.randint(0, 200, size=(3, )) for _ in range(15)]
inputset.append(np.array([150, 150, 0]))
inputset.append(np.array([0, 0, 0]))
# prints: 
# [array([35, 49, 47]), 
# array([139, 116, 172]), 
# array([ 40, 108, 196]), 
# array([192,  72, 153]), 
# array([117,  91,  76]), 
# array([  2, 170,  66]), 
# array([  9,   0, 134]), 
# array([ 98,  26, 103]), 
# array([ 20,  16, 141]), 
# array([ 92, 179, 127]), 
# array([115, 167, 119]), 
# array([ 76,  65, 134]), 
# array([67, 51, 70]), 
# array([177,  22, 118]), 
# array([162,  51,  66]), 
# array([150, 150,   0]), 
# array([0, 0, 0])]

That error should have 3 more errors, how are you running your code?

I am working in jupyter-lab Notebook

This is the full error:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/representation/graph.py:178, in Graph.evaluate(self, p_error, *args)
    177 try:
--> 178     node_results[node] = node(*pred_results)
    179 except Exception as error:

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/representation/node.py:216, in Node.__call__(self, *args)
    214         raise ValueError(message)
--> 216 result = self.evaluator(*args)
    218 if isinstance(result, int) and -(2**63) < result < (2**63) - 1:

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/representation/evaluator.py:39, in GenericEvaluator.__call__(self, *args, **kwargs)
     38 def __call__(self, *args, **kwargs):
---> 39     return self.operation(*args, *self.properties["args"], **self.properties["kwargs"])

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/extensions/table.py:121, in LookupTable.apply(key, table)
    120     message = f"LookupTable cannot be looked up with {key}"
--> 121     raise ValueError(message)
    123 if np.issubdtype(table.dtype, np.integer):

ValueError: LookupTable cannot be looked up with True

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/representation/graph.py:438, in Graph.measure_bounds(self, inputset)
    437 try:
--> 438     evaluation = self.evaluate(*sample)
    439     for node, value in evaluation.items():

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/representation/graph.py:180, in Graph.evaluate(self, p_error, *args)
    179     except Exception as error:
--> 180         raise RuntimeError(
    181             "Evaluation of the graph failed\n\n"
    182             + self.format(
    183                 highlighted_nodes={node: ["evaluation of this node failed"]},
    184                 show_bounds=False,
    185             )
    186         ) from error
    188 return node_results

RuntimeError: Evaluation of the graph failed

 %0 = x                              # EncryptedTensor<uint6, shape=(3,)>
 %1 = %0[0]                          # EncryptedScalar<uint6>
 %2 = 140                            # ClearScalar<uint8>
 %3 = greater_equal(%1, %2)          # EncryptedScalar<uint1>
 %4 = %0[1]                          # EncryptedScalar<uint6>
 %5 = 90                             # ClearScalar<uint7>
 %6 = greater_equal(%4, %5)          # EncryptedScalar<uint1>
 %7 = add(%3, %6)                    # EncryptedScalar<uint2>
 %8 = tlu(%7, table=[0 1 1])         # EncryptedScalar<uint1>
 %9 = %0[0]                          # EncryptedScalar<uint6>
%10 = 120                            # ClearScalar<uint7>
%11 = less_equal(%9, %10)            # EncryptedScalar<uint1>
%12 = %0[0]                          # EncryptedScalar<uint6>
%13 = 139                            # ClearScalar<uint8>
%14 = less_equal(%12, %13)           # EncryptedScalar<uint1>
%15 = add(%11, %14)                  # EncryptedScalar<uint2>
%16 = tlu(%15, table=[0 0 1])        # EncryptedScalar<uint1>
%17 = %0[1]                          # EncryptedScalar<uint6>
%18 = 80                             # ClearScalar<uint7>
%19 = less_equal(%17, %18)           # EncryptedScalar<uint1>
%20 = %0[1]                          # EncryptedScalar<uint6>
%21 = 89                             # ClearScalar<uint7>
%22 = less_equal(%20, %21)           # EncryptedScalar<uint1>
%23 = add(%19, %22)                  # EncryptedScalar<uint2>
%24 = tlu(%23, table=[0 0 1])        # EncryptedScalar<uint1>
%25 = add(%16, %24)                  # EncryptedScalar<uint2>
%26 = tlu(%25, table=[0 1 1])        # EncryptedScalar<uint1>
%27 = %0[0]                          # EncryptedScalar<uint6>
%28 = 120                            # ClearScalar<uint7>
%29 = less(%27, %28)                 # EncryptedScalar<uint1>
%30 = %0[1]                          # EncryptedScalar<uint6>
%31 = 80                             # ClearScalar<uint7>
%32 = less(%30, %31)                 # EncryptedScalar<uint1>
%33 = add(%29, %32)                  # EncryptedScalar<uint2>
%34 = tlu(%33, table=[0 1 1])        # EncryptedScalar<uint1>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of this node failed
%35 = array([%34, %26, %8])          # EncryptedTensor<uint1, shape=(3,)>
return %35

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
Cell In[33], line 26
     24 inputset.append(np.array([0, 0, 0]))
     25 print(inputset)
---> 26 circuit = hasHighBloodpressure.compile(inputset, configuration=configuration)

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/compilation/decorators.py:159, in compiler.<locals>.decoration.<locals>.Compilable.compile(self, inputset, configuration, artifacts, **kwargs)
    131 def compile(
    132     self,
    133     inputset: Optional[Union[Iterable[Any], Iterable[Tuple[Any, ...]]]] = None,
   (...)
    136     **kwargs,
    137 ) -> Circuit:
    138     """
    139     Compile the function into a circuit.
    140 
   (...)
    156             compiled circuit
    157     """
--> 159     return self.compiler.compile(inputset, configuration, artifacts, **kwargs)

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/compilation/compiler.py:434, in Compiler.compile(self, inputset, configuration, artifacts, **kwargs)
    425 self.artifacts = (
    426     artifacts
    427     if artifacts is not None
   (...)
    430     else None
    431 )
    433 try:
--> 434     self._evaluate("Compiling", inputset)
    435     assert self.graph is not None
    437     mlir = GraphConverter().convert(self.graph, self.configuration)

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/compilation/compiler.py:282, in Compiler._evaluate(self, action, inputset)
    279     self._trace(first_sample)
    280     assert self.graph is not None
--> 282 bounds = self.graph.measure_bounds(self.inputset)
    283 self.graph.update_with_bounds(bounds)
    285 if self.artifacts is not None:

File ~/.local/share/virtualenvs/zama-concrete-69GT50Jl/lib/python3.10/site-packages/concrete/fhe/representation/graph.py:459, in Graph.measure_bounds(self, inputset)
    457 except Exception as error:
    458     message = f"Bound measurement using inputset[{index}] failed"
--> 459     raise RuntimeError(message) from error
    461 return bounds

RuntimeError: Bound measurement using inputset[0] failed

Ah, it’s something we should improve :slight_smile:
And I’ve just created the PR to do that :wink:

It should be merged soon and be available in the next release!

In the meantime, you should be able to do:

some_table = fhe.LookupTable([0, 0, 1])
def apply(some_boolean):
    return some_table[some_boolean + 0]

As adding 0 to a boolean will convert it to an integer :slight_smile:

1 Like

Hi @umutsahin

Thanks for the fast replies and the quick PR! :star_struck:

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.

1 Like

This will take very long (~8min) to run for the first time after circuit compilation but does the right thing

That timing includes compilation, encryption, decryption and probably key generation time as well! Check out our How to Deploy documentation to perform those steps individually. And you can use the time module to measure each operation.

it prints a wrong output

That’s weird indeed. Would it be possible to provide the full code for both implementations so I can take a look :slight_smile:

1 Like

Yes, you are absolutely right. I did not mean it takes too long. For me, the longer execution time was actually a good indicator that it works correctly. There are many operations and that takes time :slight_smile: Actually the longest is the key generation. Which is only performed the first time.

I was just not sure (since I am new to all of this), if my implementation could be improved in general, performance and design-wise.

Maybe my inputset is now too big :smiley:

Absolutely! here is the full code for the implementations:

With & and | operators
import numpy as np
from concrete import fhe

configuration = fhe.Configuration(dataflow_parallelize=True)

inputset = [np.random.randint(0, 200, size=(3, )) for _ in range(13)]
inputset.append(np.array([150, 150, 0]))
inputset.append(np.array([0, 0, 0]))

@fhe.compiler({"x": "encrypted"})
def hasHighBloodpressure1(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])

circuit1 = hasHighBloodpressure1.compile(inputset, configuration=configuration)

sample = np.array([140, 90, 0])
print(circuit1.encrypt_run_decrypt(sample))
With LUTs
import numpy as np
from concrete import fhe

configuration = fhe.Configuration(dataflow_parallelize=True)

inputset = [np.random.randint(0, 200, size=(3, )) for _ in range(13)]
inputset.append(np.array([150, 150, 0]))
inputset.append(np.array([0, 0, 0]))

or_table = fhe.LookupTable([0, 1, 1])
def or_bits(x, y):
    return or_table[x + y + 0]

and_table = fhe.LookupTable([0, 0, 1])
def and_bits(x, y):
    return and_table[x + y + 0]

@fhe.compiler({"x": "encrypted"})
def hasHighBloodpressure2(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])

circuit2 = hasHighBloodpressure2.compile(inputset, configuration=configuration)

sample = np.array([140, 90, 0])
print(circuit2.encrypt_run_decrypt(sample))
1 Like

Thank you! I’ve created an internal issue to track this. In the meantime, you have the working version, so you can proceed with your task I presume :slight_smile:

1 Like

absolutely, I marked your answer above as the solution. You helped me a lot here. Thanks again! :blush:
Can you post updates on this issue as soon as you have some news?

One question: For this code, the generated client keys (saved as a file) are ~3.3GB. Is this roughly what is expected…it seems rather big? This is also the operation that takes the longest.

I used the deployment code provided here with my own hasHighBloodpressure function: Deploy - Concrete

1 Like

Hey @umutsahin

Just a follow up question: Is it true that some operations produce a very big client key when running:

circuit.keys.load_if_exists_generate_and_save_otherwise("client_keys_file")

For example, the addition of a cleartext integer with an encrypted value will result in a very small (6843 Bytes) key, but when using less than for example, the keys almost get as big as 2GB. And when having a function as I have above (see hasHighBloodpressure) with almost 8-12 comparison operators it will reach over 3GB.

Is there an overview of the different operations and how they affect the key size?

Hey @G43beli,

I was off for some time now, sorry for the late answer!

Yes it’s true that different circuits have different key sizes. Unfortunately, there is no clear semantics of how much each operation affects the key size. Key size primarily depend on the selected parameters and it depends highly on your circuit and configuration (see Exactness - Concrete).

If your circuit doesn’t involve any table lookups (e.g. just addition, matmul, conv, etc.) then we call that circuit a leveled circuit, and leveled circuits don’t require key switching keys or bootstrapping keys, so they are much smaller!

Hope this helps :slight_smile: