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.
@@ -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.3.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",
@@ -1,4 +1,4 @@
1
1
  # functioneer/__init__.py
2
- __version__ = "0.3.0"
2
+ __version__ = "0.4.0"
3
3
  from functioneer.analysis import AnalysisModule, AnalysisStep, Define, Fork, Execute, Optimize
4
4
  from functioneer.parameter import Parameter
@@ -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 ValueError(f"Missing the following required params while evaluating function: {', '.join(missing_args)}")
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)