Search code examples
pythontransformationtrigonometry

Reverse function to obtain original input values


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.


Solution

  • 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!