Reverse function to obtain original input values

188 Views Asked by At

I have the following get_angles function that creates angles from input_features. The features returned by the function are being used to train a variational quantum circuit.

def get_angles(x):
    beta0 = 2 * np.arcsin(np.sqrt(x[1] ** 2) / np.sqrt(x[0] ** 2 + x[1] ** 2 + 1e-12))
    beta1 = 2 * np.arcsin(np.sqrt(x[2] ** 2) / np.sqrt(x[2] ** 2 + x[2] ** 2 + 1e-12))
    beta2 = 2 * np.arcsin(np.linalg.norm(x[2:]) / np.linalg.norm(x))
    
    return np.array([beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2])

As such:

input_features = [10, 20, 30, 40, 50]`

# Transform the features
features = np.array(get_angles(input_features))

Now, I would like to reverse this operation by taking the final features values and transforming them back into the input_features values used in the get_angles function. Is there a way to reverse the get_angles function defined above?

Thanks in advance.

Expecting to receive the input_features from running the final features through a get_reverse_angles function, I tried multiple variations of a get_reverse_angles function such as the one below to no avail.

def get_reverse_angles(angles):
    # Extract the angles
    beta2, neg_beta1, pos_beta1, neg_beta0, pos_beta0 = angles
    
    # Solve for x using trigonometric equations
    x0 = np.sqrt(2)
    x1 = np.sin(beta2 / 2) * np.sqrt(2)
    x2 = np.sin(pos_beta1 / 2) * np.sqrt(2)
    x3 = np.sin(pos_beta0 / 2) * np.sqrt(2)
    x4 = np.sin(neg_beta0 / 2) * np.sqrt(2)
    
    # Compute x0 using the first equation
    x0 = np.sqrt(x1 ** 2 + x2 ** 2 + x3 ** 2 + x4 ** 2)
    
    # Return the values of the reversed operation
    return np.array([x0, x1 * x0, x2 * x0, x3 * x0, x4 * x0])

The get_reverse_angles function returned [ 1.79350156 2.41835701 0.97063605 1.33346136 -1.33346136] as opposed to the expected [10 20 30 40 50] input_features.

1

There are 1 best solutions below

10
SVBazuev On BEST ANSWER

This answer is rewritten based on additional comments by @camaya :

  1. The overloaded get_angles_or_features function seems promising and almost does the trick, it only needs to work for larger input features, i.e., input_features = [22393, 22962, 22689, 21849, 20979].
  2. It may be worth trying given there is only one sample in input_features with 5 numbers of 5 elements each. The input data always contains 5 elements and is sorted in arbitrary order.

So what do we have?!

  1. Incoming data input_features = [22393, 22962, 22689, 21849, 20979] - a list of unsorted integers.

  2. Three variables are declared in the get_angles function:

    Table "Stages of input data recovery"
    Step Variable Elements involved in calculations
    1 beta0 input[1] & input[0] it can be restored using features[4]
    2 beta1 input[2] cannot be restored using features[2] too many variations
    3 beta2 input[:] if you save input[2], you can restore (input[3], input[4]) or (input[4], input[3]) using features[0]

    (input[:] == input_features ; def get_angles(x): return features[:])

Step 1

import numpy as np
from datetime import datetime
from typing import Union, List


def get_angles_or_features(
        input: Union[List[int], np.ndarray[float]]
        ) -> Union[np.ndarray, List, None]:
    """"""

    input_type = type(input).__name__
    # Check type input
    if not (input_type == 'ndarray' or input_type == 'list'):
        print("Input Error")
        return None
    # Processing get angles
    elif input_type == 'list':
        beta0 = (
            2 * np.arcsin((np.sqrt(input[1] ** 2))
                          / np.sqrt(input[0] ** 2 + input[1] ** 2 + 1e-12)
                          )
        )
        beta1 = (
            2 * np.arcsin(np.sqrt(input[2] ** 2)
                          / np.sqrt(input[2] ** 2 + input[2] ** 2 + 1e-12)
                          )
        )
        beta2 = (
            2 * np.arcsin(np.linalg.norm(input[2:]) / np.linalg.norm(input))
        )

        return np.array([beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2])

    # Restoring data input_features
    elif input_type == 'ndarray':
        beta0 = input[4]

        x0, x1, x2, x3, x4 = None, None, None, None, None

        start1 = datetime.now()
        for x0 in range(1, 100000):
            for x1 in range(1, 100000):
                if (check := beta0 == (
                    np.arcsin(np.sqrt(x1 ** 2)
                              / np.sqrt(x0 ** 2 + x1 ** 2 + 1e-12)))):
                    break
            if check:
                print("We spent on the selection "
                      f"of the first two elements: {datetime.now() - start1}")
                break

        return [x0, x1, x2, x3, x4]


# input_features = [10, 20, 30, 40, 50]
input_features = [35, 50, 65, 80, 90]
# input_features = [22393, 22962, 22689, 21849, 20979]
# input_features = [5, 4, 3, 2, 1]
# input_features = [99960, 99970, 99980, 99990, 99999]
# input_features = [3, 2, 1, 5, 4]


# Transform the features
features = np.array(get_angles_or_features(input_features))

print(f"input_features    >>> {input_features}")
print(f"features          >>> {features}")

# Restoring the original data input_features
restored_features = get_angles_or_features(features)

print(f"restored_features >>> {restored_features}")

Output to the console:

input_features    >>> [35, 50, 65, 80, 90]
features          >>> [ 2.3025178  -0.78539816  0.78539816 -0.96007036  0.96007036]
We spent on the selection of the first two elements: 0:00:25.827939
restored_features >>> [35, 50, None, None, None]

We are guaranteed to get the first two elements, but for this we used two nested for loops, the time complexity of this code is O(n^2) in the worst case.

Step 2

It is not possible to define the third element in the second step, you can only reduce the number of iterations for the third step.

import numpy as np
from datetime import datetime
from typing import Union, List


def get_angles_or_features(
        input: Union[List[int], np.ndarray[float]]
        ) -> Union[np.ndarray, List, None]:
    """"""

    input_type = type(input).__name__
    # Check type input
    if not (input_type == 'ndarray' or input_type == 'list'):
        print("Input Error")
        return None
    # Processing get angles
    elif input_type == 'list':
        beta0 = (
            2 * np.arcsin((np.sqrt(input[1] ** 2))
                          / np.sqrt(input[0] ** 2 + input[1] ** 2 + 1e-12)
                          )
        )
        beta1 = (
            2 * np.arcsin(np.sqrt(input[2] ** 2)
                          / np.sqrt(input[2] ** 2 + input[2] ** 2 + 1e-12)
                          )
        )
        beta2 = (
            2 * np.arcsin(np.linalg.norm(input[2:]) / np.linalg.norm(input))
        )

        return np.array([beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2])

    # Restoring data input_features
    elif input_type == 'ndarray':
        beta0, beta1 = input[4], input[2]

        x0, x1, x2, x3, x4 = None, None, None, None, None

        # Step 1
        start1 = datetime.now()
        for x0 in range(1, 100000):
            for x1 in range(1, 100000):
                if (check := beta0 == (
                    np.arcsin(np.sqrt(x1 ** 2)
                              / np.sqrt(x0 ** 2 + x1 ** 2 + 1e-12)))):
                    break
            if check:
                print("We spent on the selection "
                      f"of the first two elements: {datetime.now() - start1}"
                      )
                break
        # Step 2
        start2 = datetime.now()
        _x2 = tuple(x for x in range(1, 100000)
                    if beta1 == np.arcsin(np.sqrt(x ** 2)
                                          / np.sqrt(x ** 2 + x ** 2 + 1e-12)
                                          )
                    )
        end2 = datetime.now()
        print("Reduced future iterations from 100000 "
              f"to {(_len := len(_x2))} and wasted time: {end2 - start2}"
              )
    return [x0, x1, (type(_x2), f"len: {_len}"), x3, x4]


# input_features = [10, 20, 30, 40, 50]
input_features = [35, 50, 65, 80, 90]
# input_features = [22393, 22962, 22689, 21849, 20979]
# input_features = [5, 4, 3, 2, 1]
# input_features = [99960, 99970, 99980, 99990, 99999]
# input_features = [3, 2, 1, 5, 4]


# Transform the features
features = np.array(get_angles_or_features(input_features))

print(f"input_features    >>> {input_features}")
print(f"features          >>> {features}")

# Restoring the original data input_features
restored_features = get_angles_or_features(features)

print(f"restored_features >>> {restored_features}")

Output to the console:

input_features    >>> [35, 50, 65, 80, 90]
features          >>> [ 2.3025178  -0.78539816  0.78539816 -0.96007036  0.96007036]
We spent on the selection of the first two elements: 0:00:26.472476
Reduced future iterations from 100000 to 43309 and wasted time: 0:00:00.814264
restored_features >>> [35, 50, (<class 'tuple'>, 'len: 43309'), None, None]

However, 43309 iterations of the first for loop is too expensive...

This is due to the fact that only one element input_features is used to calculate beta1 — this increases the inverse variability.

beta1 = 2 * np.arcsin(np.sqrt(x[2] ** 2) / np.sqrt(x[2] ** 2 + x[2] ** 2 + 1e-12))

If it is acceptable to add one element to features for backward compatibility,
then the variability can be leveled.

Step 3

from typing import Union, Tuple, List
from datetime import datetime

import numpy as np


def get_angles_or_features(
        input: Union[List[int], np.ndarray[float]]
        ) -> Union[Tuple, None]:
    """"""

    input_type = type(input).__name__
    # Check type input
    if not (input_type == 'tuple' or input_type == 'list'):
        print("Input Error")
        return None

    # Processing get angles
    if input_type == 'list':
        beta0 = (
            2 * np.arcsin((np.sqrt(input[1] ** 2))
                          / np.sqrt(input[0] ** 2 + input[1] ** 2 + 1e-12)))
        beta1 = (
            2 * np.arcsin(np.sqrt(input[2] ** 2)
                          / np.sqrt(input[2] ** 2 + input[2] ** 2 + 1e-12)))
        beta2 = (
            2 * np.arcsin(np.linalg.norm(input[2:]) / np.linalg.norm(input)))

        return (np.array([beta2, -beta1 / 2, beta1 / 2,
                          -beta0 / 2, beta0 / 2]), input[2])
    # Conversion angles
    elif input_type == 'tuple':
        beta0, beta2 = input[0][4], input[0][0] / 2
        x2 = int(input[-1])
        start, end = (x2 - 3500 if x2 - 3500 >= 0 else 0,
                      x2 + 3501 if x2 + 3501 <= 43000 else 43000)
        # Defining x0 & x1
        _x0_x1 = tuple(
            (x0, x1)
            for x0 in range(start, end)
            for x1 in range(start, end)
            if (0.46 < x2 / (x0 + x1) < 0.51
                and (beta0 == np.arcsin(np.sqrt(x1 ** 2)
                     / np.sqrt(x0 ** 2 + x1 ** 2 + 1e-12)))
                )
            )[0]
        x0, x1 = _x0_x1
        # Defining x3 & x4
        regeneraite_features = (
            [x0, x1, x2, x3, x4]
            for x3 in range(start, end)
            for x4 in range(start, end)
            if (0.5 < x2 / (x3 + x4) < 0.54
                and (beta2 == np.arcsin(
                              np.linalg.norm([x2, x3, x4])
                              / np.linalg.norm([x0, x1, x2, x3, x4])))
                )
            )

        return tuple(regeneraite_features)


all_input_features = [
    [20979, 20583, 19433, 18988, 18687],
    [22689, 21849, 20979, 20583, 19433],
    [22962, 22689, 21849, 20979, 20583],
    [22393, 22962, 22689, 21849, 20979],
    [21849, 20979, 20583, 19433, 18988]
]

if __name__ == "__main__":
    for input_features in all_input_features:
        # Transform the features
        features = get_angles_or_features(input_features)

        print(f"\ninput_features     >>> {input_features}")
        print(f"features           >>> {features}")
        start_time = datetime.now()
        restored_features = get_angles_or_features(features)
        # print(f"restored_features  >>> {features}")
        print(f"restored_features  >>> {restored_features}")
        print(f"Duration of the recovery process: {datetime.now() - start_time}")

Output to the console:

input_features     >>> [20979, 20583, 19433, 18988, 18687]
features           >>> (array([ 1.6856525 , -0.78539816,  0.78539816, -0.77587052,  0.77587052]), 19433)        
restored_features  >>> ([20979, 20583, 19433, 18687, 18988], [20979, 20583, 19433, 18988, 18687])
Duration of the recovery process: 0:08:55.662949

input_features     >>> [22689, 21849, 20979, 20583, 19433]
features           >>> (array([ 1.68262106, -0.78539816,  0.78539816, -0.7665401 ,  0.7665401 ]), 20979)        
restored_features  >>> ([22689, 21849, 20979, 19433, 20583], [22689, 21849, 20979, 20583, 19433])
Duration of the recovery process: 0:09:29.221780

input_features     >>> [22962, 22689, 21849, 20979, 20583]
features           >>> (array([ 1.69663709, -0.78539816,  0.78539816, -0.77941808,  0.77941808]), 21849)        
restored_features  >>> ([22962, 22689, 21849, 19089, 22347], [22962, 22689, 21849, 20583, 20979], [22962, 22689, 21849, 20979, 20583], [22962, 22689, 21849, 22347, 19089])
Duration of the recovery process: 0:09:36.666942

input_features     >>> [22393, 22962, 22689, 21849, 20979]
features           >>> (array([ 1.73553479, -0.78539816,  0.78539816, -0.79794298,  0.79794298]), 22689)        
restored_features  >>> ([22393, 22962, 22689, 20979, 21849], [22393, 22962, 22689, 21849, 20979])
Duration of the recovery process: 0:10:10.256594

input_features     >>> [21849, 20979, 20583, 19433, 18988]
features           >>> (array([ 1.68858074, -0.78539816,  0.78539816, -0.76508714,  0.76508714]), 20583)        
restored_features  >>> ([21849, 20979, 20583, 18988, 19433], [21849, 20979, 20583, 19433, 18988])
Duration of the recovery process: 0:09:07.385657

Good. When you save the input[2] you have the opportunity to get an acceptable result,
but it is still not absolutely pure.

In addition, the duration of the recovery process is still very long.

Resume

We had to take these three steps to show that this approach is not effective enough.

When you process data that you will need in the future, keep the opportunity to access it.

At least this way...

from typing import List, Union, Tuple, Dict
import numpy as np

def get_angles(x: List[int]
               ) -> Union[Tuple[np.ndarray, tuple], Dict[tuple, list]]:
    """"""

    beta0 = 2 * np.arcsin(np.sqrt(x[1] ** 2) / np.sqrt(x[0] ** 2 + x[1] ** 2 + 1e-12))
    beta1 = 2 * np.arcsin(np.sqrt(x[2] ** 2) / np.sqrt(x[2] ** 2 + x[2] ** 2 + 1e-12))
    beta2 = 2 * np.arcsin(np.linalg.norm(x[2:]) / np.linalg.norm(x))
    
    return (np.array([beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2]),
            (x[0], x[1], x[2], x[3], x[4])
            )

    # or this way...
    return {(x[0], x[1], x[2], x[3], x[4]):
            [beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2]
            }

You can go even further...

from typing import List
import numpy as np


@freeze_arguments
def get_angles(x: List[int]) -> np.ndarray:
    """"""

    beta0 = 2 * np.arcsin(np.sqrt(x[1] ** 2) / np.sqrt(x[0] ** 2 + x[1] ** 2 + 1e-12))
    beta1 = 2 * np.arcsin(np.sqrt(x[2] ** 2) / np.sqrt(x[2] ** 2 + x[2] ** 2 + 1e-12))
    beta2 = 2 * np.arcsin(np.linalg.norm(x[2:]) / np.linalg.norm(x))
    
    return np.array([beta2, -beta1 / 2, beta1 / 2, -beta0 / 2, beta0 / 2])

But that's a completely different story!