Skip to main content

Effortlessly explore function behavior with automated batch analysis.

Project description

Functioneer

Author: Quinn Marsh
Date: February 02, 2025
PyPI: https://pypi.org/project/functioneer/

Functioneer lets you effortlessly explore function behavior with automated batch analysis. With just a few lines of code, you can queue up thousands or even millions of function evaluations, with various parameter combinations and/or optimizations. Retrieve structured results in formats like pandas for seamless integration into your workflows. Perfect for parameter sweeps, engineering simulations, and digital twin optimization.

Use cases

  • Analysis and Optimization of Digital Twins: Explore the design trade-space and understand performance of your simulated system.
  • Machine Learning and AI: Autonomously test thousands of architectures or other parameters for ML models (like neural networks) to see which perform best.
  • Your Imagination is the Limit: What function will you engineer?

How Functioneer Works

At its core, functioneer organizes analyses as tree where a set of parameters starts at the trunk and moves out towards the leaves. Along the way, the set of parameters 'flows' through a series of analysis steps (each of which can be defined in a single line of code). Each analysis step can modify or use the parameters in various ways, such as defining new parameters, modifying parameters, or using the parameters to evaluate or even optimize any function of your choice. One key feature of functioneer is the ability to introduce forks: a type of analysis step that splits the analysis into multiple parallel branches, each exploring different values for a specific parameter. Using many Forks in series allows you to queue up thousands or even millions of parameter combinations with only a few lines of code. This structured approach enables highly flexible and dynamic analyses, suitable for a wide range of applications.

Summary of most useful types of analysis steps:

  • Define: Adds a new parameter to the analysis
  • Fork: Splits the analysis into multiple parallel branches, each exploring different values for a specific parameter
  • Execute: Calls a provided function using the parameters
  • Optimize: Quickly set up an optimization by providing a function and defining which parameters are going to be optimized
Important Terms
  • AnalysisModule

    • Definition: The central container for an analysis pipeline.
    • Function: Holds a sequence of analysis steps and manages a set of parameters that flow through the pipeline.
  • Parameters

    • Definition: Named entities that represent inputs, intermediate values, or outputs of the analysis.
    • Function: Can be created, modified, or used in computations during analysis steps.
  • Analysis Steps

    • Definition: Individual operations performed during the analysis.
    • Function: Modify parameters by defining new ones, updating existing values, forking the analysis, or executing/optimizing functions.
  • Fork

    • Definition: A special type of analysis step that splits the pipeline into multiple branches.
    • Function: Creates independent branches where each branch explores a different value or configuration for a given parameter.
  • Branch

    • Definition: One of the independent paths created by a Fork.
    • Function: Represents a distinct variation of the analysis, each processing a specific set of parameter values.
  • Leaf

    • Definition: The endpoint of a branch after all analysis steps have been executed.
    • Function: Represents the final state of parameters for that branch. Each leaf corresponds to a specific combination of parameter values and results. When results are tabulated, each row corresponds to a leaf.

Installation

Install Functioneer directly from PyPI:

pip install functioneer

Getting Started

Below are a few quick examples of how to use Functioneer. Each example will build on the last, introducing one piece of functionality. By the end you will have witnessed the computational power of this fully armed and fully operational library.

Choose a Function to Analyze

Functioneer is designed to analyze ANY function(s) with ANY number of inputs and outputs. For the following examples, we use the Rosenbrock Function for (1) its relative simplicity, (2) 4 inputs (plenty to play with) and (3) its historical significance as an optimization benchmark.

# Example Function
# Rosenbrock function (known minimum of 0 at: x=1, y=1, a=1, b=100)
def rosenbrock(x, y, a, b):
    return (a-x)**2 + b*(y-x**2)**2

Example 1: The Basics (Defining Parameters and Executing a Function)

Set up an analysis sequence by defining four parameters to match our function, then executing the function Note: Parameter IDs MUST match your function's args, function executions inside functioneer are fully keyword arg based.

import functioneer as fn

# Create new analysis
anal = fn.AnalysisModule() # its not ānal is anál!

# Define analysis sequence
anal.add.define('a', 1) # Define parameter 'a'
anal.add.define('b', 100) # Define parameter 'b'
anal.add.define('x', 1) # Define parameter 'x'
anal.add.define('y', 1) # Define parameter 'y'

anal.add.execute(func=rosenbrock) # Execute function with parameter ids matched to kwargs

# Run the analysis sequence
results = anal.run()

print(results['df'])
Output:
   runtime  a    b  x  y  rosenbrock                   datetime
0      0.0  1  100  1  1           0 2025-06-24 03:26:48.842824

As we expect, the rosenbrock parameter evaluates to 0 when a=1, b=100, x=1, y=1

Note: the results['df'] is a pandas DataFrame containing all parameters in addition to runtime and datetime for the given branch

Example 2: Single Parameter Forks (Testing Variations of a Parameter)

Let's say you want to test a range of values for some parameters... If you want to test a set of values for a parameter you can create a fork in the analysis sequence. This splits the analysis into multiple branches, each exploring different values for a the given parameter.

Say we want to evaluate and plot the Rosenbrock surface over the x-y domain. Let's evaluate Rosenbrock on a grid where x=(0, 1, 2) and y=(1, 10) which should result in 6 final branches / leaves...

Note: the parameter's name is by default set from the function name, but can be overridden using the 'assign_to' arg. Note: some boiler plate can be removed by defining initial parameters in the AnalysisModule() declaration Note: initial parameter values will be overwritten as needed by parameter steps

# Create new analysis
init_params = dict(a=1, b=100, x=1, y=1) # define initial parameters
anal = fn.AnalysisModule(init_params)

# Define analysis sequence
anal.add.fork('x', value_set=(0, 1, 2)) # Fork analysis, create a branch for each value of 'x': 0, 1, 2
anal.add.fork('y', value_set=(1, 10)) # Fork analysis, create a branch for each value of 'y': 1, 10

anal.add.execute(func=rosenbrock, assign_to='brock_purdy') # Execute function (for each branch) with parameters matched to kwargs

# Run the analysis sequence
results = anal.run()
print(results['df'].drop(columns='datetime'))
Output:
    runtime  a    b  x   y  brock_purdy
0  0.001294  1  100  0   1          101
1  0.000000  1  100  0  10        10001
2  0.000000  1  100  1   1            0
3  0.000000  1  100  1  10         8100
4  0.000000  1  100  2   1          901
5  0.000000  1  100  2  10         3601

The parameters x and y were given 3 and 2 fork values respectively, this created 6 total leaves (end of each branch) in the analysis. rosen has been evaluated for each leaf. Essentially you have begun to map the Rosenbrock function over the x-y domain.

Example 3: Optimization

Let's say you want to find the local minimum of the Rosenbrock (optimize x and y) for several variations of a and b (different flavors Rosenbrock functions). You would fork the analysis at parameters a and b, then perform an optimization on each branch.

# Create new analysis
anal = fn.AnalysisModule(dict(x=0, y=0))

# Define analysis sequence
anal.add.fork('a', value_set=(1, 2)) # Fork analysis, create a branch for each value of 'a': 0, 1, 2
anal.add.fork('b', value_set=(0, 100, 200)) # Fork analysis, create a branch for each value of 'b': 0, 100, 200

anal.add.optimize(func=rosenbrock, opt_param_ids=('x', 'y'))

# Run the analysis sequence
results = anal.run()
print(results['df'].drop(columns='datetime'))
Output:
    runtime  a    b         x         y         rosen 
0  0.001017  1    0  1.000000  0.000000  4.930381e-32 
1  0.009276  1  100  0.999763  0.999523  5.772481e-08 
2  0.007347  1  200  0.999939  0.999873  8.146869e-09 
3  0.002572  2    0  2.000000  0.000000  0.000000e+00 
4  0.011093  2  100  1.999731  3.998866  4.067518e-07 
5  0.030206  2  200  1.999554  3.998225  2.136755e-07 

For each branch, the Rosenbrock Function has been minimized and the solution values for x, y and rosen are shown.

Note: the initial values (x0) used in the optimization are simply the existing parameter values (in this case x and y are 0) going into the optimization step. Note: due to optimization the runtimes for some of the analyses have gone up.

Example 4: Multi-parameter Forks

If you want to test specific combinations of parameters (instead of creating a grid) use a multi-parameter fork.

# Create new analysis
anal = fn.AnalysisModule(dict(a=1, b=100))

# Define analysis sequence
anal.add.fork.multi(('x', 'y'), value_sets=((0, 1, 2), (0, 10, 20))) # Fork analysis, create a branch for each value of 'y': 1, 10
anal.add.execute(func=rosenbrock) # Execute function (for each branch) with parameters matched to kwargs

# Run the analysis sequence
results = anal.run()
print(results['df'].drop(columns='datetime'))
Output:
   runtime  a    b  x   y  rosen
0      0.0  1  100  0   0      1
1      0.0  1  100  1  10   8100
2      0.0  1  100  2  20  25601

Notice 3 branches have been create for each combination of x and y: (x=0, y=0), (x=1, y=10), (x=2, y=20)

Example 5: Analysis Steps can be Conditional

Any analysis step can be given a conditional function that must return true at runtime or else the analysis step will be skipped. An example use case is when you want to skip an expensive analysis step if the parameters aren't looking "good".

As an arbitrary example, assume that we only care about cases where the optimized value of y is above 0.5. Also assume expensive_func is costly to run and we want to avoid running it when y<0.5.

# Create new analysis
anal = fn.AnalysisModule(dict(x=0, y=0))

# Define analysis sequence
anal.add.fork('a', value_set=(1, 2))
anal.add.fork('b', value_set=(0, 100, 200))
anal.add.optimize(func=rosenbrock, opt_param_ids=('x', 'y'))

# Only evaluate 'expensive_func' if the optimized 'y' is above 0.5
expensive_func = lambda x, y: x+y
anal.add.execute(func=expensive_func, output_param_ids='expensive_param', condition=lambda y: y>0.5)

results = anal.run()
print(results['df'].drop(columns='datetime'))
Output:
    runtime         x         y  a    b    rosenbrock  expensive_param
0  0.001997  1.000000  0.000000  1    0  4.930381e-32              NaN
1  0.004206  0.999763  0.999523  1  100  5.772481e-08         1.999286
2  0.012199  0.999939  0.999873  1  200  8.146869e-09         1.999811
3  0.000965  2.000000  0.000000  2    0  0.000000e+00              NaN
4  0.010121  1.999731  3.998866  2  100  4.067518e-07         5.998596
5  0.012308  1.999554  3.998225  2  200  2.136755e-07         5.997779 

Notice how the evaluation of expensive_param has been skipped where the optimized y did not meet our criteria y>0.5

License

This project is licensed under the MIT License.

You are free to use, modify, and distribute this software. Please include proper attribution by retaining the copyright notice in your copies or substantial portions of the software.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

functioneer-0.3.0.tar.gz (23.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

functioneer-0.3.0-py3-none-any.whl (22.6 kB view details)

Uploaded Python 3

File details

Details for the file functioneer-0.3.0.tar.gz.

File metadata

  • Download URL: functioneer-0.3.0.tar.gz
  • Upload date:
  • Size: 23.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.5

File hashes

Hashes for functioneer-0.3.0.tar.gz
Algorithm Hash digest
SHA256 66e36b7e6948582d7a99cca5b9b00c9870b4286d46d9b6f687021e126a6f3ceb
MD5 167363dac45c8edd644a1d0177bf9491
BLAKE2b-256 21c020c3ff408908d72f44b00435fae36a8da923dcd25c4819628178ff568a8f

See more details on using hashes here.

File details

Details for the file functioneer-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: functioneer-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 22.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.5

File hashes

Hashes for functioneer-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b428a58544b6247ad09e5ff07067088fe61fe012e66fe086054f2ea621c85ac
MD5 37f340818f21179f6ad7ce8662fe3b0d
BLAKE2b-256 7059b16690f8de21ddd0eacbc0c04e270af3814bee1fc98864bcc5ebb12dea4e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page