If conditional Implementation in Python

Hello. I have been using FHE scheme that does not support if conditional. I am searching for TFHE in Python. Would Concrete-numpy be useful for my machine learning algorithm (note that my ML algorithm is not in Concrete-ml).

Hello. Could you give us a bit more details in the algorithm you are trying to implement? This will help to address your question properly.

Thanks!

Thank you for your reply. Actually, I am working with gene expression data sets that have two dimensions by [Biclustering algorithms] (https://www.researchgate.net/profile/George_Church/publication/2329589_Biclustering_of_Expression_Data/links/550c04030cf2063799394f5e.pdf). Previously I used Pyfhel in my project but I realized that it did not support if conditionals then I have to change my scheme in the middle. I wonder whether Concrete-numpy can possibly be alternative solution to impelement conditional for two values (one constant and one numpy array)?

1 Like

Thanks for the details. So yes Concrete-Numpy allows you to reproduce the if conditionals in FHE using table lookup. This is what we use in Concrete-ML to implement decision trees. However, it will require you to implement the if else using an operator like numpy.where rather than actual if else statements which are not currently supported as is in Concrete-Numpy.

Edit: I might have been a bit too quick on this answer. The operator numpy.where is not currently supported in the current version but will come in a future release. That being said as long as you can build a TLU to represent your if conditional you should be able to do what you want.

1 Like

Thank you for clarifying the issue!
Is it possible to show a brief demo of the sample implementation of what you mentioned?

Hey,

For example: if you want to compute

 y = x < 4 ? 2x : x + 1, for an x in [0, ..., 31],

which stands for “if x < 4, then 2x else x+1”, you can build a table T, which is defined by

T[i] = i < 4 ? 2i : i + 1, for an i in [0, ..., 31],

and just replace your IF by y = T[x]. Table lookups are explained in Table Lookup — Concrete Numpy Manual.

In a further release of Concrete-Numpy, this conversion is done automatically for the user.

Cheers

2 Likes

sir array with negative values is not working in lookup table method

numpy.array must be of dtype uint{8,16,32,64}

Hey,

Sorry, I have not understood what you mean. And what’s the link with the original question, again? Docs about TLU: Table Lookup — Concrete Numpy Manual

Yes, but I don’t know what’s the link with the original question or your question related to negative things.

i am trying to implement if condition with lookup table. but my lookup table entries have negative values.

  1. i am trying to access those values i am not getting correctly.
    2)Generating above error trying access negative index.(like access elements with negative index in python)

Hi, the current version of concrete-numpy doesn’t support negative inputs to the function or negative values in the table lookup. However, it does accept negative inputs to the table lookup. So you can do:

import concrete.numpy as hnp

table = hnp.LookupTable([2, 1, 3, 0])

def function(x):
    return table[-x]

compiler = hnp.NPFHECompiler(function, {"x": "encrypted"})

inputset = range(4)
circuit = compiler.compile_on_inputset(inputset)

for i in range(4):
    print(function(i), "==", circuit.encrypt_run_decrypt(i))

Those features are being worked on right now and they will be available in the upcoming releases.

According to what you mentioned in the previous post, I tried to implement if statement within a lookup table:

table = hnp.LookupTable() #how to fill this lookup table for working with a constant?
def conditional_Constant(x):
     if x <= 300:
          stop = True
     else:
          stop = False
      return stop

inputset = msr #int 
compiler = hnp.NPFHECompiler(conditional, {"x": "encrypted"})
circuit = compiler.compile_on_inputset(inputset)
circuit.keygen()
public_args = circuit.encrypt(msr)
encrypted_result = circuit.run(public_args)
decrypted_result = circuit.decrypt(encrypted_result)
print(decrypted_result)

For the second example, I have to compare two encrypted values (msr (constant int), and msr_row (numpy.ndarray)
if msr <= msr_row:
return msr

For both situations (comparison of two values and/or one constant value) how can possibly get the result without errors such as “object is not iterable”?

Hi, your inputset should be iterable. So you can do:

inputset = [msr]
circuit = compiler.compile_on_inputset(inputset)

But having a single input in the inputset is not recommended as inputset is used for bounds measurement and a single input is not enough to do that.

As for how to fill your table lookup, I’m not sure how you are planning to use it. It’s not a complete replacement for if statements, it’s just a replacement of certain if statements.

If you have this code for example:

def f(x):
    if x < 10:
        return x * 5
    else:
        return x + 5

you can convert it to this:

table = []
for i in range(16):  # 4-bits of input
    if x < 10:
        table.append(x * 5)
    else:
        table.append(x + 5)

def f(x):
   return hnp.LookupTable(table)[x]

But if you want to have complex loop logic, conditional break, or early returns, you cannot do it with the table lookups.

1 Like

Hi, the new version provides another way to achieve this, and it’s easier to maintain:

import concrete.numpy as cnp
import numpy as np

def deterministic_unary_function(x):
    if x > 100:
        return x + 1
    elif x < 20:
        if x > 5:
            return x * 4
        else:
            result = 0
            for i in range(1, x):
                result += i
            return result
    else:
        return x

@cnp.compiler({"x": "encrypted"})
def function(x):
    return cnp.univariate(deterministic_unary_function)(x)

inputset = [np.random.randint(0, 110, size=()) for _ in range(10)]
circuit = function.compile(inputset)

for i in range(110):
    print(i, "->", circuit.encrypt_run_decrypt(i))

Cheers!
Umut

2 Likes

Hi @umutsahin and thank you for making the conditional statements much more easier for univariate functions.
In some occasions like code below, I have to choose between two encrypted values (implemented in this topic by Concrete-numpy). I’d appreciate if you could help me find a solution with the new version of CNP:

    def _node_addition(self, data, rows, cols):
        """Performs the row/column addition step"""
        stop = False
        while not stop:
            cols_old = np.copy(cols)
            rows_old = np.copy(rows)

            msr, _, _ = self._calculate_msr(data, rows, cols)
            col_msr = self._calculate_msr_col_addition(data, rows, cols)
            cols2add = **np.where(col_msr <= msr)[0]** #col_msr and msr should be encrypted
            cols[cols2add] = True

            msr, _, _ = self._calculate_msr(data, rows, cols)
            row_msr, row_inverse_msr = self._calculate_msr_row_addition(data, rows, cols)
            rows2add = **np.where(np.logical_or(row_msr <= msr, row_inverse_msr <= msr))[0]** #row_msr and row_inverse, and msr should be encrypted
            rows[rows2add] = True

            if np.all(rows == rows_old) and np.all(cols == cols_old):
                stop = True

Hey @ShVS,

I’ll take look into your issue on Monday :slight_smile:
I might ask for more details, hope that’s okay for you.

Have a nice weekend!
Umut

No problem at all. Have a calm & relaxing weekend ahead!

Thank you in advance,
Shokofeh

Hey @ShVS,

Let’s start with the dynamic loop. So, concrete-numpy converts your function to directed acyclic graph, which means there cannot be loops in the graph. If there are loops in your code that loops constant number of times, they are unrolled, thus supported. So, unfortunately, you cannot have the while loop you have.

The issue is that we need to know exactly which operations are performed to select the crypto parameters. In the end, the execution path cannot depend on the input data. You might convert it to a static loop that loops N times, where N can depend on the shape of data.

Another issue is with dynamic indexing. You’re trying to index encrypted values (cols and rows) with other encrypted values (cols2add and rows2add). Unfortunately, this is not possible right now.

However, I might offer you a workaround. Since the decision is binary, you can use an additional bit + tlu to eliminate the values you don’t want. Here is an example code.

import concrete.numpy as cnp
import numpy as np

table = cnp.LookupTable([i for i in range(2 ** 6)] + [0 for _ in range(2 ** 6)])

@cnp.compiler({"x": "encrypted"})
def function(x):
    resulting_rows = []
    for i in range(x.shape[0]):
        row = x[i]
        condition = np.sum(row) > 10

        # set the 7th bit of each value in the row to 0 or 1
        tagged_row = row + ((2 ** 6) * condition)
        # depending on the 7th bit, set values to 0 or themselves
        maybe_eliminated_row = table[tagged_row]

        resulting_rows.append(maybe_eliminated_row)
    return np.concatenate(tuple(resulting_rows)).reshape(x.shape)

inputset = [
    np.random.randint(0, 10, size=(3, 2))
    for _ in range(10)
]

circuit = function.compile(
    inputset,
    cnp.Configuration(
        verbose=True,
        dump_artifacts_on_unexpected_failures=False,
        enable_unsafe_features=True,
        use_insecure_key_cache=True,
        insecure_key_cache_location=".keys",
    ),
)

sample = np.array([[4, 1], [6, 5], [9, 0]])
print(circuit.encrypt_run_decrypt(sample))

Just a small note, assignment to tensors will come in the next release and you’ll be able to do:

@cnp.compiler({"x": "encrypted"})
def function(x):
    for i in range(x.shape[0]):
        row = x[i]
        condition = np.sum(row) > 10
        tagged_row = row + ((2 ** 6) * condition)
        x[i] = table[tagged_row]
    return x

But for now, np.concatenate trick I shared should work.

Your use case seems to be very complicated so my apologies if I misunderstood your goal. Please let me know if that’s the case :slight_smile:
Umut

3 Likes