functioneer 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- functioneer-0.4.0/PKG-INFO +180 -0
- functioneer-0.4.0/README.md +149 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/pyproject.toml +2 -2
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer/__init__.py +1 -1
- functioneer-0.4.0/src/functioneer/analysis.py +293 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer/parameter.py +1 -1
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer/steps.py +31 -63
- functioneer-0.4.0/src/functioneer.egg-info/PKG-INFO +180 -0
- functioneer-0.3.0/PKG-INFO +0 -268
- functioneer-0.3.0/README.md +0 -237
- functioneer-0.3.0/src/functioneer/analysis.py +0 -372
- functioneer-0.3.0/src/functioneer.egg-info/PKG-INFO +0 -268
- {functioneer-0.3.0 → functioneer-0.4.0}/LICENSE +0 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/setup.cfg +0 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer/util.py +0 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer.egg-info/SOURCES.txt +0 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer.egg-info/dependency_links.txt +0 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer.egg-info/requires.txt +0 -0
- {functioneer-0.3.0 → functioneer-0.4.0}/src/functioneer.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: functioneer
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Effortlessly explore function behavior with automated batch analysis.
|
|
5
|
+
Author-email: Quinn Marsh <quinnmarsh@hotmail.com>
|
|
6
|
+
Maintainer-email: Quinn Marsh <quinnmarsh@hotmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/qthedoc/functioneer
|
|
9
|
+
Project-URL: Issues, https://github.com/qthedoc/functioneer/issues
|
|
10
|
+
Project-URL: Funding, https://donate.pypi.org
|
|
11
|
+
Project-URL: Say Thanks!, http://quinnmarsh.com
|
|
12
|
+
Keywords: functioneer,analysis,batch run,batch runner,automation,autorun,trade space,digital twin
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Requires-Python: >=3.7
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy>=1.18.5
|
|
28
|
+
Requires-Dist: scipy>=1.5.2
|
|
29
|
+
Requires-Dist: pandas>=1.0.5
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Functioneer
|
|
33
|
+
|
|
34
|
+
**Author**: Quinn Marsh\
|
|
35
|
+
**PyPI**: https://pypi.org/project/functioneer/
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Installation
|
|
42
|
+
```
|
|
43
|
+
pip install functioneer
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Full set of examples: [Examples.ipynb](https://github.com/qthedoc/functioneer/blob/main/examples/Examples.ipynb)*\
|
|
47
|
+
*This is currently the main form of documentation, lots of good stuff in there!
|
|
48
|
+
|
|
49
|
+
### Choose a Function to Analyze
|
|
50
|
+
Choose any function(s) you like. We use the [Rosenbrock Function](https://en.wikipedia.org/wiki/Rosenbrock_function) in these examples for its simplicity, many inputs and its historical significance as an optimization benchmark.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
import functioneer as fn
|
|
54
|
+
|
|
55
|
+
# Rosenbrock function (known minimum of 0 at: x=1, y=1, a=1, b=100)
|
|
56
|
+
def rosenbrock(x, y, a, b):
|
|
57
|
+
return (a-x)**2 + b*(y-x**2)**2
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Example 1: Forks and Function Evaluation (The Basics)
|
|
61
|
+
|
|
62
|
+
**Goal**: Test `rosenbrock` function with multiple values for parameters `x` and `y`.
|
|
63
|
+
|
|
64
|
+
Note: forks for `x` and `y` create a 'grid' of values\
|
|
65
|
+
Note: Parameter IDs MUST match your function's args, function evals inside functioneer are fully keyword arg based.
|
|
66
|
+
```
|
|
67
|
+
anal = fn.AnalysisModule() # Create new analysis
|
|
68
|
+
anal.add.define({'a': 1, 'b': 100}) # define a and b
|
|
69
|
+
anal.add.fork('x', (0, 1, 2)) # Fork analysis, create branches for x=0, x=1, x=2
|
|
70
|
+
anal.add.fork('y', (1, 10))
|
|
71
|
+
anal.add.execute(func=rosenbrock) #
|
|
72
|
+
results = anal.run()
|
|
73
|
+
print('Example 1 Output:')
|
|
74
|
+
print(results['df'][['a', 'b', 'x', 'y', 'rosenbrock']])
|
|
75
|
+
```
|
|
76
|
+
```
|
|
77
|
+
Example 1 Output:
|
|
78
|
+
a b x y rosenbrock
|
|
79
|
+
0 1 100 0 1 101
|
|
80
|
+
1 1 100 0 10 10001
|
|
81
|
+
2 1 100 1 1 0
|
|
82
|
+
3 1 100 1 10 8100
|
|
83
|
+
4 1 100 2 1 901
|
|
84
|
+
5 1 100 2 10 3601
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Example 2: Optimization
|
|
88
|
+
|
|
89
|
+
**Goal**: Optimize `x` and `y` to find the minimum `rosenbrock` value for various `a` and `b` values.
|
|
90
|
+
|
|
91
|
+
Note: values for `x` and `y` before optimization are used as initial guesses
|
|
92
|
+
```
|
|
93
|
+
anal = fn.AnalysisModule({'x': 0, 'y': 0})
|
|
94
|
+
anal.add.fork('a', (1, 2))
|
|
95
|
+
anal.add.fork('b', (0, 100, 200))
|
|
96
|
+
anal.add.optimize(func=rosenbrock, opt_param_ids=('x', 'y'))
|
|
97
|
+
results = anal.run()
|
|
98
|
+
print('\nExample 2 Output:')
|
|
99
|
+
print(results['df'][['a', 'b', 'x', 'y', 'rosenbrock']])
|
|
100
|
+
```
|
|
101
|
+
```
|
|
102
|
+
Example 2 Output:
|
|
103
|
+
a b x y rosenbrock
|
|
104
|
+
0 1 0 1.000000 0.000000 4.930381e-32
|
|
105
|
+
1 1 100 0.999763 0.999523 5.772481e-08
|
|
106
|
+
2 1 200 0.999939 0.999873 8.146869e-09
|
|
107
|
+
3 2 0 2.000000 0.000000 0.000000e+00
|
|
108
|
+
4 2 100 1.999731 3.998866 4.067518e-07
|
|
109
|
+
5 2 200 1.999554 3.998225 2.136755e-07
|
|
110
|
+
```
|
|
111
|
+
## Key Features
|
|
112
|
+
|
|
113
|
+
- **Test variations of a parameter with a single line of code:** Avoid writing deeply nested loops. Typically varying *n* parameters requires *n* nested loops... not anymore!
|
|
114
|
+
|
|
115
|
+
- **Quickly swap out optimization variables:** Most optimization libraries require your function to take in a list or array of values, BUT this makes it very annoying to remap your parameters to and from the array each time you simple want to change an optimization parameter!
|
|
116
|
+
|
|
117
|
+
- **Get results in a consistent easy to use format:** No more questions, the results are presented in a nice clean pandas data frame every time.
|
|
118
|
+
|
|
119
|
+
## Use cases
|
|
120
|
+
|
|
121
|
+
- **Analysis and Optimization of Digital Twins**: Explore the design trade-space and understand performance of your simulated system.
|
|
122
|
+
- **Machine Learning and AI**: Autonomously test thousands of architectures or other parameters for ML models (like neural networks) to see which perform best.
|
|
123
|
+
- **Your Imagination is the Limit**: What function will you engineer?
|
|
124
|
+
|
|
125
|
+
## How Functioneer Works
|
|
126
|
+
|
|
127
|
+
At its core, functioneer organizes analyses as a 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/overwriting 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 given 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.
|
|
128
|
+
|
|
129
|
+
Summary of most useful types of *analysis steps*:
|
|
130
|
+
- Define: Adds a new parameter to the analysis
|
|
131
|
+
- Fork: Splits the analysis into multiple parallel *branches*, each exploring different values for a specific parameter
|
|
132
|
+
- Execute: Calls a provided function using the parameters
|
|
133
|
+
- Optimize: Quickly set up an optimization by providing a function and defining which parameters are going to be optimized
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary>
|
|
137
|
+
<span style="font-size:1.5em;">Important Terms</span>
|
|
138
|
+
</summary>
|
|
139
|
+
|
|
140
|
+
* AnalysisModule
|
|
141
|
+
* Definition: The central container for an analysis pipeline.
|
|
142
|
+
* Function: Holds a sequence of analysis steps and manages a set of parameters that flow through the pipeline.
|
|
143
|
+
|
|
144
|
+
* Parameters
|
|
145
|
+
* Definition: Named entities that represent inputs, intermediate values, or outputs of the analysis.
|
|
146
|
+
* Function: Can be created, modified, or used in computations during analysis steps.
|
|
147
|
+
|
|
148
|
+
* Analysis Steps
|
|
149
|
+
* Definition: Individual operations performed during the analysis.
|
|
150
|
+
* Function: Modify parameters by defining new ones, updating existing values, forking the analysis, or executing/optimizing functions.
|
|
151
|
+
|
|
152
|
+
* Fork
|
|
153
|
+
* Definition: A special type of *analysis step* that splits the pipeline into multiple branches.
|
|
154
|
+
* Function: Creates independent branches where each branch explores a different value or configuration for a given parameter.
|
|
155
|
+
|
|
156
|
+
* Branch
|
|
157
|
+
* Definition: One of the independent paths created by a Fork.
|
|
158
|
+
* Function: Represents a distinct variation of the analysis, each processing a specific set of parameter values.
|
|
159
|
+
|
|
160
|
+
* Leaf
|
|
161
|
+
* Definition: The endpoint of a branch after all analysis steps have been executed.
|
|
162
|
+
* 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.
|
|
163
|
+
</details>
|
|
164
|
+
|
|
165
|
+
## Inspiration
|
|
166
|
+
I wanted to be an Analysis Ninja... effortlessly swapping parameters and optimization variables and most importantly getting results quickly! But manually rearranging code for what seemed like simple asks was really baking my noodle. Simple things like adding a variable to the analysis, or swapping out an optimization variable, required a shocking amount of code rework. Thus Functioneer was born.
|
|
167
|
+
|
|
168
|
+
## Acknowledgments
|
|
169
|
+
Thanks to the amazing open source communities: Python, numpy, pandas, etc that make this possible.
|
|
170
|
+
|
|
171
|
+
Thanks to LightManufacturing, where I had the opportunity to develop advanced digital twins for solar thermal facilities... and then analyze them. It was here, where the seed for Functioneer was planted.
|
|
172
|
+
|
|
173
|
+
Thanks to God for incepting my mind with what seemed like the craziest idea at the time: to structure an analysis as a pipeline of *analysis steps* with the *parameters* flowing thru like water.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
|
178
|
+
|
|
179
|
+
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.
|
|
180
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Functioneer
|
|
2
|
+
|
|
3
|
+
**Author**: Quinn Marsh\
|
|
4
|
+
**PyPI**: https://pypi.org/project/functioneer/
|
|
5
|
+
|
|
6
|
+
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.
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
### Installation
|
|
11
|
+
```
|
|
12
|
+
pip install functioneer
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Full set of examples: [Examples.ipynb](https://github.com/qthedoc/functioneer/blob/main/examples/Examples.ipynb)*\
|
|
16
|
+
*This is currently the main form of documentation, lots of good stuff in there!
|
|
17
|
+
|
|
18
|
+
### Choose a Function to Analyze
|
|
19
|
+
Choose any function(s) you like. We use the [Rosenbrock Function](https://en.wikipedia.org/wiki/Rosenbrock_function) in these examples for its simplicity, many inputs and its historical significance as an optimization benchmark.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
import functioneer as fn
|
|
23
|
+
|
|
24
|
+
# Rosenbrock function (known minimum of 0 at: x=1, y=1, a=1, b=100)
|
|
25
|
+
def rosenbrock(x, y, a, b):
|
|
26
|
+
return (a-x)**2 + b*(y-x**2)**2
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Example 1: Forks and Function Evaluation (The Basics)
|
|
30
|
+
|
|
31
|
+
**Goal**: Test `rosenbrock` function with multiple values for parameters `x` and `y`.
|
|
32
|
+
|
|
33
|
+
Note: forks for `x` and `y` create a 'grid' of values\
|
|
34
|
+
Note: Parameter IDs MUST match your function's args, function evals inside functioneer are fully keyword arg based.
|
|
35
|
+
```
|
|
36
|
+
anal = fn.AnalysisModule() # Create new analysis
|
|
37
|
+
anal.add.define({'a': 1, 'b': 100}) # define a and b
|
|
38
|
+
anal.add.fork('x', (0, 1, 2)) # Fork analysis, create branches for x=0, x=1, x=2
|
|
39
|
+
anal.add.fork('y', (1, 10))
|
|
40
|
+
anal.add.execute(func=rosenbrock) #
|
|
41
|
+
results = anal.run()
|
|
42
|
+
print('Example 1 Output:')
|
|
43
|
+
print(results['df'][['a', 'b', 'x', 'y', 'rosenbrock']])
|
|
44
|
+
```
|
|
45
|
+
```
|
|
46
|
+
Example 1 Output:
|
|
47
|
+
a b x y rosenbrock
|
|
48
|
+
0 1 100 0 1 101
|
|
49
|
+
1 1 100 0 10 10001
|
|
50
|
+
2 1 100 1 1 0
|
|
51
|
+
3 1 100 1 10 8100
|
|
52
|
+
4 1 100 2 1 901
|
|
53
|
+
5 1 100 2 10 3601
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Example 2: Optimization
|
|
57
|
+
|
|
58
|
+
**Goal**: Optimize `x` and `y` to find the minimum `rosenbrock` value for various `a` and `b` values.
|
|
59
|
+
|
|
60
|
+
Note: values for `x` and `y` before optimization are used as initial guesses
|
|
61
|
+
```
|
|
62
|
+
anal = fn.AnalysisModule({'x': 0, 'y': 0})
|
|
63
|
+
anal.add.fork('a', (1, 2))
|
|
64
|
+
anal.add.fork('b', (0, 100, 200))
|
|
65
|
+
anal.add.optimize(func=rosenbrock, opt_param_ids=('x', 'y'))
|
|
66
|
+
results = anal.run()
|
|
67
|
+
print('\nExample 2 Output:')
|
|
68
|
+
print(results['df'][['a', 'b', 'x', 'y', 'rosenbrock']])
|
|
69
|
+
```
|
|
70
|
+
```
|
|
71
|
+
Example 2 Output:
|
|
72
|
+
a b x y rosenbrock
|
|
73
|
+
0 1 0 1.000000 0.000000 4.930381e-32
|
|
74
|
+
1 1 100 0.999763 0.999523 5.772481e-08
|
|
75
|
+
2 1 200 0.999939 0.999873 8.146869e-09
|
|
76
|
+
3 2 0 2.000000 0.000000 0.000000e+00
|
|
77
|
+
4 2 100 1.999731 3.998866 4.067518e-07
|
|
78
|
+
5 2 200 1.999554 3.998225 2.136755e-07
|
|
79
|
+
```
|
|
80
|
+
## Key Features
|
|
81
|
+
|
|
82
|
+
- **Test variations of a parameter with a single line of code:** Avoid writing deeply nested loops. Typically varying *n* parameters requires *n* nested loops... not anymore!
|
|
83
|
+
|
|
84
|
+
- **Quickly swap out optimization variables:** Most optimization libraries require your function to take in a list or array of values, BUT this makes it very annoying to remap your parameters to and from the array each time you simple want to change an optimization parameter!
|
|
85
|
+
|
|
86
|
+
- **Get results in a consistent easy to use format:** No more questions, the results are presented in a nice clean pandas data frame every time.
|
|
87
|
+
|
|
88
|
+
## Use cases
|
|
89
|
+
|
|
90
|
+
- **Analysis and Optimization of Digital Twins**: Explore the design trade-space and understand performance of your simulated system.
|
|
91
|
+
- **Machine Learning and AI**: Autonomously test thousands of architectures or other parameters for ML models (like neural networks) to see which perform best.
|
|
92
|
+
- **Your Imagination is the Limit**: What function will you engineer?
|
|
93
|
+
|
|
94
|
+
## How Functioneer Works
|
|
95
|
+
|
|
96
|
+
At its core, functioneer organizes analyses as a 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/overwriting 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 given 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.
|
|
97
|
+
|
|
98
|
+
Summary of most useful types of *analysis steps*:
|
|
99
|
+
- Define: Adds a new parameter to the analysis
|
|
100
|
+
- Fork: Splits the analysis into multiple parallel *branches*, each exploring different values for a specific parameter
|
|
101
|
+
- Execute: Calls a provided function using the parameters
|
|
102
|
+
- Optimize: Quickly set up an optimization by providing a function and defining which parameters are going to be optimized
|
|
103
|
+
|
|
104
|
+
<details>
|
|
105
|
+
<summary>
|
|
106
|
+
<span style="font-size:1.5em;">Important Terms</span>
|
|
107
|
+
</summary>
|
|
108
|
+
|
|
109
|
+
* AnalysisModule
|
|
110
|
+
* Definition: The central container for an analysis pipeline.
|
|
111
|
+
* Function: Holds a sequence of analysis steps and manages a set of parameters that flow through the pipeline.
|
|
112
|
+
|
|
113
|
+
* Parameters
|
|
114
|
+
* Definition: Named entities that represent inputs, intermediate values, or outputs of the analysis.
|
|
115
|
+
* Function: Can be created, modified, or used in computations during analysis steps.
|
|
116
|
+
|
|
117
|
+
* Analysis Steps
|
|
118
|
+
* Definition: Individual operations performed during the analysis.
|
|
119
|
+
* Function: Modify parameters by defining new ones, updating existing values, forking the analysis, or executing/optimizing functions.
|
|
120
|
+
|
|
121
|
+
* Fork
|
|
122
|
+
* Definition: A special type of *analysis step* that splits the pipeline into multiple branches.
|
|
123
|
+
* Function: Creates independent branches where each branch explores a different value or configuration for a given parameter.
|
|
124
|
+
|
|
125
|
+
* Branch
|
|
126
|
+
* Definition: One of the independent paths created by a Fork.
|
|
127
|
+
* Function: Represents a distinct variation of the analysis, each processing a specific set of parameter values.
|
|
128
|
+
|
|
129
|
+
* Leaf
|
|
130
|
+
* Definition: The endpoint of a branch after all analysis steps have been executed.
|
|
131
|
+
* 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.
|
|
132
|
+
</details>
|
|
133
|
+
|
|
134
|
+
## Inspiration
|
|
135
|
+
I wanted to be an Analysis Ninja... effortlessly swapping parameters and optimization variables and most importantly getting results quickly! But manually rearranging code for what seemed like simple asks was really baking my noodle. Simple things like adding a variable to the analysis, or swapping out an optimization variable, required a shocking amount of code rework. Thus Functioneer was born.
|
|
136
|
+
|
|
137
|
+
## Acknowledgments
|
|
138
|
+
Thanks to the amazing open source communities: Python, numpy, pandas, etc that make this possible.
|
|
139
|
+
|
|
140
|
+
Thanks to LightManufacturing, where I had the opportunity to develop advanced digital twins for solar thermal facilities... and then analyze them. It was here, where the seed for Functioneer was planted.
|
|
141
|
+
|
|
142
|
+
Thanks to God for incepting my mind with what seemed like the craziest idea at the time: to structure an analysis as a pipeline of *analysis steps* with the *parameters* flowing thru like water.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "functioneer"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
authors = [{ name = "Quinn Marsh", email = "quinnmarsh@hotmail.com" }]
|
|
9
9
|
maintainers = [{ name = "Quinn Marsh", email = "quinnmarsh@hotmail.com" }]
|
|
10
10
|
description = "Effortlessly explore function behavior with automated batch analysis."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
license = { text = "MIT" }
|
|
13
|
-
keywords = ["functioneer", "analysis", "batch run", "automation", "autorun", "trade space", "digital twin"]
|
|
13
|
+
keywords = ["functioneer", "analysis", "batch run", "batch runner", "automation", "autorun", "trade space", "digital twin"]
|
|
14
14
|
requires-python = ">=3.7"
|
|
15
15
|
dependencies = [
|
|
16
16
|
"numpy>=1.18.5",
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Quinn Marsh
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
20
|
+
# THE SOFTWARE.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
24
|
+
import copy
|
|
25
|
+
import pandas as pd
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
from functioneer.steps import AnalysisStep, Define, Fork, Execute, Optimize
|
|
30
|
+
from functioneer.parameter import ParameterSet, Parameter
|
|
31
|
+
from functioneer.util import call_with_matched_kwargs
|
|
32
|
+
|
|
33
|
+
## TODO: work towards staged analysis (eg. needed for pick best 10)
|
|
34
|
+
# allow for a tuple of dicts for starting with multiple parameter sets,
|
|
35
|
+
# also might allow sending in of the pandas datafram to add to it (if not just append new rows when done with sub analysis)
|
|
36
|
+
# pandas_to_paramsets: a function that takes in a pd and returns a tuple of paramsets ready to be fed into the next stage of analysis
|
|
37
|
+
class AnalysisModule():
|
|
38
|
+
"""
|
|
39
|
+
The central container for an analysis pipeline in functioneer.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
init_param_values : dict, optional
|
|
44
|
+
Initial parameter values as a dictionary with parameter IDs (str) as keys and their values.
|
|
45
|
+
Parameter IDs must be valid (non-empty strings, not reserved names like 'runtime' or 'datetime').
|
|
46
|
+
Defaults to an empty dictionary.
|
|
47
|
+
name : str, optional
|
|
48
|
+
Name of the analysis module. Defaults to an empty string.
|
|
49
|
+
|
|
50
|
+
Raises
|
|
51
|
+
------
|
|
52
|
+
ValueError
|
|
53
|
+
If init_param_values is not a dictionary, contains invalid parameter IDs, or includes reserved names.
|
|
54
|
+
TypeError
|
|
55
|
+
If name is not a string.
|
|
56
|
+
"""
|
|
57
|
+
def __init__(self, init_param_values={}, name='') -> None:
|
|
58
|
+
""" Initialize Functioneer Analysis
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
init_param_values : dict, optional
|
|
62
|
+
Initial parameter values as a dictionary with parameter IDs (str) as keys and their values.
|
|
63
|
+
Parameter IDs must be valid (non-empty strings, not reserved names like 'runtime' or 'datetime').
|
|
64
|
+
Defaults to an empty dictionary.
|
|
65
|
+
name : str, optional
|
|
66
|
+
Name of the analysis module. Defaults to an empty string.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If init_param_values is not a dictionary, contains invalid parameter IDs, or includes reserved names.
|
|
70
|
+
TypeError: If name is not a string.
|
|
71
|
+
"""
|
|
72
|
+
if not isinstance(name, str):
|
|
73
|
+
raise TypeError(f"Invalid AnalysisModule: 'name' must be a string, got {type(name)}")
|
|
74
|
+
if not isinstance(init_param_values, dict):
|
|
75
|
+
raise ValueError(f"Invalid AnalysisModule: 'init_param_values' must be a dictionary, got {type(init_param_values)}")
|
|
76
|
+
for param_id in init_param_values:
|
|
77
|
+
Parameter.validate_id(param_id)
|
|
78
|
+
|
|
79
|
+
self.name = name
|
|
80
|
+
self.sequence: list[AnalysisStep] = []
|
|
81
|
+
self.init_paramset: ParameterSet = ParameterSet()
|
|
82
|
+
self.init_paramset.update_param_values(init_param_values)
|
|
83
|
+
self.init_paramset.add_param(Parameter('runtime', 0))
|
|
84
|
+
self.finished_leaves:int = 0
|
|
85
|
+
|
|
86
|
+
# Namespaces
|
|
87
|
+
self.add = self.AddNamespace(self) # Instantiate the namespace
|
|
88
|
+
|
|
89
|
+
# Results and Metadata
|
|
90
|
+
self.df: pd.DataFrame = pd.DataFrame()
|
|
91
|
+
self.t0 = None
|
|
92
|
+
self.leaf_data: List[Dict[str, Any]] = []
|
|
93
|
+
self.runtime: Optional[float] = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
class AddNamespace:
|
|
99
|
+
"""Namespace for adding different types of analysis steps."""
|
|
100
|
+
def __init__(self, parent):
|
|
101
|
+
self.parent: AnalysisModule = parent
|
|
102
|
+
|
|
103
|
+
def __call__(self,
|
|
104
|
+
analysis_object: AnalysisStep
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Manually append an AnalysisStep object to the Analysis
|
|
107
|
+
"""
|
|
108
|
+
# TODO Validate AnalysisStep
|
|
109
|
+
|
|
110
|
+
# TODO analysis_object.initialize(self.sequence)
|
|
111
|
+
|
|
112
|
+
self.parent.sequence.append(analysis_object)
|
|
113
|
+
|
|
114
|
+
def define(self, param_or_dict: Union[str, Dict[str, Any]], value: Any = None, condition: Optional[Callable[..., bool]] = None) -> None:
|
|
115
|
+
"""Define a parameter with a single ID and value or a dictionary of parameters.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
param_or_dict: Parameter ID (str) or dictionary of parameter IDs to values.
|
|
119
|
+
value: Value for the parameter (if param_or_dict is a string).
|
|
120
|
+
condition: Optional condition function to determine if the step should run.
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
>>> anal.add.define('a', 1) # Define single parameter
|
|
124
|
+
>>> anal.add.define({'a': 1, 'b': 100}) # Define multiple parameters
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: If inputs are invalid (e.g., wrong types, missing value, or value provided with dict).
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(param_or_dict, dict):
|
|
130
|
+
if value is not None:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"When defining multiple parameters with a dictionary, the 'value' argument is ignored. "
|
|
133
|
+
"Use either define(param_id: str, value: Any) or define(params: Dict[str, Any])."
|
|
134
|
+
)
|
|
135
|
+
for param_id, val in param_or_dict.items():
|
|
136
|
+
self.parent.sequence.append(Define(param_id, val, condition))
|
|
137
|
+
elif isinstance(param_or_dict, str) and value is not None:
|
|
138
|
+
self.parent.sequence.append(Define(param_or_dict, value, condition))
|
|
139
|
+
else:
|
|
140
|
+
raise ValueError("Expected (param_id, value) or {param_id: value, ...}")
|
|
141
|
+
|
|
142
|
+
def fork(self, param_or_dict: Union[str, Dict[str, Tuple[Any, ...]]], value_set: Optional[Tuple[Any, ...]] = None, condition: Optional[Callable[..., bool]] = None) -> None:
|
|
143
|
+
"""Fork analysis with a single parameter or dictionary of parameter value sets.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
param_or_dict: Parameter ID (str) or dictionary of parameter IDs to value sets.
|
|
147
|
+
value_set: Tuple of values for the parameter (if param_or_dict is a string).
|
|
148
|
+
condition: Optional condition function to determine if the step should run.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
>>> anal.add.fork('x', (0, 1, 2)) # Fork single parameter
|
|
152
|
+
>>> anal.add.fork({'x': (0, 1, 2), 'y': (0, 10, 20)}) # Fork multiple parameters
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If inputs are invalid (e.g., wrong types, missing value_set, or value_set provided with dict).
|
|
156
|
+
"""
|
|
157
|
+
if isinstance(param_or_dict, dict):
|
|
158
|
+
if value_set is not None:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"When forking multiple parameters with a dictionary, the 'value_set' argument is ignored. "
|
|
161
|
+
"Use either fork(param_id: str, value_set: Tuple[Any, ...]) or fork(params: Dict[str, Tuple[Any, ...]])."
|
|
162
|
+
)
|
|
163
|
+
self.parent.sequence.append(Fork(param_or_dict, condition))
|
|
164
|
+
elif isinstance(param_or_dict, str) and value_set is not None:
|
|
165
|
+
self.parent.sequence.append(Fork({param_or_dict: value_set}, condition))
|
|
166
|
+
else:
|
|
167
|
+
raise ValueError("Expected (param_id, value_set) or {param_id: value_set, ...}")
|
|
168
|
+
|
|
169
|
+
def execute(self, func: Callable[..., Any], assign_to: Optional[Union[str, List[str], Tuple[str, ...]]] = None, unpack_result: bool = False, condition: Optional[Callable[..., bool]] = None) -> None:
|
|
170
|
+
"""Execute a function and store its result in the ParameterSet.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
func: Function to execute, taking parameter values as input.
|
|
174
|
+
assign_to: Custom Parameter ID(s) to store the result (str or list/tuple of strings).
|
|
175
|
+
unpack_result: If True, unpacks a dictionary result into multiple parameters.
|
|
176
|
+
condition: Optional condition function to determine if the step should run.
|
|
177
|
+
|
|
178
|
+
Examples:
|
|
179
|
+
>>> anal.add.execute(my_function) # Execute function, store result
|
|
180
|
+
>>> anal.add.execute(my_function, assign_to='new_param') # Execute function, store result to param: 'new_param'
|
|
181
|
+
>>> anal.add.execute(my_function_returns_dict, unpack_result=True) # Unpack dict result
|
|
182
|
+
>>> anal.add.execute(my_function_returns_dict, assign_to=['out_1', 'out_2'], unpack_result=True) # Unpack only dict keys: out_1, out_2
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: If func is not callable or assign_to is invalid.
|
|
186
|
+
"""
|
|
187
|
+
self.parent.sequence.append(Execute(func, assign_to, unpack_result, condition))
|
|
188
|
+
|
|
189
|
+
def optimize(self, func: Callable[..., float], opt_param_ids: Tuple[str, ...], assign_to: Optional[str] = None, direction: str = 'min', optimizer: Union[str, Callable] = 'SLSQP', tol: Optional[float] = None, bounds: Optional[Dict[str, Tuple[float, float]]] = None, options: Optional[Dict[str, Any]] = None, condition: Optional[Callable[..., bool]] = None, **kwargs) -> None:
|
|
190
|
+
"""Optimize a function over specified parameters.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
func: Objective function to optimize, returning a scalar.
|
|
194
|
+
opt_param_ids: Parameter IDs to optimize.
|
|
195
|
+
assign_to: Parameter ID to store the optimized value.
|
|
196
|
+
direction: Optimization direction ('min' or 'max').
|
|
197
|
+
optimizer: Optimization method (SciPy method name or callable).
|
|
198
|
+
tol: Tolerance for convergence.
|
|
199
|
+
bounds: Dictionary of parameter IDs to (min, max) tuples.
|
|
200
|
+
options: Additional optimizer options (e.g., maxiter, ftol).
|
|
201
|
+
condition: Optional condition function to determine if the step should run.
|
|
202
|
+
**kwargs: Additional arguments for the optimizer.
|
|
203
|
+
|
|
204
|
+
Examples:
|
|
205
|
+
>>> anal.add.optimize(rosenbrock, ('x', 'y')) # Minimize with SLSQP
|
|
206
|
+
>>> anal.add.optimize(rosenbrock_neg, ('x', 'y'), direction='max', optimizer='Nelder-Mead') # Maximize with Nelder-Mead
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
ValueError: If inputs are invalid (e.g., invalid direction, optimizer).
|
|
210
|
+
"""
|
|
211
|
+
self.parent.sequence.append(Optimize(func, opt_param_ids, assign_to, direction, optimizer, tol, bounds, options, condition, **kwargs))
|
|
212
|
+
|
|
213
|
+
def run(self,
|
|
214
|
+
# create_pandas = True,
|
|
215
|
+
# verbose = True
|
|
216
|
+
) -> Dict[str, Any]:
|
|
217
|
+
"""Execute the analysis sequence and return results including a DataFrame of leaf data.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Dict[str, Any]: Dictionary containing the results DataFrame, runtime, and number of finished leaves.
|
|
221
|
+
"""
|
|
222
|
+
self.t0 = time.time()
|
|
223
|
+
self.finished_leaves = 0
|
|
224
|
+
self.leaf_data = [] # Reset leaf data
|
|
225
|
+
try:
|
|
226
|
+
self._process(self.init_paramset, step_idx=0) # start on step 0
|
|
227
|
+
except Exception as e:
|
|
228
|
+
raise RuntimeError(f"Analysis failed: {str(e)}") from e
|
|
229
|
+
finally:
|
|
230
|
+
self.runtime = time.time() - self.t0
|
|
231
|
+
|
|
232
|
+
# Create DataFrame from collected leaf data
|
|
233
|
+
df = pd.DataFrame(self.leaf_data) if self.leaf_data else pd.DataFrame()
|
|
234
|
+
|
|
235
|
+
return dict(
|
|
236
|
+
df = df,
|
|
237
|
+
runtime = self.runtime,
|
|
238
|
+
finished_leaves = self.finished_leaves,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _process(self, paramset: ParameterSet, step_idx:int):
|
|
242
|
+
"""
|
|
243
|
+
Run a step of the analysis sequence and recursively process the next step.
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
# Terminate branch if no more steps
|
|
247
|
+
if step_idx >= len(self.sequence):
|
|
248
|
+
self._end_sequence(paramset)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Get current step
|
|
252
|
+
step: AnalysisStep = self.sequence[step_idx]
|
|
253
|
+
|
|
254
|
+
# Check step condition
|
|
255
|
+
try:
|
|
256
|
+
run_step = paramset.call_with_matched_kwargs(step.condition) if step.condition else True
|
|
257
|
+
except Exception as e:
|
|
258
|
+
raise RuntimeError(f"Error evaluating step condition: {str(e)}") from e
|
|
259
|
+
|
|
260
|
+
# Run the analysis step
|
|
261
|
+
t0 = time.time()
|
|
262
|
+
try:
|
|
263
|
+
new_paramsets = step.run(paramset) if run_step else (copy.deepcopy(paramset),)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
raise RuntimeError(f"Error executing step: {str(e)}") from e
|
|
266
|
+
step_runtime = time.time() - t0
|
|
267
|
+
|
|
268
|
+
# Handle branch termination
|
|
269
|
+
if new_paramsets is None:
|
|
270
|
+
self._end_sequence(paramset)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# Validate new paramsets
|
|
274
|
+
if not isinstance(new_paramsets, tuple) or not all(isinstance(ps, ParameterSet) for ps in new_paramsets):
|
|
275
|
+
raise ValueError(f"Invalid step result, Expected tuple of ParameterSet, got {type(new_paramsets)}")
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
step_type = type(step).__name__
|
|
279
|
+
details = step.get_details()
|
|
280
|
+
raise RuntimeError(f"Error in {step_type} step at index {step_idx} with details {details}: {str(e)}") from e
|
|
281
|
+
|
|
282
|
+
# Process next step for each new parameter set (recursively)
|
|
283
|
+
for ps in new_paramsets:
|
|
284
|
+
ps.update_param('runtime', value=ps.get_value('runtime', 0.0) + step_runtime) # Update cumulative runtime
|
|
285
|
+
self._process(ps, step_idx + 1)
|
|
286
|
+
|
|
287
|
+
def _end_sequence(self, paramset: ParameterSet) -> None:
|
|
288
|
+
"""Record data for a completed analysis leaf (the end of a branch)."""
|
|
289
|
+
self.finished_leaves += 1
|
|
290
|
+
leaf_dict = paramset.values_dict.copy()
|
|
291
|
+
leaf_dict['datetime'] = datetime.now()
|
|
292
|
+
self.leaf_data.append(leaf_dict)
|
|
293
|
+
|
|
@@ -175,7 +175,7 @@ class ParameterSet(dict[str, Parameter]):
|
|
|
175
175
|
if missing_args:
|
|
176
176
|
missing_args = [f"'{arg}'" for arg in missing_args]
|
|
177
177
|
# TODO: catch this error higher up so we can provide info about WHAT param or function was being evaluated
|
|
178
|
-
raise
|
|
178
|
+
raise KeyError(f"Missing the following required params while evaluating function: {', '.join(missing_args)}")
|
|
179
179
|
|
|
180
180
|
# Call the function with matched kwargs
|
|
181
181
|
return func(**matched_kwargs)
|