.. _functions-as-config-params: Configurable functions ----------------------------------------------- The AC module allows the automatic configuration of *configurable* functions that are called inside other functions. These *configurable* functions can be *global* or *local* configurable functions, and can also be the *entrypoint* for the automatic configuration process. .. _global-configurable-functions: Global configurable functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Consider the following configurable function: .. code:: python :number-lines: from optilog.tuning import ac, Bool, Int, Real, Categorical @ac def func1( x, data, p1: Bool() = True, p2: Real(-1.3, 2) = 0, p3: Int(-5, 5) = 0, p4: Categorical("A", "B", "C") = "A"): ... # Does some computations and returns a float This function is used inside another configurable function, as it is shown below. .. code:: python :number-lines: import random @ac def func2(data, seed, n: Int(0, 5) = 3): random.seed(seed) val1 = func1(random.randint(10, 20), data) val2 = func1(random.randint(10, 20), data) return val1 * val2 * n For this case, *func1* is called using the same value for its configurable parameters. The *tuning* module automatically handles this case. When a function is decorated with the ``@ac`` decorator it becomes a *global configurable* function. By default, all the configurable functions will be called using the same values for their configurable parameters. .. _local-configurable-functions: Local configurable functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Consider the previous example where *func1* behaves as a :ref:`global configurable function ` inside *func2*: .. code:: python :number-lines: import random @ac def func2(data, seed, n: Int(0, 5) = 3): random.seed(seed) val1 = func1(random.randint(10, 20), data) val2 = func1(random.randint(10, 20), data) return val1 * val2 * n For this example, we will assume that the performance of *func2* may be improved if both calls to *func1* receive different values for its configurable parameters. Then, *func1* should behave as a *local* configurable function. We can achieve that using the ``CfgCall`` parameter type: .. code:: python :number-lines: import random from optilog.tuning import ac, Int, CfgCall @ac def func2( data, seed, func1_call1: CfgCall(func1), func1_call2: CfgCall(func1), n: Int(0, 5) = 3 ): random.seed(seed) val1 = func1_call1(random.randint(10, 20), data) val2 = func1_call2(random.randint(10, 20), data) return val1 * val2 * n The ``CfgCall`` parameter type receives a configurable function and allows its *local* configuration. For the example, two parameters are added to *func2*: *func1_call1* and *func1_call2*. These are two different instances of *func1* that can receive different values for the configurable parameters of each of them. Notice that ``CfgCall`` does not need a default value, because the configurable parameters are already annotated with default values. The configured callable gets injected by the `ac` decorator. The user doesn't need to provide any value for the parameter. .. _entrypoint-functions: Entrypoint functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An *entrypoint* is a function/blackbox that the AC tool will call to perform its configuration process. Any :ref:`global configurable function ` can be an *entrypoint* to the AC tool. Your *entrypoint* may also be a :ref:`BlackBox `. Consider the example function that we have discussed in the :ref:`Local configurable functions ` section: .. code:: python :number-lines: import random from optilog.tuning import ac, Int, CfgCall @ac def func2( data, seed, func1_call1: CfgCall(func1), func1_call2: CfgCall(func1), n: Int(0, 5) = 3 ): random.seed(seed) val1 = func1_call1(random.randint(10, 20), data) val2 = func1_call2(random.randint(10, 20), data) return val1 * val2 * n We can try to minimize the result returned by the *func2* function. To do that, we can make this function an *entrypoint* and configure the values for the *n* parameter and for the two calls to *func1* (which in this case is a *local* configurable function). We only have to print the value we want to optimize using any format: .. code:: python :number-lines: import random from optilog.tuning import ac, Int, CfgCall @ac def func2( data, seed, func1_call1: CfgCall(func1), func1_call2: CfgCall(func1), n: Int(0, 5) = 3 ): random.seed(seed) val1 = func1_call1(random.randint(10, 20), data) val2 = func1_call2(random.randint(10, 20), data) res = val1 * val2 * n print("Result:", res) return res Later, the user will have to provide the regular expression that is needed to capture the printed result when :ref:`initializing the Configurator class ` (for the example we could use the regular expression ``^Result: ([+-]?\d+(?:\.\d+)?)$`` to match for floats). During the execution of some functions, we might find and report intermediate results. Optilog is aware of this, and will only capture the last printed line that matches the regular expression. Finally, the user must know the list of *global* configurable functions that the *entrypoint* is calling (either directly or though another function), as we also have to specify them during the :ref:`initialization of the Configurator class ` for Optilog to configure them.