Usage

3. How to use Undefined

This is the recommended usage. The users can interact with the package at their discretion.

Our package is published to the PyPI. Therefore, users can easily install the package via a pip command. We recommend users to utilize Python version 3.8 or newer.

Our package name on PyPI is undefined_AD, where AD stands for automatic differentiation.

Use the following command to install undefined:

1pip install undefined_AD

Import our supporting API to interact with the package. To do so, enter the following into the Python script. Details about the trace function can be found below:

1from undefined.API import trace

Also, if the users are planning to use the exponential and trig functions, they should import them from our package:

1from undefined.Calculator import sin, cos, tan, exp, log, sqrt

Please refer to the implementation page for a complete list of supported operations.

Note: Our package will not work with the functions in other libraries, such as np.sin. Please import our customized functions from undefined.Calculator.

Once imported successfully, users can calculate the derivative of a given function by using the syntax from the section below.

Here, we will demo the R -> R cases in the 3.1 and 3.2 to illustrate the basic usage of Undefined. In 3.3, we will demo the higher order inputs and outputs.

3.1 Forward Mode Demo

The trace function will calculate the derivative, which will intake a user defined function using lambda, the mode (default is forward mode), and associated values. The trace function will return the derivatives of the function.

Here, we showed a demo with \(\mathbb{R}\) -> \(\mathbb{R}\):

*Note:* In our design, the users do not need to instantiate an undefined object. They can just develop the function using Python build-in lambda, and we will not accept other types of input functions.

 1# R -> R implementation
 2# assuming undefined has been installed.
 3
 4from undefined.API import trace
 5from undefined.Calculator import sin, cos, exp, log, sqrt
 6
 7# define the user input function using lambda
 8f1 = lambda x, y: sqrt(exp(x*y)) + 1
 9
10# calculate the derivative when x = 2 and y = 1 for the input function
11output_f1 = trace(f1, x = 2, y = 1)
12
13# print out the forward mode derivative
14print(output_f1)
15
16# output summary results for f1 ({function values}, {derivative results})
17(3.72, [[1.359], [2.718]])

The users can also access the partial derivative (Jacobian) results by using the indexes, as the results is outputted in a tuple

1# use index 0 to access the function value
2print(output_f1[0])
3>>>3.72
4
5# use index 1 to access the derivative value (the Jacobian Matrix) of the function
6print(output_f1[1])
7>>>[[1.359], [2.718]]

In the derivative results (the Jacobian Matrix), the order is the same as the lambda function’s variable order. In this case, the first is the partial derivative for x and the other one is for y.

The trace function can also handle multiple dimensional calculation. Refer to section 3.3 below.

3.2 Reverse Mode Demo (Our Extension)

The trace function will also be able to calculate derivatives in reverse mode by specifying the mode parameters. Take the example below as a demo.

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin
 3
 4# user defined function
 5f = lambda x: sqrt(exp(sin(x)))
 6
 7# call the trace function in reverse mode, and provide input x = 2
 8print(trace(f, mode = "reverse", x = 2))
 9
10# the function will return the function value and the derivative when x = 2.
11>>> (1.58, [-0.328])

In the example, we can see that our reverse mode can output the function and the derivative values rounded in 3 decimal places, if applicable. Another feature we developed in our reverse mode is to output the computational graph.

First, let us look at the graph structure we generated by utilizing the same function for the example.

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin
 3
 4# user defined function
 5f = lambda x: sqrt(exp(sin(x)))
 6
 7# call the trace function in reverse mode, and provide input x = 2
 8# set plot equals to True so that it will generate the computational graph
 9print(trace(f, mode = "reverse", plot = True, x = 2))
10
11# Output: the function will return the function value and the derivative when x = 2.
12Computational Graph (1.58, UDPrimitive.SQRT)
13|
14|<-(parent)-Computational Graph (2.48, UDPrimitive.EXP)
15|      |
16|      |<-(parent)-Computational Graph (0.91, UDPrimitive.SIN)
17|      |      |
18|      |      |<-(parent)-Computational Graph (2, UDPrimitive.VAR)
19Figure saved as /15:12:16:188518.png
20(1.58, -0.328)

Moreover, the reverse mode will auto save the plot to your current working directory. The associated graph generated from the function above is shown below.

reverse_mode_example1

From the graph above, we can see that the graph correctly reflects to computation from the VAR to SIN to EXP and to SQRT.

Undefined, like the name suggested, has unlimited boundary. Let’s try a complicated example:

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin, tan
 3
 4# user defined function
 5f = lambda x, y: exp(1-6*x) * tan(4*x + 2*y) + x**2*y
 6
 7# call the trace function in reverse mode, and provide input x = 2
 8# set plot equals to True so that it will generate the computational graph
 9print(trace(f, mode = "reverse", plot = True, x = 1, y = 2))
10
11# Output: the function will return the function value and the derivative when x = 1, y = 2.
12Computational Graph (1.95, UDPrimitive.ADD)
13|
14|<-(parent)-Computational Graph (-0.05, UDPrimitive.MUL)
15|      |
16|      |<-(parent)-Computational Graph (0.01, UDPrimitive.EXP)
17|      |      |
18|      |      |<-(parent)-Computational Graph (-5, UDPrimitive.RSUB)
19|      |      |      |
20|      |      |      |<-(parent)-Computational Graph (6, UDPrimitive.RMUL)
21|      |      |      |      |
22|      |      |      |      |<-(parent)-Computational Graph (1, UDPrimitive.VAR)
23|      |
24|      |<-(parent)-Computational Graph (-6.8, UDPrimitive.TAN)
25|      |      |
26|      |      |<-(parent)-Computational Graph (8, UDPrimitive.ADD)
27|      |      |      |
28|      |      |      |<-(parent)-Computational Graph (4, UDPrimitive.RMUL)
29|      |      |      |      |
30|      |      |      |      |<-(parent)-Computational Graph (1, UDPrimitive.VAR)
31|      |      |      |
32|      |      |      |<-(parent)-Computational Graph (4, UDPrimitive.RMUL)
33|      |      |      |      |
34|      |      |      |      |<-(parent)-Computational Graph (2, UDPrimitive.VAR)
35|
36|<-(parent)-Computational Graph (2, UDPrimitive.MUL)
37|      |
38|      |<-(parent)-Computational Graph (1, UDPrimitive.POW)
39|      |      |
40|      |      |<-(parent)-Computational Graph (1, UDPrimitive.VAR)
41|      |
42|      |<-(parent)-Computational Graph (2, UDPrimitive.VAR)
43Figure saved as /15:13:13:665393.png
44(1.95, [[5.548], [1.637]])
reverse_mode_example2

From the results above, we can see that that undefined package can handle complicated functions. However, we do have some limitations. We will discuss that in the section below.

Of note: as we used the networkx library to achieve the graph, the graph will be different even if you run the same code twice or many different times.

3.3 Multiple Vectors Inputs and Outputs

Here, let us use the forward mode to demo how to use Undefined using multiple inputs. The reverse mode would result the same

\(\mathbb{R}^m -> \mathbb{R}\)

This means that we will need to calculate for multiple x input for the same function. We designed our function to use numpy.array to take multiple inputs. See the example below on how to pass in multiple inputs. Note: you will need to use the double bracket.

 1from undefined.API import trace
 2from undefined.Calculator import sqrt
 3import numpy as np
 4
 5# user defined function
 6f = lambda x: 2*x + sqrt(x)
 7
 8# call the trace function in undefined, and provide input x = 1 and 2.
 9print(trace(f, x = np.array([[1,2]])))
10
11# Output
12(array([[3.  , 5.41]]), [[2.5, 2.354]])

In the output above, the first array represents the function values and the second array represents the derivative values. You have likely noticed already that the two functions used here are the same ones used in the previous demo. Check out the values with the previous demo and you will see the values are indeed the same.

\(\mathbb{R} -> \mathbb{R}^n\)

This means that we will need to calculate for a single x input for the multiple functions. In this case, we designed our function to use a list to take multiple functions as inputs. See the example below on how to pass in multiple functions as input.

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin
 3
 4# user defined functions
 5f1 = lambda x: sqrt(exp(sin(x)))
 6f2 = lambda x: 2*x + sqrt(x)
 7
 8# call the trace function in undefined, and provide input functions f1 and f2, and the x value.
 9print(trace([f1, f2], x = 2))
10
11# Output
12(array([1.58, 5.41]), array([-0.328,  2.354]))

In the output above, the first array represents the function values and the second array represents the derivative values. I bet you have noticed already that the two functions I used here are the same ones I used in the previous demo. Check out the values with the previous demo and you will see the values are the same.

\(\mathbb{R}^m -> \mathbb{R}^n\)

This means that we will need to calculate for multiple x inputs for the multiple functions. It is the combination of both conditions above. See the example below on how to pass in multiple functions and values as input.

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin
 3
 4# user defined functions
 5f1 = lambda x: sqrt(exp(sin(x)))
 6f2 = lambda x: 2*x + sqrt(x)
 7
 8# call the trace function in undefined, and provide input functions f1 and f2, and the x values.
 9print(trace([f1, f2], x = np.array([[1, 2]])))
10
11# Output
12(array([[[1.52, 1.58]], [[3.  , 5.41]]]), array([[[ 0.411, -0.328]], [[ 2.5  ,  2.354]]]))

In the output above, the first array represents the function values and the second array represents the derivative values. The first 2D list in the first tuple is the function value from the first function, and the first 2D list in the second tuple is the derivative from the first section. The second 2D list corresponding to the second function from the input.

Additional Demo To maximize the flexibility for the users, our function can take a mixture as input, meaning the number of input values for variables do not need to be the same. For example, in the \(\mathbb{R}^m -> \mathbb{R}^n\), the users could input a function of x and y and give 2 values for x and 1 value for y. Our function would still work. See the example below:

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin
 3
 4# user defined functions
 5f3 = lambda x, y: x**2 + 2**y
 6f4 = lambda x, y: 2*x - 2/y
 7
 8# call the trace function in undefined, and provide input functions f3 and f4, and the x and y values.
 9print(trace([f3, f4], x = np.array([[1,2]]), y = 4))
10
11# Output
12(array([[[17. , 20. ]], [[ 1.5,  3.5]]]), array([[[ 2.,  4.], [11.09 , 11.09 ]], [[ 2.,  2.], [ 0.125,  0.125]]]))

When there are multiple input variables, in this case x and y, our program will order the results in the same order that it’s been passed into the function. In this case, the first item in the first list in the first array represents the function value from the f3 when x = 1 y = 4, and the second item is from f3 when x = 2, y = 4, etc. The second array represents the derivative value. The first list represent the derivative value of f3 when x = 1, y = 4 with respect to x and y, and the second list is the derivative value of f4 when x = 1, y = 4 with respect to x and y. The last two lists represent when x = 2, y = 4 for derivative values for f3 and f4 in that order.

3.4 Seeds Vector option

Since we used the Jacobian matrix, we provided the option for the users to define their own seed vector to decide which partial derivatives they want to take for the input function(s). You can use the seeds option in both forward and reverse modes. See the examples below on to use interact with seeds argument.

Our default setting for seed is to calculate the derivatives on the functions’ projection to the given variable, so 1 for the variable. However, you can define whatever projection you want in our package. See how you can implement below.

In the code below, we demo the usage for seed in one single function input with various number of variables.

Single Variable

When there is only one variable, you can just use int to set you seeds.

 1from undefined.API import trace
 2from undefined.Calculator import sqrt, exp, sin
 3
 4# user defined functions.
 5f1 = lambda x: sqrt(exp(sin(x))) + 2**x
 6
 7# define the seeds in the trace function
 8print(trace(f1, seeds = 1, x = 2))
 9
10# output ({function value}, {derivative value/Jacobian matrix})
11(5.58, 2.445)

Multiple Variable

In the case when there are multiple variables, you will need to use a numpy.array to pass the seed values in the seeds.

Note: You will need to use double bracket in the numpy.array when pass in the seeds vector.

 1import numpy as np
 2
 3f2 = lambda x, y: sqrt(exp(sin(x))) + 2**y
 4
 5# define the seeds for two variable in the trace function
 6print(trace(f2, seeds = np.array([[1, 0], [0, 1]]), x = 2, y = 1))
 7
 8# output ({function value}, {derivative value/Jacobian matrix multipled by the seeds vector})
 9(3.58, [[-0.328], [1.386]])
10
11# you could combine the seed with multiple inputs values
12print(trace(f2, seeds = np.array([[1, 0], [0, 1]]), x = np.array([[2, 5]]), y = 1))
13
14# output ({function value}, {derivative value/Jacobian matrix multipled by the seeds vector})
15(array([[3.58, 2.62]]), [[-0.328, 0.088], [1.386, 1.386]])
16
17# you could use seeds in the reverse mode as well
18print(trace(f2, mode = "reverse", seeds = np.array([[1, 0], [0, 1]]), x = np.array([[2, 5]]), y = 1))
19
20# output
21(array([[3.58, 2.62]]), [[-0.328, 0.088], [1.386, 1.386]])
22# it is expected that the results from forward and reverse modes are the same

3.5 A Few Tips

Although our package is smart and can handle many different scenarios and cases, there are exceptions.

  • We cannot unpack more number of input variables than the user defined functions have. For example, if the user defined function is the following:

1f = lambda x, y: x + exp(x)

Then the user passed additional variable into the trace function:

1trace(f, mode = "reverse", x = 2, y = 3)

In this case, we will not throw an error, but no guarantee the results are legit because the inputs does not make sense. So, please double check!

  • If you are using the forward mode, set the plot = True will not work as we do not store the intermediate values in the forward mode.

  • The position of each argument in trace is important as well. Please keep in mind that mode, plot and seeds need to be before you input the values for the variables.

  • We have tested our package with extreme values and edge cases to increase the robustness of our package. However, there is a chance that we did not exhaust possible cases.

3.6 Debugging

If there is an issue occurred, do not panic! It is expected. We offer a few words here if you have to debug a function or use case that we do not handle.

Since the forward model does not store the intermediate values, we recommend the users to use reverse mode for their debugging propose. We offer the graph structure and the computational graph as output to facilitate with the process. Moreover, we also provide the source codes for the users to examine our workflow. Please refer to the Code Details section. Since our design is encapsulated and modularized, it is easy for the users to spot the possible error(s).