megaconf 0.1.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.
- megaconf-0.1.0/LICENSE +9 -0
- megaconf-0.1.0/PKG-INFO +230 -0
- megaconf-0.1.0/README.md +213 -0
- megaconf-0.1.0/pyproject.toml +34 -0
- megaconf-0.1.0/pyproject.toml.orig +34 -0
- megaconf-0.1.0/src/megaconf/__init__.py +3 -0
- megaconf-0.1.0/src/megaconf/expansion.py +78 -0
- megaconf-0.1.0/src/megaconf/generate.py +122 -0
- megaconf-0.1.0/src/megaconf/inputs.py +27 -0
- megaconf-0.1.0/src/megaconf/models.py +38 -0
- megaconf-0.1.0/src/megaconf/py.typed +0 -0
- megaconf-0.1.0/src/megaconf/utils.py +54 -0
megaconf-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Richard Wessels
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
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, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
megaconf-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: megaconf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Easily create many variations of a config.
|
|
5
|
+
Author: Richard Wessels
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Dist: numpy>=2.5.2
|
|
11
|
+
Requires-Dist: pydantic>=2.13.5
|
|
12
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Project-URL: Homepage, https://github.com/RichardWessels/megaconf
|
|
15
|
+
Project-URL: Issues, https://github.com/RichardWessels/megaconf/issues
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# megaconf
|
|
19
|
+
|
|
20
|
+
megaconf is a tool to help with producing configs. This tool allows you to describe configuration combinations declaratively, instead of writing nested loops in code.
|
|
21
|
+
|
|
22
|
+
## How it works
|
|
23
|
+
|
|
24
|
+
megaconf generates configurations from two inputs:
|
|
25
|
+
|
|
26
|
+
1. Base config – a starting configuration.
|
|
27
|
+
2. Override config – rules describing how to modify the base config to produce multiple variants.
|
|
28
|
+
|
|
29
|
+
The result is a set of generated configs.
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
Suppose we start with a base config:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
algo: NN
|
|
37
|
+
lr: 0.001
|
|
38
|
+
layer_count: 8
|
|
39
|
+
activation: ReLU
|
|
40
|
+
initializer: He
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
We want to run experiments across different algorithms and hyperparameters.
|
|
44
|
+
|
|
45
|
+
This might look like:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
for algo in ["XGBoost", "NN"]:
|
|
49
|
+
for lr in [0.1, 0.01, 0.001]:
|
|
50
|
+
if algo == "NN":
|
|
51
|
+
for layer_count in [4, 8, 12, 16]:
|
|
52
|
+
for activation in ["ReLU", "Tanh"]:
|
|
53
|
+
...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
As the number of parameters and dependencies grows, this approach becomes hard to maintain.
|
|
57
|
+
|
|
58
|
+
megaconf allows expressing the same logic using override rules.
|
|
59
|
+
|
|
60
|
+
Override Config:
|
|
61
|
+
```yaml
|
|
62
|
+
- fixed:
|
|
63
|
+
algo: XGBoost
|
|
64
|
+
product:
|
|
65
|
+
lr: [0.1, 0.01, 0.001]
|
|
66
|
+
|
|
67
|
+
- fixed:
|
|
68
|
+
algo: NN
|
|
69
|
+
joint:
|
|
70
|
+
activation: [ReLU, Tanh]
|
|
71
|
+
initializer: [He, Glorot]
|
|
72
|
+
product:
|
|
73
|
+
lr: [0.1, 0.01, 0.001]
|
|
74
|
+
layer_count: [4, 8, 12, 16]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Override Methods
|
|
78
|
+
|
|
79
|
+
megaconf supports three override methods.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### `fixed`
|
|
84
|
+
|
|
85
|
+
Sets specific values in the config.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
fixed:
|
|
89
|
+
algo: NN
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Equivalent to:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
config["algo"] = "NN"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If the key does not exist in the base config, it is added.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `joint`
|
|
103
|
+
|
|
104
|
+
Iterates over multiple parameters together.
|
|
105
|
+
|
|
106
|
+
All lists must be the same length.
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
joint:
|
|
110
|
+
activation: [ReLU, Tanh]
|
|
111
|
+
initializer: [He, Glorot]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Result:
|
|
115
|
+
```
|
|
116
|
+
activation=ReLU initializer=He
|
|
117
|
+
activation=Tanh initializer=Glorot
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This is useful for paired parameters.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### `product`
|
|
125
|
+
|
|
126
|
+
Produces the cartesian product of parameter values.
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
product:
|
|
130
|
+
lr: [0.1, 0.01, 0.001]
|
|
131
|
+
layer_count: [4, 8, 12]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This generates:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
lr=0.1 layer_count=4
|
|
138
|
+
lr=0.1 layer_count=8
|
|
139
|
+
lr=0.1 layer_count=12
|
|
140
|
+
lr=0.01 layer_count=4
|
|
141
|
+
...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This is equivalent to a grid search.
|
|
145
|
+
|
|
146
|
+
## How Overrides Are Combined
|
|
147
|
+
|
|
148
|
+
Within an override block:
|
|
149
|
+
|
|
150
|
+
1. fixed values are applied first.
|
|
151
|
+
2. joint combinations are generated.
|
|
152
|
+
3. product combinations are generated.
|
|
153
|
+
|
|
154
|
+
The final configs are the combination of these rules applied to the base config.
|
|
155
|
+
|
|
156
|
+
## Interface
|
|
157
|
+
|
|
158
|
+
Create configurations with `generate_configs`. It accepts a base config and an
|
|
159
|
+
overrides config.
|
|
160
|
+
|
|
161
|
+
Both inputs can be provided in two ways:
|
|
162
|
+
- as a Python object (e.g., a dictionary or list).
|
|
163
|
+
- as a path to a configuration file (.yaml, .yml, or .json).
|
|
164
|
+
|
|
165
|
+
`generate_configs` returns an iterator that yields each generated config. The
|
|
166
|
+
number of generated configs depends on the combinations produced by the override
|
|
167
|
+
rules. Use `generate_configs_list` if you need all generated configs in a list.
|
|
168
|
+
|
|
169
|
+
Example usage:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from megaconf import generate_configs
|
|
173
|
+
|
|
174
|
+
base_config = {
|
|
175
|
+
"algo": "NN",
|
|
176
|
+
"lr": 0.001,
|
|
177
|
+
"layer_count": 8,
|
|
178
|
+
"activation": "ReLU",
|
|
179
|
+
"initializer": "He"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
overrides = [
|
|
183
|
+
{
|
|
184
|
+
"fixed": {
|
|
185
|
+
"algo": "XGBoost"
|
|
186
|
+
},
|
|
187
|
+
"product": {
|
|
188
|
+
"lr": [0.1, 0.01, 0.001]
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"fixed": {
|
|
193
|
+
"algo": "NN"
|
|
194
|
+
},
|
|
195
|
+
"joint": {
|
|
196
|
+
"activation": ["ReLU", "Tanh"],
|
|
197
|
+
"initializer": ["He", "Glorot"]
|
|
198
|
+
},
|
|
199
|
+
"product": {
|
|
200
|
+
"lr": [0.1, 0.01],
|
|
201
|
+
"layer_count": [4, 8]
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
configs = generate_configs(base_config, overrides)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Or if the base and overrides separate config files:
|
|
210
|
+
```python
|
|
211
|
+
configs = generate_configs(
|
|
212
|
+
"base_config.yaml",
|
|
213
|
+
"overrides.yaml"
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Nesting
|
|
218
|
+
If a key is deeply nested in the config, a dot-key separator is used by default. This means that specifying a nested key is done as follows:
|
|
219
|
+
```yaml
|
|
220
|
+
fixed:
|
|
221
|
+
key1.key2.key3: value
|
|
222
|
+
```
|
|
223
|
+
However, if a dot conflicts with your key strings, you can specify a custom key separator as follows:
|
|
224
|
+
```python
|
|
225
|
+
configs = generate_configs(
|
|
226
|
+
"base_config.yaml",
|
|
227
|
+
"overrides.yaml",
|
|
228
|
+
key_separator="::"
|
|
229
|
+
)
|
|
230
|
+
```
|
megaconf-0.1.0/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# megaconf
|
|
2
|
+
|
|
3
|
+
megaconf is a tool to help with producing configs. This tool allows you to describe configuration combinations declaratively, instead of writing nested loops in code.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
megaconf generates configurations from two inputs:
|
|
8
|
+
|
|
9
|
+
1. Base config – a starting configuration.
|
|
10
|
+
2. Override config – rules describing how to modify the base config to produce multiple variants.
|
|
11
|
+
|
|
12
|
+
The result is a set of generated configs.
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
Suppose we start with a base config:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
algo: NN
|
|
20
|
+
lr: 0.001
|
|
21
|
+
layer_count: 8
|
|
22
|
+
activation: ReLU
|
|
23
|
+
initializer: He
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
We want to run experiments across different algorithms and hyperparameters.
|
|
27
|
+
|
|
28
|
+
This might look like:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
for algo in ["XGBoost", "NN"]:
|
|
32
|
+
for lr in [0.1, 0.01, 0.001]:
|
|
33
|
+
if algo == "NN":
|
|
34
|
+
for layer_count in [4, 8, 12, 16]:
|
|
35
|
+
for activation in ["ReLU", "Tanh"]:
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
As the number of parameters and dependencies grows, this approach becomes hard to maintain.
|
|
40
|
+
|
|
41
|
+
megaconf allows expressing the same logic using override rules.
|
|
42
|
+
|
|
43
|
+
Override Config:
|
|
44
|
+
```yaml
|
|
45
|
+
- fixed:
|
|
46
|
+
algo: XGBoost
|
|
47
|
+
product:
|
|
48
|
+
lr: [0.1, 0.01, 0.001]
|
|
49
|
+
|
|
50
|
+
- fixed:
|
|
51
|
+
algo: NN
|
|
52
|
+
joint:
|
|
53
|
+
activation: [ReLU, Tanh]
|
|
54
|
+
initializer: [He, Glorot]
|
|
55
|
+
product:
|
|
56
|
+
lr: [0.1, 0.01, 0.001]
|
|
57
|
+
layer_count: [4, 8, 12, 16]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Override Methods
|
|
61
|
+
|
|
62
|
+
megaconf supports three override methods.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### `fixed`
|
|
67
|
+
|
|
68
|
+
Sets specific values in the config.
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
fixed:
|
|
72
|
+
algo: NN
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Equivalent to:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
config["algo"] = "NN"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If the key does not exist in the base config, it is added.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### `joint`
|
|
86
|
+
|
|
87
|
+
Iterates over multiple parameters together.
|
|
88
|
+
|
|
89
|
+
All lists must be the same length.
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
joint:
|
|
93
|
+
activation: [ReLU, Tanh]
|
|
94
|
+
initializer: [He, Glorot]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Result:
|
|
98
|
+
```
|
|
99
|
+
activation=ReLU initializer=He
|
|
100
|
+
activation=Tanh initializer=Glorot
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This is useful for paired parameters.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `product`
|
|
108
|
+
|
|
109
|
+
Produces the cartesian product of parameter values.
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
product:
|
|
113
|
+
lr: [0.1, 0.01, 0.001]
|
|
114
|
+
layer_count: [4, 8, 12]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This generates:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
lr=0.1 layer_count=4
|
|
121
|
+
lr=0.1 layer_count=8
|
|
122
|
+
lr=0.1 layer_count=12
|
|
123
|
+
lr=0.01 layer_count=4
|
|
124
|
+
...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This is equivalent to a grid search.
|
|
128
|
+
|
|
129
|
+
## How Overrides Are Combined
|
|
130
|
+
|
|
131
|
+
Within an override block:
|
|
132
|
+
|
|
133
|
+
1. fixed values are applied first.
|
|
134
|
+
2. joint combinations are generated.
|
|
135
|
+
3. product combinations are generated.
|
|
136
|
+
|
|
137
|
+
The final configs are the combination of these rules applied to the base config.
|
|
138
|
+
|
|
139
|
+
## Interface
|
|
140
|
+
|
|
141
|
+
Create configurations with `generate_configs`. It accepts a base config and an
|
|
142
|
+
overrides config.
|
|
143
|
+
|
|
144
|
+
Both inputs can be provided in two ways:
|
|
145
|
+
- as a Python object (e.g., a dictionary or list).
|
|
146
|
+
- as a path to a configuration file (.yaml, .yml, or .json).
|
|
147
|
+
|
|
148
|
+
`generate_configs` returns an iterator that yields each generated config. The
|
|
149
|
+
number of generated configs depends on the combinations produced by the override
|
|
150
|
+
rules. Use `generate_configs_list` if you need all generated configs in a list.
|
|
151
|
+
|
|
152
|
+
Example usage:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from megaconf import generate_configs
|
|
156
|
+
|
|
157
|
+
base_config = {
|
|
158
|
+
"algo": "NN",
|
|
159
|
+
"lr": 0.001,
|
|
160
|
+
"layer_count": 8,
|
|
161
|
+
"activation": "ReLU",
|
|
162
|
+
"initializer": "He"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
overrides = [
|
|
166
|
+
{
|
|
167
|
+
"fixed": {
|
|
168
|
+
"algo": "XGBoost"
|
|
169
|
+
},
|
|
170
|
+
"product": {
|
|
171
|
+
"lr": [0.1, 0.01, 0.001]
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"fixed": {
|
|
176
|
+
"algo": "NN"
|
|
177
|
+
},
|
|
178
|
+
"joint": {
|
|
179
|
+
"activation": ["ReLU", "Tanh"],
|
|
180
|
+
"initializer": ["He", "Glorot"]
|
|
181
|
+
},
|
|
182
|
+
"product": {
|
|
183
|
+
"lr": [0.1, 0.01],
|
|
184
|
+
"layer_count": [4, 8]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
configs = generate_configs(base_config, overrides)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Or if the base and overrides separate config files:
|
|
193
|
+
```python
|
|
194
|
+
configs = generate_configs(
|
|
195
|
+
"base_config.yaml",
|
|
196
|
+
"overrides.yaml"
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Nesting
|
|
201
|
+
If a key is deeply nested in the config, a dot-key separator is used by default. This means that specifying a nested key is done as follows:
|
|
202
|
+
```yaml
|
|
203
|
+
fixed:
|
|
204
|
+
key1.key2.key3: value
|
|
205
|
+
```
|
|
206
|
+
However, if a dot conflicts with your key strings, you can specify a custom key separator as follows:
|
|
207
|
+
```python
|
|
208
|
+
configs = generate_configs(
|
|
209
|
+
"base_config.yaml",
|
|
210
|
+
"overrides.yaml",
|
|
211
|
+
key_separator="::"
|
|
212
|
+
)
|
|
213
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "megaconf"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Easily create many variations of a config."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"numpy>=2.5.2",
|
|
9
|
+
"pydantic>=2.13.5",
|
|
10
|
+
"pyyaml>=6.0.3",
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
license = "MIT"
|
|
17
|
+
license-files = ["LICENSE"]
|
|
18
|
+
|
|
19
|
+
[[project.authors]]
|
|
20
|
+
name = "Richard Wessels"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/RichardWessels/megaconf"
|
|
24
|
+
Issues = "https://github.com/RichardWessels/megaconf/issues"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.1.1",
|
|
29
|
+
"ruff>=0.16.5",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "megaconf"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Easily create many variations of a config."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Richard Wessels" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"numpy>=2.5.2",
|
|
12
|
+
"pydantic>=2.13.5",
|
|
13
|
+
"pyyaml>=6.0.3",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
license = "MIT"
|
|
20
|
+
license-files = ["LICENSE"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/RichardWessels/megaconf"
|
|
24
|
+
Issues = "https://github.com/RichardWessels/megaconf/issues"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=9.1.1",
|
|
29
|
+
"ruff>=0.16.5",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_joint_generator(joint_config: dict[str, list]) -> Iterator[dict]:
|
|
9
|
+
"""Yield joint overrides by zipping aligned list values.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
joint_config: Mapping of keys to equally sized lists.
|
|
13
|
+
|
|
14
|
+
Yields:
|
|
15
|
+
Dictionaries containing one value per key at each shared index.
|
|
16
|
+
"""
|
|
17
|
+
if not joint_config:
|
|
18
|
+
yield {}
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
list_length = len(next(iter(joint_config.values())))
|
|
22
|
+
|
|
23
|
+
for i in range(list_length):
|
|
24
|
+
yield {k: v[i] for k, v in joint_config.items()}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_product_generator(
|
|
28
|
+
product_config: dict[str, list],
|
|
29
|
+
sampling: Literal["without_replacement", "with_replacement"] | None = None,
|
|
30
|
+
n_samples: int | None = None,
|
|
31
|
+
) -> Iterator[dict]:
|
|
32
|
+
"""Yield product overrides from the Cartesian product of list values.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
product_config: Mapping of keys to candidate values.
|
|
36
|
+
sampling: Optional sampling mode over product indices.
|
|
37
|
+
n_samples: Number of sampled combinations when sampling is enabled.
|
|
38
|
+
|
|
39
|
+
Yields:
|
|
40
|
+
Dictionaries containing one chosen value per product key.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If sampling is enabled without ``n_samples`` or mode is invalid.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if not product_config:
|
|
47
|
+
yield {}
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
prod_items = product_config.items()
|
|
51
|
+
prod_keys = [item[0] for item in prod_items]
|
|
52
|
+
prod_values = [item[1] for item in prod_items]
|
|
53
|
+
|
|
54
|
+
n = int(np.prod([len(it) for it in prod_values]))
|
|
55
|
+
|
|
56
|
+
if sampling is None:
|
|
57
|
+
loop_values = range(n)
|
|
58
|
+
else:
|
|
59
|
+
if n_samples is None:
|
|
60
|
+
raise ValueError("Argument `n_samples` required when using sampling.")
|
|
61
|
+
if sampling == "with_replacement":
|
|
62
|
+
loop_values = random.choices(range(n), k=n_samples)
|
|
63
|
+
elif sampling == "without_replacement":
|
|
64
|
+
loop_values = random.sample(range(n), k=n_samples)
|
|
65
|
+
else:
|
|
66
|
+
# illegal state
|
|
67
|
+
raise ValueError("Invalid value for `sampling` given.")
|
|
68
|
+
|
|
69
|
+
for num in loop_values:
|
|
70
|
+
i = num
|
|
71
|
+
|
|
72
|
+
res = []
|
|
73
|
+
for it in reversed(prod_values):
|
|
74
|
+
i, r = divmod(i, len(it))
|
|
75
|
+
res.append(it[r])
|
|
76
|
+
res = list(reversed(res))
|
|
77
|
+
|
|
78
|
+
yield {prod_keys[i]: res[i] for i in range(len(prod_keys))}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import TypeAdapter
|
|
7
|
+
|
|
8
|
+
from .expansion import get_joint_generator, get_product_generator
|
|
9
|
+
from .inputs import load_data_from_file
|
|
10
|
+
from .models import BaseConfigInput, Override, OverridesInput
|
|
11
|
+
from .utils import convert_flat_dict_to_nested_dict, deep_update_dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _generate_configs(
|
|
15
|
+
base_config: dict,
|
|
16
|
+
overrides: list[Override],
|
|
17
|
+
key_separator: str,
|
|
18
|
+
sampling: Literal["without_replacement", "with_replacement"] | None = None,
|
|
19
|
+
n_samples: int | None = None,
|
|
20
|
+
) -> Iterator[dict]:
|
|
21
|
+
"""Generate merged configs for all fixed, joint, and product combinations.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_config: Base configuration to copy and update.
|
|
25
|
+
overrides: Validated override groups.
|
|
26
|
+
key_separator: Separator for flat keys that target nested fields.
|
|
27
|
+
sampling: Optional sampling mode for product combinations.
|
|
28
|
+
n_samples: Number of product combinations to sample.
|
|
29
|
+
|
|
30
|
+
Yields:
|
|
31
|
+
Fully merged configuration dictionaries.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
for override in overrides:
|
|
35
|
+
fixed = override.fixed
|
|
36
|
+
|
|
37
|
+
for joint_conf in get_joint_generator(override.joint):
|
|
38
|
+
# NOTE: only product space is sampled
|
|
39
|
+
for prod_conf in get_product_generator(
|
|
40
|
+
override.product, sampling=sampling, n_samples=n_samples
|
|
41
|
+
):
|
|
42
|
+
new_config = deepcopy(base_config)
|
|
43
|
+
override_dict = fixed | joint_conf | prod_conf
|
|
44
|
+
|
|
45
|
+
override_dict = convert_flat_dict_to_nested_dict(
|
|
46
|
+
override_dict, key_separator
|
|
47
|
+
)
|
|
48
|
+
new_config = deep_update_dict(new_config, override_dict)
|
|
49
|
+
|
|
50
|
+
yield new_config
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def generate_configs(
|
|
54
|
+
base_config: BaseConfigInput,
|
|
55
|
+
overrides: OverridesInput | None,
|
|
56
|
+
key_separator=".",
|
|
57
|
+
sampling: Literal["without_replacement", "with_replacement"] | None = None,
|
|
58
|
+
n_samples: int | None = None,
|
|
59
|
+
) -> Iterator[dict]:
|
|
60
|
+
"""Generate configurations from a base config and override definitions.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
base_config: Base config dictionary or path to YAML/JSON.
|
|
64
|
+
overrides: Override list or path to YAML/JSON override definitions.
|
|
65
|
+
key_separator: Separator used in flat override keys for nesting.
|
|
66
|
+
sampling: Optional sampling mode for product combinations.
|
|
67
|
+
n_samples: Number of product combinations to sample.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
An iterator of generated configuration dictionaries.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
ValueError: If base config does not resolve to a dictionary.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if not isinstance(base_config, dict):
|
|
77
|
+
path = Path(base_config)
|
|
78
|
+
base_config = load_data_from_file(path)
|
|
79
|
+
|
|
80
|
+
if isinstance(overrides, (str, Path)):
|
|
81
|
+
path = Path(overrides)
|
|
82
|
+
overrides = load_data_from_file(path)
|
|
83
|
+
|
|
84
|
+
# validation
|
|
85
|
+
if not isinstance(base_config, dict):
|
|
86
|
+
raise ValueError("Base config must be a dictionary.")
|
|
87
|
+
overrides_validated = TypeAdapter(list[Override]).validate_python(overrides)
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
not overrides
|
|
91
|
+
): # NOTE: need to confirm that this general falsy check is not a problem
|
|
92
|
+
yield base_config
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
yield from _generate_configs(
|
|
96
|
+
base_config, overrides_validated, key_separator, sampling, n_samples
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def generate_configs_list(
|
|
101
|
+
base_config: BaseConfigInput,
|
|
102
|
+
overrides: OverridesInput | None,
|
|
103
|
+
key_separator=".",
|
|
104
|
+
sampling: Literal["without_replacement", "with_replacement"] | None = None,
|
|
105
|
+
n_samples: int | None = None,
|
|
106
|
+
) -> list[dict]:
|
|
107
|
+
"""Return generated configurations as a list.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
base_config: Base config dictionary or path to YAML/JSON.
|
|
111
|
+
overrides: Override list or path to YAML/JSON override definitions.
|
|
112
|
+
key_separator: Separator used in flat override keys for nesting.
|
|
113
|
+
sampling: Optional sampling mode for product combinations.
|
|
114
|
+
n_samples: Number of product combinations to sample.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
All generated configurations materialized in a list.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return list(
|
|
121
|
+
generate_configs(base_config, overrides, key_separator, sampling, n_samples)
|
|
122
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from yaml import safe_load
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_data_from_file(file_path: Path | str) -> Any:
|
|
9
|
+
"""Load YAML or JSON data from disk.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
file_path: Path to a ``.yaml``, ``.yml``, or ``.json`` file.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Parsed file contents.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ValueError: If the file extension is unsupported.
|
|
19
|
+
"""
|
|
20
|
+
file_path = Path(file_path)
|
|
21
|
+
|
|
22
|
+
with open(file_path, encoding="utf-8") as f:
|
|
23
|
+
if file_path.suffix in [".yaml", ".yml"]:
|
|
24
|
+
return safe_load(f)
|
|
25
|
+
if file_path.suffix == ".json":
|
|
26
|
+
return json.load(f)
|
|
27
|
+
raise ValueError("File must end in `.yaml`, `.yml` or `.json`.")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from os import PathLike
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Override(BaseModel):
|
|
8
|
+
fixed: dict[str, Any] = Field(default_factory=dict)
|
|
9
|
+
joint: dict[str, list] = Field(default_factory=dict)
|
|
10
|
+
product: dict[str, list] = Field(default_factory=dict)
|
|
11
|
+
|
|
12
|
+
@field_validator("joint")
|
|
13
|
+
@classmethod
|
|
14
|
+
def validate_joint_lengths(cls, joint: dict[str, list]):
|
|
15
|
+
"""Validate that all lists in ``joint`` have equal length.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
joint: Mapping of override keys to lists.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The validated ``joint`` mapping.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If list lengths differ across ``joint`` keys.
|
|
25
|
+
"""
|
|
26
|
+
lengths = {key: len(value) for key, value in joint.items()}
|
|
27
|
+
|
|
28
|
+
if len(set(lengths.values())) > 1:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"All lists in 'joint' must have equal length. Got lengths: {lengths}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return joint
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
type ConfigDict = dict[str, Any]
|
|
37
|
+
type BaseConfigInput = ConfigDict | str | PathLike[str]
|
|
38
|
+
type OverridesInput = list[dict[str, Any]] | str | PathLike[str]
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def convert_flat_dict_to_nested_dict(
|
|
6
|
+
dictionary: dict[str, Any], key_separator=".", override_duplicates=False
|
|
7
|
+
) -> dict:
|
|
8
|
+
"""
|
|
9
|
+
Convert from format: {"k1.k2": value} -> {"k1": {"k2": value}}
|
|
10
|
+
"""
|
|
11
|
+
output_dict = {}
|
|
12
|
+
|
|
13
|
+
for composite_key, value in dictionary.items():
|
|
14
|
+
current_dict = output_dict
|
|
15
|
+
key_list = composite_key.split(key_separator)
|
|
16
|
+
|
|
17
|
+
# step through dictionary
|
|
18
|
+
for key in key_list[:-1]:
|
|
19
|
+
if key in current_dict and not isinstance(current_dict[key], dict):
|
|
20
|
+
if override_duplicates:
|
|
21
|
+
current_dict[key] = {}
|
|
22
|
+
else:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
f"Duplicate key found: '{composite_key}'. "
|
|
25
|
+
"Set `override_duplicates`=True to skip this error."
|
|
26
|
+
)
|
|
27
|
+
current_dict[key] = current_dict.get(key, {})
|
|
28
|
+
current_dict = current_dict[key]
|
|
29
|
+
|
|
30
|
+
# write value
|
|
31
|
+
key = key_list[-1]
|
|
32
|
+
if key in current_dict and not override_duplicates:
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
f"Duplicate key found: '{composite_key}'. "
|
|
35
|
+
"Set `override_duplicates`=True to skip this error."
|
|
36
|
+
)
|
|
37
|
+
current_dict[key] = value
|
|
38
|
+
return output_dict
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def deep_update_dict(base: dict, override: dict) -> dict:
|
|
42
|
+
"""
|
|
43
|
+
Recursively replaces each key/value in `base` that is also present in `override`
|
|
44
|
+
When key not in `base`, adds the key.
|
|
45
|
+
"""
|
|
46
|
+
output = deepcopy(base)
|
|
47
|
+
|
|
48
|
+
for key, value in override.items():
|
|
49
|
+
if isinstance(value, dict) and isinstance(output.get(key), dict):
|
|
50
|
+
output[key] = deep_update_dict(output[key], value)
|
|
51
|
+
else:
|
|
52
|
+
output[key] = deepcopy(value)
|
|
53
|
+
|
|
54
|
+
return output
|