functioneer 0.0.1__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.0.1/LICENSE +26 -0
- functioneer-0.0.1/PKG-INFO +84 -0
- functioneer-0.0.1/README.md +28 -0
- functioneer-0.0.1/pyproject.toml +39 -0
- functioneer-0.0.1/setup.cfg +4 -0
- functioneer-0.0.1/src/functioneer/__init__.py +6 -0
- functioneer-0.0.1/src/functioneer/analysis.py +388 -0
- functioneer-0.0.1/src/functioneer/parameter.py +122 -0
- functioneer-0.0.1/src/functioneer/util.py +29 -0
- functioneer-0.0.1/src/functioneer.egg-info/PKG-INFO +84 -0
- functioneer-0.0.1/src/functioneer.egg-info/SOURCES.txt +12 -0
- functioneer-0.0.1/src/functioneer.egg-info/dependency_links.txt +1 -0
- functioneer-0.0.1/src/functioneer.egg-info/requires.txt +3 -0
- functioneer-0.0.1/src/functioneer.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Proprietary Software License
|
|
2
|
+
----------------------------
|
|
3
|
+
|
|
4
|
+
Software: functioneer (Functioneer)
|
|
5
|
+
Author: Quinn Marsh
|
|
6
|
+
Copyright: © [2024] Quinn Marsh
|
|
7
|
+
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute copies of the Software, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
1. Attribution: All copies, substantial portions of the Software, and derivative works must include this original copyright notice, the name of the author(s), and a link to the original repository.
|
|
13
|
+
|
|
14
|
+
2. Non-commercial Use: The Software may not be used, in whole or in part, for commercial purposes without explicit permission from the copyright holder.
|
|
15
|
+
|
|
16
|
+
3. No Warranty: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
17
|
+
|
|
18
|
+
4. No Misrepresentation: Users of the Software shall not misrepresent the origin of the Software and should clearly indicate any modifications made.
|
|
19
|
+
|
|
20
|
+
By using, copying, modifying, or distributing this Software, you agree to the terms and conditions outlined above.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
For details on using the Software within the terms of this license, please contact Quinn Marsh.
|
|
24
|
+
|
|
25
|
+
Quinn Marsh
|
|
26
|
+
[quinnmarsh@hotmail.com, X @qthedoc]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: functioneer
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Functioneer is a Python package that simplifies the setup of automated analyses for your functions.
|
|
5
|
+
Author-email: Quinn Marsh <quinnmarsh@hotmail.com>
|
|
6
|
+
Maintainer-email: Quinn Marsh <quinnmarsh@hotmail.com>
|
|
7
|
+
License: Proprietary Software License
|
|
8
|
+
----------------------------
|
|
9
|
+
|
|
10
|
+
Software: functioneer (Functioneer)
|
|
11
|
+
Author: Quinn Marsh
|
|
12
|
+
Copyright: © [2024] Quinn Marsh
|
|
13
|
+
|
|
14
|
+
All rights reserved.
|
|
15
|
+
|
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute copies of the Software, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
1. Attribution: All copies, substantial portions of the Software, and derivative works must include this original copyright notice, the name of the author(s), and a link to the original repository.
|
|
19
|
+
|
|
20
|
+
2. Non-commercial Use: The Software may not be used, in whole or in part, for commercial purposes without explicit permission from the copyright holder.
|
|
21
|
+
|
|
22
|
+
3. No Warranty: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
4. No Misrepresentation: Users of the Software shall not misrepresent the origin of the Software and should clearly indicate any modifications made.
|
|
25
|
+
|
|
26
|
+
By using, copying, modifying, or distributing this Software, you agree to the terms and conditions outlined above.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
For details on using the Software within the terms of this license, please contact Quinn Marsh.
|
|
30
|
+
|
|
31
|
+
Quinn Marsh
|
|
32
|
+
[quinnmarsh@hotmail.com, X @qthedoc]
|
|
33
|
+
|
|
34
|
+
Project-URL: Homepage, https://github.com/qthedoc/functioneer
|
|
35
|
+
Project-URL: Issues, https://github.com/qthedoc/functioneer/issues
|
|
36
|
+
Project-URL: Funding, https://donate.pypi.org
|
|
37
|
+
Project-URL: Say Thanks!, http://quinnmarsh.com
|
|
38
|
+
Keywords: functioneer,analysis,automation,autorun
|
|
39
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
40
|
+
Classifier: Intended Audience :: Science/Research
|
|
41
|
+
Classifier: Topic :: Scientific/Engineering
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
49
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
50
|
+
Requires-Python: >=3.10
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
License-File: LICENSE
|
|
53
|
+
Requires-Dist: numpy>=1.18.5
|
|
54
|
+
Requires-Dist: scipy>=1.5.2
|
|
55
|
+
Requires-Dist: pandas>=1.0.5
|
|
56
|
+
|
|
57
|
+
# Functioneer
|
|
58
|
+
|
|
59
|
+
**Author**: Quinn Marsh
|
|
60
|
+
**Date**: October 21, 2024
|
|
61
|
+
|
|
62
|
+
Functioneer is a powerful Python package that simplifies the setup of automated analyses for your functions. With Functioneer, you can configure and execute functions with unlimited combinations of inputs, making it perfect for optimizations, parameter sweeps, testing, and any scenario where you need to test a function(s) over a wide range of inputs.
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **Fast Setup**: Define inputs, configurations, and combinations in seconds.
|
|
67
|
+
- **Automated Execution**: Run functions with diverse parameter combinations, reducing repetitive setup.
|
|
68
|
+
- **Flexible Input Handling**: Supports a variety of input types for complex analyses.
|
|
69
|
+
- **Combinatorial Efficiency**: Specify ranges and conditions to create the full space of function possibilities.
|
|
70
|
+
- **Output Management**: Easily retrieve and analyze the output of all function executions.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
Install Functioneer directly from PyPI:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
pip install functioneer
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
For full documentation on setting up Functioneer, customizing parameters, and handling outputs, visit the documentation site.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
Functioneer is released as open-source software with a custom license. Please see the LICENSE file for usage guidelines.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Functioneer
|
|
2
|
+
|
|
3
|
+
**Author**: Quinn Marsh
|
|
4
|
+
**Date**: October 21, 2024
|
|
5
|
+
|
|
6
|
+
Functioneer is a powerful Python package that simplifies the setup of automated analyses for your functions. With Functioneer, you can configure and execute functions with unlimited combinations of inputs, making it perfect for optimizations, parameter sweeps, testing, and any scenario where you need to test a function(s) over a wide range of inputs.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Fast Setup**: Define inputs, configurations, and combinations in seconds.
|
|
11
|
+
- **Automated Execution**: Run functions with diverse parameter combinations, reducing repetitive setup.
|
|
12
|
+
- **Flexible Input Handling**: Supports a variety of input types for complex analyses.
|
|
13
|
+
- **Combinatorial Efficiency**: Specify ranges and conditions to create the full space of function possibilities.
|
|
14
|
+
- **Output Management**: Easily retrieve and analyze the output of all function executions.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install Functioneer directly from PyPI:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
pip install functioneer
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Documentation
|
|
25
|
+
For full documentation on setting up Functioneer, customizing parameters, and handling outputs, visit the documentation site.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
Functioneer is released as open-source software with a custom license. Please see the LICENSE file for usage guidelines.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "functioneer"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [{ name = "Quinn Marsh", email = "quinnmarsh@hotmail.com" }]
|
|
9
|
+
maintainers = [{ name = "Quinn Marsh", email = "quinnmarsh@hotmail.com" }]
|
|
10
|
+
description = "Functioneer is a Python package that simplifies the setup of automated analyses for your functions."
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = { file = "LICENSE" }
|
|
13
|
+
keywords = ["functioneer", "analysis", "automation", "autorun"]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy>=1.18.5",
|
|
17
|
+
"scipy>=1.5.2",
|
|
18
|
+
"pandas>=1.0.5"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
23
|
+
"Intended Audience :: Science/Research",
|
|
24
|
+
"Topic :: Scientific/Engineering",
|
|
25
|
+
"Programming Language :: Python :: 3.6",
|
|
26
|
+
"Programming Language :: Python :: 3.7",
|
|
27
|
+
"Programming Language :: Python :: 3.8",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
"Homepage" = "https://github.com/qthedoc/functioneer"
|
|
37
|
+
"Issues" = "https://github.com/qthedoc/functioneer/issues"
|
|
38
|
+
"Funding" = "https://donate.pypi.org"
|
|
39
|
+
"Say Thanks!" = "http://quinnmarsh.com"
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
import logging
|
|
3
|
+
import copy
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from time import time
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.optimize import minimize
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from functioneer.parameter import ParameterSet, Parameter
|
|
12
|
+
from functioneer.util import call_with_matched_kwargs
|
|
13
|
+
|
|
14
|
+
class AnalysisStep():
|
|
15
|
+
"""
|
|
16
|
+
Template AnalysisStep object containing basic info
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, condition: Callable[..., bool] | None = None) -> None:
|
|
19
|
+
if condition is not None and not isinstance(condition, Callable):
|
|
20
|
+
raise ValueError(f"AnalysisStep condition must be a function that returns a bool")
|
|
21
|
+
|
|
22
|
+
self.condition = condition
|
|
23
|
+
|
|
24
|
+
# self.branch_cnt = 1
|
|
25
|
+
|
|
26
|
+
def run(self, paramset: ParameterSet):
|
|
27
|
+
"""
|
|
28
|
+
template method for executing the AnalysisStep
|
|
29
|
+
returns a list of one or more ParameterSets that have been altered according to the AnalysisStep
|
|
30
|
+
returns a tuple of one or more parametersets
|
|
31
|
+
|
|
32
|
+
TODO decide if Default behavior is to COPY or PASS the paramset
|
|
33
|
+
|
|
34
|
+
validates paramset
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(paramset, ParameterSet):
|
|
37
|
+
# logging.error(f"paramset is not of type ParameterSet")
|
|
38
|
+
raise ValueError(f"paramset is not of type ParameterSet")
|
|
39
|
+
|
|
40
|
+
return (paramset,)
|
|
41
|
+
|
|
42
|
+
class Define(AnalysisStep):
|
|
43
|
+
"""
|
|
44
|
+
Define AnalysisStep: Adds parameter to parameterset
|
|
45
|
+
Will create new Parameter in ParameterSet if one does not already exist.
|
|
46
|
+
"""
|
|
47
|
+
def __init__(self,
|
|
48
|
+
name: str,
|
|
49
|
+
value = None,
|
|
50
|
+
# func = None,
|
|
51
|
+
# func_output_mode = 'single',
|
|
52
|
+
condition = None
|
|
53
|
+
):
|
|
54
|
+
super().__init__(condition)
|
|
55
|
+
|
|
56
|
+
if isinstance(name, str):
|
|
57
|
+
self.parameter = Parameter(name, value)
|
|
58
|
+
|
|
59
|
+
# elif isinstance(param, Parameter):
|
|
60
|
+
# pass
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
logging.error(f"param must be of type Parameter")
|
|
64
|
+
|
|
65
|
+
def run(self, paramset):
|
|
66
|
+
# Validate
|
|
67
|
+
super().run(paramset)
|
|
68
|
+
|
|
69
|
+
# add new Parameter to Paramset
|
|
70
|
+
paramset.add_param(self.parameter)
|
|
71
|
+
|
|
72
|
+
return (paramset,)
|
|
73
|
+
|
|
74
|
+
class Fork(AnalysisStep):
|
|
75
|
+
"""
|
|
76
|
+
Fork AnalysisStep: Splits analysis into several parallel analysis based on the provided parameters and values.
|
|
77
|
+
Will create new Parameter in ParameterSet if one does not already exist.
|
|
78
|
+
"""
|
|
79
|
+
def __init__(self,
|
|
80
|
+
param_ids,
|
|
81
|
+
value_sets, # value_sets
|
|
82
|
+
condition = None
|
|
83
|
+
):
|
|
84
|
+
super().__init__(condition)
|
|
85
|
+
|
|
86
|
+
# Handle single parameter id
|
|
87
|
+
if Parameter.is_valid_id(param_ids):
|
|
88
|
+
self.param_ids = (param_ids,)
|
|
89
|
+
value_sets = (value_sets,)
|
|
90
|
+
|
|
91
|
+
# Handle multiple param ids
|
|
92
|
+
elif isinstance(param_ids, (tuple, list)) and all([Parameter.is_valid_id(id) for id in param_ids]):
|
|
93
|
+
self.param_ids = param_ids
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(f"Fork param_ids must be a valid param id or a tuple of valid param ids")
|
|
97
|
+
|
|
98
|
+
# Identify number of parameters
|
|
99
|
+
self.param_cnt = len(self.param_ids)
|
|
100
|
+
|
|
101
|
+
# Validate same number of params and value sets
|
|
102
|
+
if len(self.param_ids) != len(value_sets):
|
|
103
|
+
raise ValueError(f"Invalid Fork (param_ids='{self.param_ids}'): Mismatch between number of parameters and number or value sets.")
|
|
104
|
+
|
|
105
|
+
self.value_cnt = len(value_sets[0])
|
|
106
|
+
for vs in value_sets:
|
|
107
|
+
if len(vs) != self.value_cnt:
|
|
108
|
+
raise ValueError(f"Invalid Fork (param_ids='{self.param_ids}'): Each parameter's value set must be the same length")
|
|
109
|
+
|
|
110
|
+
self.value_sets = value_sets
|
|
111
|
+
|
|
112
|
+
# TODO Validate value types
|
|
113
|
+
# this will be optional but left alone and values are tuples then it might be good to throw a warning in case user meant
|
|
114
|
+
# raise ValueError(f"Warning: values of paramfork are of type 'tuple'. to silence this error set Parameter.value_type tot tuple")
|
|
115
|
+
|
|
116
|
+
def run(self, paramset: dict[str, Parameter]):
|
|
117
|
+
super().run(paramset)
|
|
118
|
+
|
|
119
|
+
# Do forky stuff
|
|
120
|
+
|
|
121
|
+
next_paramsets = []
|
|
122
|
+
for branch_idx in range(self.value_cnt):
|
|
123
|
+
ps: ParameterSet = copy.deepcopy(paramset)
|
|
124
|
+
for id, vs in zip(self.param_ids, self.value_sets):
|
|
125
|
+
val = vs[branch_idx]
|
|
126
|
+
ps.update_param(id, val)
|
|
127
|
+
|
|
128
|
+
next_paramsets.append(ps)
|
|
129
|
+
|
|
130
|
+
return next_paramsets
|
|
131
|
+
|
|
132
|
+
class Execute(AnalysisStep):
|
|
133
|
+
"""
|
|
134
|
+
Execute AnalysisStep: Executes provided function using matched kwargs and sets parameters based on returned dict
|
|
135
|
+
"""
|
|
136
|
+
def __init__(self,
|
|
137
|
+
func: Callable,
|
|
138
|
+
output_param_ids: str = None,
|
|
139
|
+
input_param_ids: str = None,
|
|
140
|
+
condition = None
|
|
141
|
+
):
|
|
142
|
+
super().__init__(condition)
|
|
143
|
+
|
|
144
|
+
self.func = func
|
|
145
|
+
|
|
146
|
+
if output_param_ids is None:
|
|
147
|
+
self.output_param_ids = output_param_ids
|
|
148
|
+
elif Parameter.is_valid_id(output_param_ids):
|
|
149
|
+
self.output_param_ids = output_param_ids
|
|
150
|
+
elif Parameter.is_valid_id_iterable(output_param_ids):
|
|
151
|
+
self.output_param_ids = output_param_ids
|
|
152
|
+
else:
|
|
153
|
+
raise ValueError(f"Execute Step's output_param_ids must be a str or tuple of str")
|
|
154
|
+
|
|
155
|
+
self.input_param_ids = input_param_ids
|
|
156
|
+
|
|
157
|
+
def run(self, paramset):
|
|
158
|
+
"""
|
|
159
|
+
Executes funciton and modifies paramset
|
|
160
|
+
|
|
161
|
+
TODO future args: auto_add_new_args=True
|
|
162
|
+
"""
|
|
163
|
+
super().run(paramset)
|
|
164
|
+
|
|
165
|
+
# TODO might need to deepcopy params is preservation of the upper level is needed
|
|
166
|
+
next_paramset = copy.deepcopy(paramset)
|
|
167
|
+
|
|
168
|
+
# Execute function: Positional argument input mode
|
|
169
|
+
if self.input_param_ids:
|
|
170
|
+
output = next_paramset.call_with_positional_args(func=self.func, param_ids=self.input_param_ids)
|
|
171
|
+
|
|
172
|
+
# Execute function: Keyword argument input mode
|
|
173
|
+
else:
|
|
174
|
+
output = next_paramset.call_with_matched_kwargs(func=self.func)
|
|
175
|
+
|
|
176
|
+
# Process output: Direct output mode
|
|
177
|
+
if self.output_param_ids and isinstance(self.output_param_ids, str):
|
|
178
|
+
next_paramset.update_param(id=self.output_param_ids, value=output)
|
|
179
|
+
|
|
180
|
+
# Process output: Positional output mode
|
|
181
|
+
elif self.output_param_ids:
|
|
182
|
+
if len(self.output_param_ids) != len(output):
|
|
183
|
+
raise ValueError(f"Number of function outputs ({len(output)}) does not match the specified output_param_ids ({len(self.output_param_ids)})")
|
|
184
|
+
|
|
185
|
+
for i, id in enumerate(self.output_param_ids):
|
|
186
|
+
next_paramset.update_param(id=id, value=output[i])
|
|
187
|
+
|
|
188
|
+
# Process output: Keyword output mode
|
|
189
|
+
else:
|
|
190
|
+
if not isinstance(output, dict):
|
|
191
|
+
raise ValueError(f"function in step {69} does not return valid dict of param names and values")
|
|
192
|
+
|
|
193
|
+
# next_paramsets.update_values(results)
|
|
194
|
+
|
|
195
|
+
# TODO this should be funcitonalized
|
|
196
|
+
# paramset.update_param_values(results)
|
|
197
|
+
for id, val in output.items():
|
|
198
|
+
next_paramset.update_param(id, value=val)
|
|
199
|
+
|
|
200
|
+
return (next_paramset,)
|
|
201
|
+
|
|
202
|
+
class Optimize(AnalysisStep):
|
|
203
|
+
"""
|
|
204
|
+
Optimize AnalysisStep: Minimizes (or maximizes) objective by optimizing opt vars
|
|
205
|
+
"""
|
|
206
|
+
def __init__(self,
|
|
207
|
+
func,
|
|
208
|
+
obj_param_id = None,
|
|
209
|
+
opt_param_ids = None,
|
|
210
|
+
method = 'SLSQP',
|
|
211
|
+
ftol = None,
|
|
212
|
+
xtol = None,
|
|
213
|
+
func_output_mode = 'single',
|
|
214
|
+
condition = None
|
|
215
|
+
):
|
|
216
|
+
super().__init__(condition)
|
|
217
|
+
|
|
218
|
+
# Validate stuff
|
|
219
|
+
# if not is_valid_kwarg_func(func): TODO make this funciton validator a thing
|
|
220
|
+
# raise ValueError(f"Invalid Optimize: 'func' is not a valid objective function: {obj_param_id}")
|
|
221
|
+
if not isinstance(func, Callable):
|
|
222
|
+
raise ValueError(f"Invalid Optimize: 'func' is not a valid objective function: {obj_param_id}")
|
|
223
|
+
self.func = func
|
|
224
|
+
|
|
225
|
+
if not Parameter.is_valid_id(obj_param_id):
|
|
226
|
+
raise ValueError(f"Invalid Optimize: 'obj_param_id' is not a valid param id: {obj_param_id}")
|
|
227
|
+
self.obj_param_id = obj_param_id
|
|
228
|
+
|
|
229
|
+
# TODO funcitonalize these checks so they can be onelinesrs
|
|
230
|
+
if not Parameter.is_valid_id_iterable(opt_param_ids):
|
|
231
|
+
raise ValueError(f"Invalid Optimize: 'obj_param_id' is not a valid param id tuple: {opt_param_ids}")
|
|
232
|
+
self.opt_param_ids = opt_param_ids
|
|
233
|
+
|
|
234
|
+
self.method = method
|
|
235
|
+
self.ftol = ftol
|
|
236
|
+
self.xtol = xtol
|
|
237
|
+
self.func_output_mode = func_output_mode
|
|
238
|
+
|
|
239
|
+
def run(self, paramset):
|
|
240
|
+
# Validate
|
|
241
|
+
super().run(paramset)
|
|
242
|
+
|
|
243
|
+
# Copy
|
|
244
|
+
next_paramset = copy.deepcopy(paramset)
|
|
245
|
+
|
|
246
|
+
# create objective wrapper where x is a list of opt vars
|
|
247
|
+
def objective_wrapper(x): # (func, next_paramset, opt_param_ids)
|
|
248
|
+
'''
|
|
249
|
+
evaluates the user supplied objective function using fresh optimizer values from x.
|
|
250
|
+
uses x to modify parameter set and then run user objective function
|
|
251
|
+
'''
|
|
252
|
+
# Create test parameter set with values from optimizer
|
|
253
|
+
test_paramset = copy.deepcopy(next_paramset)
|
|
254
|
+
test_paramset.update_param_values(dict(zip(self.opt_param_ids, x)))
|
|
255
|
+
|
|
256
|
+
# evaluate objective parameter
|
|
257
|
+
obj_val = test_paramset.call_with_matched_kwargs(self.func)
|
|
258
|
+
|
|
259
|
+
# TODO add code for different output methods
|
|
260
|
+
|
|
261
|
+
return obj_val
|
|
262
|
+
|
|
263
|
+
# Create x0 array
|
|
264
|
+
x0 = [next_paramset[id].value for id in self.opt_param_ids]
|
|
265
|
+
|
|
266
|
+
# run optimization
|
|
267
|
+
results = minimize(objective_wrapper, x0, method=self.method)
|
|
268
|
+
|
|
269
|
+
# Check if the number of optimization parameters matches the length of x
|
|
270
|
+
if len(self.opt_param_ids) != len(results.x):
|
|
271
|
+
raise ValueError("Number of optimization parameters does not match the length of the optimization result")
|
|
272
|
+
|
|
273
|
+
# Set optimization parameters
|
|
274
|
+
next_paramset.update_param_values(dict(zip(self.opt_param_ids, results.x)))
|
|
275
|
+
|
|
276
|
+
# Set objective parameter
|
|
277
|
+
next_paramset.update_param(self.obj_param_id, value=results.fun)
|
|
278
|
+
|
|
279
|
+
return (next_paramset,)
|
|
280
|
+
|
|
281
|
+
class End(AnalysisStep):
|
|
282
|
+
def __init__(self, condition = None):
|
|
283
|
+
super().__init__(condition)
|
|
284
|
+
|
|
285
|
+
def run(self, paramset):
|
|
286
|
+
if self.condition:
|
|
287
|
+
return None
|
|
288
|
+
else:
|
|
289
|
+
return super().run(paramset)
|
|
290
|
+
|
|
291
|
+
class AnalysisModule():
|
|
292
|
+
def __init__(self, name='') -> None:
|
|
293
|
+
|
|
294
|
+
self.sequence = []
|
|
295
|
+
self.finished_leaves:int = 0
|
|
296
|
+
|
|
297
|
+
self.name = name
|
|
298
|
+
|
|
299
|
+
self.df: pd.DataFrame = pd.DataFrame()
|
|
300
|
+
|
|
301
|
+
self.t0 = None
|
|
302
|
+
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
def add(self, analysis_object: AnalysisStep) -> None:
|
|
306
|
+
'''
|
|
307
|
+
appends AnalysisStep to the Analysis Sequence
|
|
308
|
+
e.g.: define, fork, evaluation or optimization
|
|
309
|
+
'''
|
|
310
|
+
# TODO Validate AnalysisStep
|
|
311
|
+
|
|
312
|
+
# TODO analysis_object.initialize(self.sequence)
|
|
313
|
+
|
|
314
|
+
self.sequence.append(analysis_object)
|
|
315
|
+
|
|
316
|
+
def run(self,
|
|
317
|
+
create_pandas = True,
|
|
318
|
+
verbose = True
|
|
319
|
+
):
|
|
320
|
+
# self.sequence = tuple(self.sequence)
|
|
321
|
+
|
|
322
|
+
self.t0 = time()
|
|
323
|
+
self.finished_leaves = 0
|
|
324
|
+
self.execute_step(ParameterSet(), step_idx=0) # start on step 0
|
|
325
|
+
|
|
326
|
+
print('done with analysis!')
|
|
327
|
+
|
|
328
|
+
results = dict(
|
|
329
|
+
df = self.df,
|
|
330
|
+
runtime = self.runtime,
|
|
331
|
+
finished_leaves = self.finished_leaves,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return results
|
|
335
|
+
|
|
336
|
+
def execute_step(self, paramset: ParameterSet, step_idx:int):
|
|
337
|
+
"""
|
|
338
|
+
Runs a single step of the analysis sequence, then recursivly calls the next.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
# End analysis branch if no more steps
|
|
342
|
+
if step_idx >= len(self.sequence):
|
|
343
|
+
self.end_sequence(paramset)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# Extract step object
|
|
347
|
+
analysis_step: AnalysisStep = self.sequence[step_idx]
|
|
348
|
+
|
|
349
|
+
# Check if step has a conditional
|
|
350
|
+
run_step = True
|
|
351
|
+
if analysis_step.condition is not None:
|
|
352
|
+
run_step = paramset.call_with_matched_kwargs(analysis_step.condition)
|
|
353
|
+
|
|
354
|
+
t0 = time()
|
|
355
|
+
if run_step:
|
|
356
|
+
# Run the next step
|
|
357
|
+
new_paramsets = analysis_step.run(paramset)
|
|
358
|
+
else: # pass step
|
|
359
|
+
new_paramsets = (copy.deepcopy(paramset),)
|
|
360
|
+
step_runtime = time() - t0
|
|
361
|
+
|
|
362
|
+
# Process Step and Recursivly call next step
|
|
363
|
+
for ps in new_paramsets:
|
|
364
|
+
# Add this steps runtime to total runtime
|
|
365
|
+
ps['runtime'].value += step_runtime
|
|
366
|
+
|
|
367
|
+
# Recursivly call next step
|
|
368
|
+
self.execute_step(ps, step_idx+1)
|
|
369
|
+
|
|
370
|
+
# End analysis branch if no paramset returned
|
|
371
|
+
# TODO decide how to handle End: (paramset is None) OR (type(analysis_step)==End)
|
|
372
|
+
if new_paramsets is None:
|
|
373
|
+
self.end_sequence(paramset)
|
|
374
|
+
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
def end_sequence(self, paramset: ParameterSet):
|
|
378
|
+
# increment leaf count
|
|
379
|
+
self.finished_leaves += 1
|
|
380
|
+
|
|
381
|
+
# Add a new row to DataFrame
|
|
382
|
+
new_row = paramset.values_dict.copy() # Copy the values_dict
|
|
383
|
+
new_row['datetime'] = datetime.now() # Add the current datetime
|
|
384
|
+
|
|
385
|
+
# Append the new row
|
|
386
|
+
self.df = pd.concat([self.df, pd.DataFrame([new_row])], ignore_index=True)
|
|
387
|
+
|
|
388
|
+
self.runtime = time() - self.t0
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
class Parameter():
|
|
5
|
+
def __init__(self,
|
|
6
|
+
id: str,
|
|
7
|
+
value = None,
|
|
8
|
+
value_set = None,
|
|
9
|
+
# fun = None,
|
|
10
|
+
# bounds = (None, None),
|
|
11
|
+
):
|
|
12
|
+
|
|
13
|
+
if isinstance(id, str) and id != '':
|
|
14
|
+
self.id = id
|
|
15
|
+
else:
|
|
16
|
+
logging.error(f"Parameter name must be a string with at least one character")
|
|
17
|
+
self.value = value
|
|
18
|
+
# self.values = values
|
|
19
|
+
# self.function = fun
|
|
20
|
+
# self.bounds = bounds
|
|
21
|
+
|
|
22
|
+
def set(self, **kwargs):
|
|
23
|
+
'''
|
|
24
|
+
set any attribute of a parameter
|
|
25
|
+
'''
|
|
26
|
+
for attr, value in kwargs.items():
|
|
27
|
+
if hasattr(self, attr):
|
|
28
|
+
setattr(self, attr, value)
|
|
29
|
+
else:
|
|
30
|
+
raise AttributeError(f"Attribute '{attr}' does not exist in the parameter.")
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def is_valid_id(id):
|
|
34
|
+
# TODO add this to other places
|
|
35
|
+
return isinstance(id, str) and id != ''
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def is_valid_id_iterable(id_iterable):
|
|
39
|
+
return isinstance(id_iterable, (list, tuple)) and all([Parameter.is_valid_id(id) for id in id_iterable])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ParameterSet(dict):
|
|
43
|
+
def __init__(self):
|
|
44
|
+
|
|
45
|
+
self.add_param(Parameter('runtime', 0))
|
|
46
|
+
|
|
47
|
+
def add_param(self, parameter):
|
|
48
|
+
if not isinstance(parameter, Parameter):
|
|
49
|
+
raise ValueError("parameter must be an instance of Parameter")
|
|
50
|
+
|
|
51
|
+
id = parameter.id
|
|
52
|
+
|
|
53
|
+
if id in self:
|
|
54
|
+
logging.info(f"Parameter '{id}' overwritten")
|
|
55
|
+
|
|
56
|
+
self[id] = parameter
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def values_dict(self):
|
|
60
|
+
return {key: param.value for key, param in self.items()}
|
|
61
|
+
|
|
62
|
+
def update_param(self, id, value=None):
|
|
63
|
+
"""
|
|
64
|
+
Updates attributes of parameter matching 'id' or creates a new parameter if none match
|
|
65
|
+
|
|
66
|
+
TODO add other attributes here if needed
|
|
67
|
+
"""
|
|
68
|
+
if value is not None:
|
|
69
|
+
if id in self:
|
|
70
|
+
self[id].value = value
|
|
71
|
+
else:
|
|
72
|
+
self.add_param(Parameter(id, value))
|
|
73
|
+
|
|
74
|
+
def update_param_values(self, values_dict: dict):
|
|
75
|
+
for id, val in values_dict.items():
|
|
76
|
+
self.update_param(id, val)
|
|
77
|
+
|
|
78
|
+
# def get_value_list(self, param_ids):
|
|
79
|
+
# # TODO Validate param_ids are in paramset
|
|
80
|
+
# return [self[id].value for id in param_ids]
|
|
81
|
+
|
|
82
|
+
def call_with_matched_kwargs(self, func):
|
|
83
|
+
"""
|
|
84
|
+
Calls function with parameter values from the ParameterSet with matching keyword args.
|
|
85
|
+
Throws error if required arg is missing in paramset.
|
|
86
|
+
"""
|
|
87
|
+
kwargs = self.values_dict
|
|
88
|
+
|
|
89
|
+
# Get the function signature
|
|
90
|
+
signature = inspect.signature(func)
|
|
91
|
+
arguments = signature.parameters
|
|
92
|
+
|
|
93
|
+
# Match provided kwargs with function arguments
|
|
94
|
+
matched_kwargs = {}
|
|
95
|
+
missing_args = []
|
|
96
|
+
for name, arg in arguments.items():
|
|
97
|
+
if name in kwargs:
|
|
98
|
+
matched_kwargs[name] = kwargs[name]
|
|
99
|
+
elif arg.default == inspect.Parameter.empty:
|
|
100
|
+
missing_args.append(name)
|
|
101
|
+
|
|
102
|
+
# Raise a warning if required arguments are missing
|
|
103
|
+
if missing_args:
|
|
104
|
+
missing_args = [f"'{arg}'" for arg in missing_args]
|
|
105
|
+
# TODO: catch this error higher up so we can provide info about WHAT param or funciton was being evaluated
|
|
106
|
+
raise ValueError(f"Missing the following required params while evaluating function: {', '.join(missing_args)}")
|
|
107
|
+
|
|
108
|
+
# Call the function with matched kwargs
|
|
109
|
+
return func(**matched_kwargs)
|
|
110
|
+
|
|
111
|
+
def call_with_positional_args(self, func, param_ids):
|
|
112
|
+
"""
|
|
113
|
+
Calls function with parameter values as positional args.
|
|
114
|
+
The order of the args is the same as given in 'param_ids'.
|
|
115
|
+
"""
|
|
116
|
+
# arg_list = self.get_value_list(self.input_params)
|
|
117
|
+
|
|
118
|
+
# TODO Validate param_ids are in paramset
|
|
119
|
+
arg_list = [self[id].value for id in param_ids]
|
|
120
|
+
return func(*arg_list)
|
|
121
|
+
|
|
122
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def call_with_matched_kwargs(func, kwargs):
|
|
5
|
+
'''
|
|
6
|
+
calls funciton with matching kwargs, throws error if required arg is missing.
|
|
7
|
+
TODO: could add a special case for a kwarg called kwargs where all kwargs are just fed in there. this allows user to have **kwargs on their functions
|
|
8
|
+
'''
|
|
9
|
+
# Get the function signature
|
|
10
|
+
signature = inspect.signature(func)
|
|
11
|
+
arguments = signature.parameters
|
|
12
|
+
|
|
13
|
+
# Match provided kwargs with function arguments
|
|
14
|
+
matched_kwargs = {}
|
|
15
|
+
missing_args = []
|
|
16
|
+
for name, arg in arguments.items():
|
|
17
|
+
if name in kwargs:
|
|
18
|
+
matched_kwargs[name] = kwargs[name]
|
|
19
|
+
elif arg.default == inspect.Parameter.empty:
|
|
20
|
+
missing_args.append(name)
|
|
21
|
+
|
|
22
|
+
# Raise a warning if required arguments are missing
|
|
23
|
+
if missing_args:
|
|
24
|
+
missing_args = [f"'{arg}'" for arg in missing_args]
|
|
25
|
+
# TODO: catch this error higher up so we can provide info about WHAT param or funciton was being evaluated
|
|
26
|
+
raise ValueError(f"Missing the following required params while evaluating function: {', '.join(missing_args)}")
|
|
27
|
+
|
|
28
|
+
# Call the function with matched kwargs
|
|
29
|
+
return func(**matched_kwargs)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: functioneer
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Functioneer is a Python package that simplifies the setup of automated analyses for your functions.
|
|
5
|
+
Author-email: Quinn Marsh <quinnmarsh@hotmail.com>
|
|
6
|
+
Maintainer-email: Quinn Marsh <quinnmarsh@hotmail.com>
|
|
7
|
+
License: Proprietary Software License
|
|
8
|
+
----------------------------
|
|
9
|
+
|
|
10
|
+
Software: functioneer (Functioneer)
|
|
11
|
+
Author: Quinn Marsh
|
|
12
|
+
Copyright: © [2024] Quinn Marsh
|
|
13
|
+
|
|
14
|
+
All rights reserved.
|
|
15
|
+
|
|
16
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, and distribute copies of the Software, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
1. Attribution: All copies, substantial portions of the Software, and derivative works must include this original copyright notice, the name of the author(s), and a link to the original repository.
|
|
19
|
+
|
|
20
|
+
2. Non-commercial Use: The Software may not be used, in whole or in part, for commercial purposes without explicit permission from the copyright holder.
|
|
21
|
+
|
|
22
|
+
3. No Warranty: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
4. No Misrepresentation: Users of the Software shall not misrepresent the origin of the Software and should clearly indicate any modifications made.
|
|
25
|
+
|
|
26
|
+
By using, copying, modifying, or distributing this Software, you agree to the terms and conditions outlined above.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
For details on using the Software within the terms of this license, please contact Quinn Marsh.
|
|
30
|
+
|
|
31
|
+
Quinn Marsh
|
|
32
|
+
[quinnmarsh@hotmail.com, X @qthedoc]
|
|
33
|
+
|
|
34
|
+
Project-URL: Homepage, https://github.com/qthedoc/functioneer
|
|
35
|
+
Project-URL: Issues, https://github.com/qthedoc/functioneer/issues
|
|
36
|
+
Project-URL: Funding, https://donate.pypi.org
|
|
37
|
+
Project-URL: Say Thanks!, http://quinnmarsh.com
|
|
38
|
+
Keywords: functioneer,analysis,automation,autorun
|
|
39
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
40
|
+
Classifier: Intended Audience :: Science/Research
|
|
41
|
+
Classifier: Topic :: Scientific/Engineering
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
49
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
50
|
+
Requires-Python: >=3.10
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
License-File: LICENSE
|
|
53
|
+
Requires-Dist: numpy>=1.18.5
|
|
54
|
+
Requires-Dist: scipy>=1.5.2
|
|
55
|
+
Requires-Dist: pandas>=1.0.5
|
|
56
|
+
|
|
57
|
+
# Functioneer
|
|
58
|
+
|
|
59
|
+
**Author**: Quinn Marsh
|
|
60
|
+
**Date**: October 21, 2024
|
|
61
|
+
|
|
62
|
+
Functioneer is a powerful Python package that simplifies the setup of automated analyses for your functions. With Functioneer, you can configure and execute functions with unlimited combinations of inputs, making it perfect for optimizations, parameter sweeps, testing, and any scenario where you need to test a function(s) over a wide range of inputs.
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **Fast Setup**: Define inputs, configurations, and combinations in seconds.
|
|
67
|
+
- **Automated Execution**: Run functions with diverse parameter combinations, reducing repetitive setup.
|
|
68
|
+
- **Flexible Input Handling**: Supports a variety of input types for complex analyses.
|
|
69
|
+
- **Combinatorial Efficiency**: Specify ranges and conditions to create the full space of function possibilities.
|
|
70
|
+
- **Output Management**: Easily retrieve and analyze the output of all function executions.
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
Install Functioneer directly from PyPI:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
pip install functioneer
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
For full documentation on setting up Functioneer, customizing parameters, and handling outputs, visit the documentation site.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
Functioneer is released as open-source software with a custom license. Please see the LICENSE file for usage guidelines.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/functioneer/__init__.py
|
|
5
|
+
src/functioneer/analysis.py
|
|
6
|
+
src/functioneer/parameter.py
|
|
7
|
+
src/functioneer/util.py
|
|
8
|
+
src/functioneer.egg-info/PKG-INFO
|
|
9
|
+
src/functioneer.egg-info/SOURCES.txt
|
|
10
|
+
src/functioneer.egg-info/dependency_links.txt
|
|
11
|
+
src/functioneer.egg-info/requires.txt
|
|
12
|
+
src/functioneer.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
functioneer
|