Skip to content

Hidden-Shift Problem for Bent Functions

View on GitHub

Here we implement the hidden shift algorithm for the family of Boolean bent functions using the Classiq platform.

Make sure we have all necessary packages:

!pip install galois

We assume we know how to implement the dual of \(f\) and get \(s\) according to the algorithm in [1]:Screen Shot 2023-06-27 at 18.05.48.png

from classiq import *


@qfunc
def hidden_shift(
    oracle: QCallable[QArray[QBit]],
    oracle_shifted: QCallable[QArray[QBit]],
    target: QArray[QBit],
) -> None:
    hadamard_transform(target)
    oracle_shifted(target)
    hadamard_transform(target)
    oracle(target)
    hadamard_transform(target)


NUM_VARIABLES = 4


@qfunc
def main(s: Output[QArray[QBit]]) -> None:

    @qfunc
    def arith_func(vars: QArray[QBit, NUM_VARIABLES], res: QBit):
        res ^= (vars[0] & vars[1]) ^ (vars[2] & vars[3])

    @qfunc
    def arith_func_shifted(vars: QArray[QBit, NUM_VARIABLES], res: QBit):
        res ^= ((vars[0] ^ 1) & vars[1]) ^ (vars[2] & vars[3])

    allocate(NUM_VARIABLES, s)

    hidden_shift(
        lambda y: phase_oracle(arith_func, y),
        lambda y: phase_oracle(arith_func_shifted, y),
        s,
    )


constraints = Constraints(optimization_parameter="width")
qmod_simple = create_model(main, constraints, out_file="hidden_shift_simple")
qprog_simple = synthesize(qmod_simple)
show(qprog_simple)
sample_results_simple = execute(qprog_simple).result_value()
sample_results_simple.counts_of_output("s")
{'1000': 1000}

More Complex Functions

We take a Maiorana-McFarland function with random permutation on the y and h function is the and operation between all the y-variables.

import random
from functools import reduce

import numpy as np

NUM_VARIABLES = 16

# Define the list
my_list = list(range(NUM_VARIABLES // 2))

# Get a random permutation
random.seed(1)
random.shuffle(my_list)

# Create a permutation dict and its inverse
perm_dict = {i: my_list[i] for i in range(NUM_VARIABLES // 2)}
inverse_perm_dict = {v: k for k, v in perm_dict.items()}


def h(y):
    return reduce(lambda a, b: a & b, [y[i] for i in range(NUM_VARIABLES // 2)])


def h_dual(x):
    return reduce(
        lambda a, b: a & b, [x[inverse_perm_dict[i]] for i in range(NUM_VARIABLES // 2)]
    )


def f_func(x, y):
    return (
        reduce(
            lambda a, b: a ^ b,
            [x[i] & y[perm_dict[i]] for i in range(NUM_VARIABLES // 2)],
        )
    ) ^ h(y)


def f_dual_func(x, y):
    return (
        reduce(
            lambda a, b: a ^ b,
            [x[inverse_perm_dict[i]] & y[i] for i in range(NUM_VARIABLES // 2)],
        )
    ) ^ h_dual(x)


def shifted(x, y, bits):
    x = [x[i] for i in range(NUM_VARIABLES // 2)]
    y = [y[i] for i in range(NUM_VARIABLES // 2)]

    for bit in bits:
        if bit < NUM_VARIABLES >> 2:
            x[bit] = x[bit] ^ 1
        else:
            bit = bit - NUM_VARIABLES // 2
            y[bit] = y[bit] ^ 1
    return f_func(x, y)
shifted_bits = [1, 3, 9]
g_func = lambda x, y: shifted(x, y, shifted_bits)

Creating the Circuit

@qfunc
def g_qfunc(vars: QArray[QBit, NUM_VARIABLES], res: QBit):
    x = QArray("x", QBit, NUM_VARIABLES // 2)
    y = QArray("y", QBit, NUM_VARIABLES // 2)
    bind(vars, [x, y])
    print("g:", g_func(x, y))
    res ^= g_func(x, y)
    bind([x, y], vars)


@qfunc
def f_dual_qfunc(vars: QArray[QBit, NUM_VARIABLES], res: QBit):
    x = QArray("x", QBit, NUM_VARIABLES // 2)
    y = QArray("y", QBit, NUM_VARIABLES // 2)

    bind(vars, [x, y])
    print("f_dual:", f_dual_func(x, y))
    res ^= f_dual_func(x, y)
    bind([x, y], vars)


@qfunc
def f_qfunc(vars: QArray[QBit, NUM_VARIABLES], res: QBit):
    x = QArray("x", QBit, NUM_VARIABLES // 2)
    y = QArray("y", QBit, NUM_VARIABLES // 2)

    bind(vars, [x, y])
    print("f:", f_func(x, y))
    res ^= f_func(x, y)
    bind([x, y], vars)


@qfunc
def main(s: Output[QArray[QBit]]) -> None:
    allocate(NUM_VARIABLES, s)

    hidden_shift(
        lambda y: phase_oracle(f_dual_qfunc, y),
        lambda y: phase_oracle(g_qfunc, y),
        s,
    )


qmod_complex = create_model(
    main, constraints=constraints, out_file="hidden_shift_complex"
)  # same constraints
qprog_complex = synthesize(qmod_complex)
show(qprog_complex)
f_dual: (((((((((x[5]) & (y[0])) ^ ((x[2]) & (y[1]))) ^ ((x[7]) & (y[2]))) ^ ((x[0]) & (y[3]))) ^ ((x[6]) & (y[4]))) ^ ((x[3]) & (y[5]))) ^ ((x[1]) & (y[6]))) ^ ((x[4]) & (y[7]))) ^ ((((((((x[5]) & (x[2])) & (x[7])) & (x[0])) & (x[6])) & (x[3])) & (x[1])) & (x[4]))
g: (((((((((x[0]) & (y[3])) ^ (((x[1]) ^ 1) & (y[6]))) ^ ((x[2]) & ((y[1]) ^ 1))) ^ (((x[3]) ^ 1) & (y[5]))) ^ ((x[4]) & (y[7]))) ^ ((x[5]) & (y[0]))) ^ ((x[6]) & (y[4]))) ^ ((x[7]) & (y[2]))) ^ ((((((((y[0]) & ((y[1]) ^ 1)) & (y[2])) & (y[3])) & (y[4])) & (y[5])) & (y[6])) & (y[7]))
sample_results_complex = execute(qprog_complex).result_value()
sample_results_complex.counts_of_output("s")
{'0101000001000000': 1000}
expected_s = "".join("1" if i in shifted_bits else "0" for i in range(NUM_VARIABLES))
assert list(sample_results_complex.counts_of_output("s").keys())[0] == expected_s

And indeed we got the correct shift!

Hidden Shift Without the Dual Function

We now use the second algorithm described in [2]. This algorithm only requires implementing \(f\) and not its dual; however, it requires \(O(n)\) samples from the circuit. Screen Shot 2023-06-27 at 18.08.23.png

@qfunc
def hidden_shift_no_dual(
    oracle: QCallable[QArray[QBit], QBit],
    oracle_shifted: QCallable[QArray[QBit], QBit],
    target: QArray[QBit],
    ind: QBit,
) -> None:
    hadamard_transform(target)
    oracle(target, ind)
    Z(ind)
    oracle_shifted(target, ind)
    hadamard_transform(target)


NUM_VARIABLES = 16


@qfunc
def main(target: Output[QArray[QBit]], ind: Output[QBit]) -> None:

    allocate(NUM_VARIABLES, target)
    allocate(1, ind)

    hidden_shift_no_dual(
        lambda vars, result: f_qfunc(vars, result),
        lambda vars, result: g_qfunc(vars, result),
        target,
        ind,
    )


qmod_no_dual = create_model(
    main, constraints=constraints, out_file="hidden_shift_no_dual"
)  # same constraints
qprog_no_dual = synthesize(qmod_no_dual)
show(qprog_no_dual)
f: (((((((((x[0]) & (y[3])) ^ ((x[1]) & (y[6]))) ^ ((x[2]) & (y[1]))) ^ ((x[3]) & (y[5]))) ^ ((x[4]) & (y[7]))) ^ ((x[5]) & (y[0]))) ^ ((x[6]) & (y[4]))) ^ ((x[7]) & (y[2]))) ^ ((((((((y[0]) & (y[1])) & (y[2])) & (y[3])) & (y[4])) & (y[5])) & (y[6])) & (y[7]))
g: (((((((((x[0]) & (y[3])) ^ (((x[1]) ^ 1) & (y[6]))) ^ ((x[2]) & ((y[1]) ^ 1))) ^ (((x[3]) ^ 1) & (y[5]))) ^ ((x[4]) & (y[7]))) ^ ((x[5]) & (y[0]))) ^ ((x[6]) & (y[4]))) ^ ((x[7]) & (y[2]))) ^ ((((((((y[0]) & ((y[1]) ^ 1)) & (y[2])) & (y[3])) & (y[4])) & (y[5])) & (y[6])) & (y[7]))
sample_results_no_dual = execute(qprog_no_dual).result_value()

Out of the sampled results, we look for \(n\) independent samples, from which we can extract s. One thousand samples should be enough with a very high probability.

# The galois library is a package that extends NumPy arrays to operate over finite fields.
# we will use it as our equations are binary equations
import galois

# here we work over Boolean arithmetics - F(2)
GF = galois.GF(2)


def is_independent_set(vectors):
    matrix = GF(vectors)
    rank = np.linalg.matrix_rank(matrix)
    if rank == len(vectors):
        return True
    else:
        return False


samples = [
    ([int(i) for i in u], int(b))
    for u, b in sample_results_no_dual.counts_of_multiple_outputs(
        ["target", "ind"]
    ).keys()
]

ind_v = []
ind_b = []
for v, b in samples:
    if is_independent_set(ind_v + [v]):
        ind_v.append(v)
        ind_b.append(b)
        if len(ind_v) == len(v):
            # reached max set
            break

assert len(ind_v) == len(v)

We now solve the equation and extract \(s\):

A = np.array(ind_v)
b = np.array(ind_b)

# Solve the linear system
s = np.linalg.solve(GF(A), GF(b))
s
GF([0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], order=2)

And we successfully received the same shift.

assert "".join(str(i) for i in s) == expected_s

References

[1]: Quantum algorithms for highly non-linear Boolean functions

[2]: Quantum algorithm for the Boolean hidden shift problem