parameterrun 0.1.0__tar.gz → 0.2.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.
- {parameterrun-0.1.0 → parameterrun-0.2.1}/.github/workflows/ci.yml +15 -30
- {parameterrun-0.1.0 → parameterrun-0.2.1}/.github/workflows/publish.yml +12 -23
- parameterrun-0.2.1/.github/workflows/release.yml +88 -0
- parameterrun-0.2.1/CHANGELOG.md +37 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/PKG-INFO +17 -7
- {parameterrun-0.1.0 → parameterrun-0.2.1}/README.md +16 -6
- {parameterrun-0.1.0 → parameterrun-0.2.1}/pyproject.toml +5 -1
- parameterrun-0.2.1/scripts/bump_version.py +159 -0
- parameterrun-0.2.1/src/parameterrun/__init__.py +5 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/src/parameterrun/parallel_utils.py +75 -7
- {parameterrun-0.1.0 → parameterrun-0.2.1}/tests/test_format_input.py +16 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/tests/test_parameter_run.py +47 -9
- parameterrun-0.1.0/CHANGELOG.md +0 -10
- parameterrun-0.1.0/src/parameterrun/__init__.py +0 -3
- {parameterrun-0.1.0 → parameterrun-0.2.1}/.gitignore +0 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/CONTRIBUTING.md +0 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/LICENSE +0 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/examples/minimal_example.py +0 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/tests/test_get_iteration.py +0 -0
- {parameterrun-0.1.0 → parameterrun-0.2.1}/tests/test_normalized_time.py +0 -0
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
name: CI
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main ]
|
|
6
4
|
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
# push:
|
|
7
|
+
# branches: [ main ]
|
|
8
|
+
# Optional only if you use GitHub merge queue:
|
|
9
|
+
# merge_group:
|
|
10
|
+
# types: [checks_requested]
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
7
14
|
|
|
8
15
|
jobs:
|
|
9
16
|
test:
|
|
@@ -16,7 +23,7 @@ jobs:
|
|
|
16
23
|
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
|
17
24
|
|
|
18
25
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
26
|
+
- uses: actions/checkout@v6
|
|
20
27
|
|
|
21
28
|
- uses: actions/setup-python@v5
|
|
22
29
|
with:
|
|
@@ -31,33 +38,11 @@ jobs:
|
|
|
31
38
|
- name: Lint
|
|
32
39
|
run: uv run ruff check .
|
|
33
40
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
41
|
+
# - name: Format check
|
|
42
|
+
# run: uv run ruff format --check .
|
|
36
43
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
44
|
+
# - name: Type check
|
|
45
|
+
# run: uv run mypy
|
|
39
46
|
|
|
40
47
|
- name: Tests
|
|
41
|
-
run: uv run pytest
|
|
42
|
-
|
|
43
|
-
build:
|
|
44
|
-
name: Build package
|
|
45
|
-
runs-on: ubuntu-latest
|
|
46
|
-
steps:
|
|
47
|
-
- uses: actions/checkout@v4
|
|
48
|
-
|
|
49
|
-
- uses: actions/setup-python@v5
|
|
50
|
-
with:
|
|
51
|
-
python-version: "3.9"
|
|
52
|
-
|
|
53
|
-
- name: Install uv
|
|
54
|
-
run: python -m pip install uv
|
|
55
|
-
|
|
56
|
-
- name: Sync environment
|
|
57
|
-
run: uv sync
|
|
58
|
-
|
|
59
|
-
- name: Build
|
|
60
|
-
run: uv run python -m build
|
|
61
|
-
|
|
62
|
-
- name: Check distributions
|
|
63
|
-
run: uv run twine check --strict dist/*
|
|
48
|
+
run: uv run pytest
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
name: Publish
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
workflow_run:
|
|
5
|
+
workflows: ["Release"]
|
|
6
|
+
types: [completed]
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
|
-
|
|
9
|
+
publish:
|
|
10
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
10
11
|
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
11
13
|
permissions:
|
|
12
14
|
contents: read
|
|
15
|
+
id-token: write
|
|
13
16
|
|
|
14
17
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
with:
|
|
20
|
+
ref: ${{ github.event.workflow_run.head_sha }}
|
|
16
21
|
|
|
17
22
|
- uses: actions/setup-python@v5
|
|
18
23
|
with:
|
|
@@ -30,21 +35,5 @@ jobs:
|
|
|
30
35
|
- name: Check distributions
|
|
31
36
|
run: uv run twine check --strict dist/*
|
|
32
37
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
name: dist
|
|
36
|
-
path: dist/
|
|
37
|
-
|
|
38
|
-
publish:
|
|
39
|
-
needs: build
|
|
40
|
-
runs-on: ubuntu-latest
|
|
41
|
-
permissions:
|
|
42
|
-
id-token: write
|
|
43
|
-
|
|
44
|
-
steps:
|
|
45
|
-
- uses: actions/download-artifact@v4
|
|
46
|
-
with:
|
|
47
|
-
name: dist
|
|
48
|
-
path: dist/
|
|
49
|
-
|
|
50
|
-
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
38
|
+
- name: Publish to PyPI
|
|
39
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: release-main
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
release:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
with:
|
|
21
|
+
ref: ${{ github.sha }}
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
|
|
24
|
+
- name: Read version from package
|
|
25
|
+
id: version
|
|
26
|
+
run: |
|
|
27
|
+
python - <<'PY'
|
|
28
|
+
import os
|
|
29
|
+
import pathlib
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
init_file = pathlib.Path("src/parameterrun/__init__.py")
|
|
33
|
+
content = init_file.read_text(encoding="utf-8")
|
|
34
|
+
match = re.search(r'__version__\s*=\s*"([^"]+)"', content)
|
|
35
|
+
if not match:
|
|
36
|
+
raise SystemExit("Could not find __version__ in src/parameterrun/__init__.py")
|
|
37
|
+
|
|
38
|
+
version = match.group(1)
|
|
39
|
+
output_file = os.environ["GITHUB_OUTPUT"]
|
|
40
|
+
with open(output_file, "a", encoding="utf-8") as f:
|
|
41
|
+
f.write(f"version={version}\n")
|
|
42
|
+
f.write(f"tag=v{version}\n")
|
|
43
|
+
PY
|
|
44
|
+
|
|
45
|
+
- name: Fail if tag already exists
|
|
46
|
+
env:
|
|
47
|
+
TAG: ${{ steps.version.outputs.tag }}
|
|
48
|
+
run: |
|
|
49
|
+
git fetch --tags origin
|
|
50
|
+
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
|
|
51
|
+
echo "Tag ${TAG} already exists. Bump __version__ before merging to main."
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
- name: Extract release notes from changelog
|
|
56
|
+
env:
|
|
57
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
58
|
+
run: |
|
|
59
|
+
python - <<'PY'
|
|
60
|
+
import os
|
|
61
|
+
import pathlib
|
|
62
|
+
import re
|
|
63
|
+
|
|
64
|
+
version = os.environ["VERSION"]
|
|
65
|
+
changelog = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
pattern = re.compile(
|
|
68
|
+
rf"^## \[{re.escape(version)}\][^\n]*\n(?P<body>.*?)(?=^## \[|\Z)",
|
|
69
|
+
re.MULTILINE | re.DOTALL,
|
|
70
|
+
)
|
|
71
|
+
match = pattern.search(changelog)
|
|
72
|
+
if not match:
|
|
73
|
+
raise SystemExit(f"Could not find CHANGELOG entry for version {version}")
|
|
74
|
+
|
|
75
|
+
notes = match.group("body").strip()
|
|
76
|
+
pathlib.Path("release_notes.md").write_text(notes + "\n", encoding="utf-8")
|
|
77
|
+
PY
|
|
78
|
+
|
|
79
|
+
- name: Create GitHub release and tag
|
|
80
|
+
env:
|
|
81
|
+
GH_TOKEN: ${{ github.token }}
|
|
82
|
+
TAG: ${{ steps.version.outputs.tag }}
|
|
83
|
+
SHA: ${{ github.sha }}
|
|
84
|
+
run: |
|
|
85
|
+
gh release create "${TAG}" \
|
|
86
|
+
--target "${SHA}" \
|
|
87
|
+
--title "${TAG}" \
|
|
88
|
+
--notes-file release_notes.md
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on Keep a Changelog and the project follows Semantic Versioning.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.1] - 2026-04-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Modified GitHub Actions workflow to trigger on pull requests to the main branch, ensuring that all changes are
|
|
14
|
+
reviewed before merging.
|
|
15
|
+
|
|
16
|
+
## [0.2.0] - 2026-03-26
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Automated version management: single source of truth in `src/parameterrun/__init__.py`
|
|
21
|
+
- Version bump script `scripts/bump_version.py` for automatic version updates across all files
|
|
22
|
+
- VERSIONING.md guide for release workflows and version management
|
|
23
|
+
- `__version__` exported from main package: `from parameterrun import __version__`
|
|
24
|
+
- Dynamic version extraction in `pyproject.toml` (no more manual version updates needed)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `parameterrun(..., reshape=True)` now reshapes outputs to the parameter-grid dimensions automatically (including
|
|
29
|
+
multi-output functions).
|
|
30
|
+
- Added early validation for duplicate parameter names, unknown swept parameters, unknown kwargs, and kwarg/sweep-name
|
|
31
|
+
conflicts.
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2026-03-25
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Initial public release of `parameterrun`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parameterrun
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Parallel parameter sweeps with joblib/MPI and tqdm progress bars.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Davtax/ParameterRun
|
|
6
6
|
Project-URL: Repository, https://github.com/Davtax/ParameterRun
|
|
@@ -104,7 +104,6 @@ def f(x, y, scale=1):
|
|
|
104
104
|
|
|
105
105
|
result = parameterrun(f, param_names=["x", "y"], param_values=[[1, 2, 3], [10, 20], ], scale=2, )
|
|
106
106
|
|
|
107
|
-
result = result.reshape(3, 2)
|
|
108
107
|
print(result.shape) # (3, 2)
|
|
109
108
|
```
|
|
110
109
|
|
|
@@ -121,10 +120,9 @@ def f(x, y, z):
|
|
|
121
120
|
|
|
122
121
|
|
|
123
122
|
result = parameterrun(f, param_names=[["x", "y"], ["z"]], param_values=[[[1, 2], [10, 20]], # (x, y) = (1,10), (2,20)
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
[[100, 200, 300]], # z varies independently
|
|
124
|
+
], n_workers=1, )
|
|
126
125
|
|
|
127
|
-
result = result.reshape(2, 3)
|
|
128
126
|
print(result.shape) # (2, 3)
|
|
129
127
|
```
|
|
130
128
|
|
|
@@ -152,6 +150,18 @@ print(square_like) # array([1, 4, 9])
|
|
|
152
150
|
|
|
153
151
|
If the function returns a single object, the result is returned as a single array when reshaping is possible.
|
|
154
152
|
|
|
153
|
+
For multi-dimensional sweeps, reshaping is automatic when `reshape=True` (default): the first dimensions always match
|
|
154
|
+
the parameter grid shape.
|
|
155
|
+
|
|
156
|
+
## Validation behavior
|
|
157
|
+
|
|
158
|
+
`parameterrun` validates inputs before launching workers and raises clear `ValueError`s when:
|
|
159
|
+
|
|
160
|
+
- parameter names are duplicated
|
|
161
|
+
- a swept parameter does not exist in the target function signature
|
|
162
|
+
- extra keyword arguments do not match the target function signature
|
|
163
|
+
- a keyword argument conflicts with a swept parameter name
|
|
164
|
+
|
|
155
165
|
## Parallel backend
|
|
156
166
|
|
|
157
167
|
`parameterrun` supports two execution backends:
|
|
@@ -166,7 +176,7 @@ If `backend=None`, the package tries to detect whether MPI is available. If MPI
|
|
|
166
176
|
|
|
167
177
|
```python
|
|
168
178
|
result = parameterrun(my_function, param_names=["x", "y"], param_values=[[1, 2, 3], [4, 5]], backend="joblib",
|
|
169
|
-
|
|
179
|
+
n_workers=-1, )
|
|
170
180
|
```
|
|
171
181
|
|
|
172
182
|
If `n_workers=1`, the function runs serially without spawning joblib workers.
|
|
@@ -214,7 +224,7 @@ The progress-bar description is generated automatically from the function name a
|
|
|
214
224
|
|
|
215
225
|
```python
|
|
216
226
|
parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbose=False, pbar_kwargs=None,
|
|
217
|
-
|
|
227
|
+
reshape=True, backend=None, desc=None, **kwargs, )
|
|
218
228
|
```
|
|
219
229
|
|
|
220
230
|
* `fun`: function to evaluate
|
|
@@ -72,7 +72,6 @@ def f(x, y, scale=1):
|
|
|
72
72
|
|
|
73
73
|
result = parameterrun(f, param_names=["x", "y"], param_values=[[1, 2, 3], [10, 20], ], scale=2, )
|
|
74
74
|
|
|
75
|
-
result = result.reshape(3, 2)
|
|
76
75
|
print(result.shape) # (3, 2)
|
|
77
76
|
```
|
|
78
77
|
|
|
@@ -89,10 +88,9 @@ def f(x, y, z):
|
|
|
89
88
|
|
|
90
89
|
|
|
91
90
|
result = parameterrun(f, param_names=[["x", "y"], ["z"]], param_values=[[[1, 2], [10, 20]], # (x, y) = (1,10), (2,20)
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
[[100, 200, 300]], # z varies independently
|
|
92
|
+
], n_workers=1, )
|
|
94
93
|
|
|
95
|
-
result = result.reshape(2, 3)
|
|
96
94
|
print(result.shape) # (2, 3)
|
|
97
95
|
```
|
|
98
96
|
|
|
@@ -120,6 +118,18 @@ print(square_like) # array([1, 4, 9])
|
|
|
120
118
|
|
|
121
119
|
If the function returns a single object, the result is returned as a single array when reshaping is possible.
|
|
122
120
|
|
|
121
|
+
For multi-dimensional sweeps, reshaping is automatic when `reshape=True` (default): the first dimensions always match
|
|
122
|
+
the parameter grid shape.
|
|
123
|
+
|
|
124
|
+
## Validation behavior
|
|
125
|
+
|
|
126
|
+
`parameterrun` validates inputs before launching workers and raises clear `ValueError`s when:
|
|
127
|
+
|
|
128
|
+
- parameter names are duplicated
|
|
129
|
+
- a swept parameter does not exist in the target function signature
|
|
130
|
+
- extra keyword arguments do not match the target function signature
|
|
131
|
+
- a keyword argument conflicts with a swept parameter name
|
|
132
|
+
|
|
123
133
|
## Parallel backend
|
|
124
134
|
|
|
125
135
|
`parameterrun` supports two execution backends:
|
|
@@ -134,7 +144,7 @@ If `backend=None`, the package tries to detect whether MPI is available. If MPI
|
|
|
134
144
|
|
|
135
145
|
```python
|
|
136
146
|
result = parameterrun(my_function, param_names=["x", "y"], param_values=[[1, 2, 3], [4, 5]], backend="joblib",
|
|
137
|
-
|
|
147
|
+
n_workers=-1, )
|
|
138
148
|
```
|
|
139
149
|
|
|
140
150
|
If `n_workers=1`, the function runs serially without spawning joblib workers.
|
|
@@ -182,7 +192,7 @@ The progress-bar description is generated automatically from the function name a
|
|
|
182
192
|
|
|
183
193
|
```python
|
|
184
194
|
parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbose=False, pbar_kwargs=None,
|
|
185
|
-
|
|
195
|
+
reshape=True, backend=None, desc=None, **kwargs, )
|
|
186
196
|
```
|
|
187
197
|
|
|
188
198
|
* `fun`: function to evaluate
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "parameterrun"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Parallel parameter sweeps with joblib/MPI and tqdm progress bars."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -54,6 +54,10 @@ dev = [
|
|
|
54
54
|
"twine>=6",
|
|
55
55
|
]
|
|
56
56
|
|
|
57
|
+
[tool.hatch.version]
|
|
58
|
+
path = "src/parameterrun/__init__.py"
|
|
59
|
+
pattern = '__version__ = "(?P<version>[^"]+)"'
|
|
60
|
+
|
|
57
61
|
[tool.hatch.build.targets.wheel]
|
|
58
62
|
packages = ["src/parameterrun"]
|
|
59
63
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Script to automate version bumping across all project files.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python scripts/bump_version.py <new_version>
|
|
7
|
+
python scripts/bump_version.py --patch
|
|
8
|
+
python scripts/bump_version.py --minor
|
|
9
|
+
python scripts/bump_version.py --major
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
python scripts/bump_version.py 0.2.0
|
|
13
|
+
python scripts/bump_version.py --minor # bumps X.Y to X.Y+1
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_version(version_str: str) -> Tuple[int, int, int]:
|
|
24
|
+
"""Parse semantic version string into (major, minor, patch)."""
|
|
25
|
+
parts = version_str.split(".")
|
|
26
|
+
if len(parts) != 3:
|
|
27
|
+
raise ValueError(f"Invalid version format: {version_str}. Expected: X.Y.Z")
|
|
28
|
+
try:
|
|
29
|
+
major, minor, patch = (int(p) for p in parts)
|
|
30
|
+
return major, minor, patch
|
|
31
|
+
except ValueError as err:
|
|
32
|
+
raise ValueError(f"Version parts must be integers: {version_str}") from err
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def version_to_string(major: int, minor: int, patch: int) -> str:
|
|
36
|
+
"""Convert (major, minor, patch) to string."""
|
|
37
|
+
return f"{major}.{minor}.{patch}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bump_version(version_str: str, bump_type: str) -> str:
|
|
41
|
+
"""Bump version based on type: 'major', 'minor', or 'patch'."""
|
|
42
|
+
major, minor, patch = parse_version(version_str)
|
|
43
|
+
|
|
44
|
+
if bump_type == "major":
|
|
45
|
+
major += 1
|
|
46
|
+
minor = 0
|
|
47
|
+
patch = 0
|
|
48
|
+
elif bump_type == "minor":
|
|
49
|
+
minor += 1
|
|
50
|
+
patch = 0
|
|
51
|
+
elif bump_type == "patch":
|
|
52
|
+
patch += 1
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(f"Unknown bump type: {bump_type}")
|
|
55
|
+
|
|
56
|
+
return version_to_string(major, minor, patch)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_current_version(init_file: Path) -> str:
|
|
60
|
+
"""Extract current version from __init__.py."""
|
|
61
|
+
content = init_file.read_text()
|
|
62
|
+
match = re.search(r'__version__\s*=\s*"([^"]+)"', content)
|
|
63
|
+
if not match:
|
|
64
|
+
raise ValueError(f"Could not find __version__ in {init_file}")
|
|
65
|
+
return match.group(1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def update_init_file(init_file: Path, new_version: str) -> None:
|
|
69
|
+
"""Update version in __init__.py."""
|
|
70
|
+
content = init_file.read_text()
|
|
71
|
+
updated = re.sub(
|
|
72
|
+
r'__version__\s*=\s*"[^"]+"',
|
|
73
|
+
f'__version__ = "{new_version}"',
|
|
74
|
+
content
|
|
75
|
+
)
|
|
76
|
+
init_file.write_text(updated)
|
|
77
|
+
print(f"✓ Updated {init_file.relative_to(init_file.parent.parent.parent)}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def update_changelog(changelog_file: Path, new_version: str) -> None:
|
|
81
|
+
"""Update CHANGELOG.md with new version section."""
|
|
82
|
+
content = changelog_file.read_text()
|
|
83
|
+
|
|
84
|
+
# Create new version entry
|
|
85
|
+
date = datetime.now().strftime("%Y-%m-%d")
|
|
86
|
+
new_entry = f"""## [{new_version}] - {date}
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
- (Your changes here)
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# Replace [Unreleased] section with new version and restore [Unreleased]
|
|
94
|
+
updated = re.sub(
|
|
95
|
+
r"## \[Unreleased\]",
|
|
96
|
+
f"{new_entry}## [Unreleased]",
|
|
97
|
+
content
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# If no changes were made, insert new version after the header
|
|
101
|
+
if updated == content:
|
|
102
|
+
lines = content.split("\n")
|
|
103
|
+
insert_idx = next(i for i, line in enumerate(lines) if line.startswith("## "))
|
|
104
|
+
lines.insert(insert_idx, new_entry.rstrip("\n"))
|
|
105
|
+
updated = "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
changelog_file.write_text(updated)
|
|
108
|
+
print(f"✓ Updated {changelog_file.relative_to(changelog_file.parent.parent.parent)}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main() -> None:
|
|
112
|
+
project_root = Path(__file__).parent.parent
|
|
113
|
+
init_file = project_root / "src" / "parameterrun" / "__init__.py"
|
|
114
|
+
changelog_file = project_root / "CHANGELOG.md"
|
|
115
|
+
|
|
116
|
+
# Parse arguments
|
|
117
|
+
if len(sys.argv) < 2:
|
|
118
|
+
print(__doc__)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
arg = sys.argv[1]
|
|
122
|
+
|
|
123
|
+
# Get current version
|
|
124
|
+
current_version = get_current_version(init_file)
|
|
125
|
+
print(f"Current version: {current_version}\n")
|
|
126
|
+
|
|
127
|
+
# Determine new version
|
|
128
|
+
if arg.startswith("--"):
|
|
129
|
+
bump_type = arg[2:]
|
|
130
|
+
if bump_type not in ["major", "minor", "patch"]:
|
|
131
|
+
print(f"Invalid bump type: {bump_type}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
new_version = bump_version(current_version, bump_type)
|
|
134
|
+
else:
|
|
135
|
+
# Validate provided version format
|
|
136
|
+
new_version = arg
|
|
137
|
+
try:
|
|
138
|
+
parse_version(new_version)
|
|
139
|
+
except ValueError as e:
|
|
140
|
+
print(f"Error: {e}")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
print(f"New version: {new_version}\n")
|
|
144
|
+
|
|
145
|
+
# Update files
|
|
146
|
+
try:
|
|
147
|
+
update_init_file(init_file, new_version)
|
|
148
|
+
update_changelog(changelog_file, new_version)
|
|
149
|
+
print(f"\n✓ Version successfully bumped to {new_version}")
|
|
150
|
+
print(" Remember to: commit, tag, and push the changes")
|
|
151
|
+
print(f" Example: git tag v{new_version}")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Error during update: {e}", file=sys.stderr)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
|
159
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
+
import inspect
|
|
2
3
|
import sys
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from itertools import product
|
|
@@ -197,18 +198,62 @@ def _get_iteration(index, param_names, param_values):
|
|
|
197
198
|
return dic_temp
|
|
198
199
|
|
|
199
200
|
|
|
201
|
+
def _flatten_param_names(param_names: List[List[str]]) -> List[str]:
|
|
202
|
+
return [name for group in param_names for name in group]
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _validate_function_arguments(fun: Callable[..., Any], param_names: List[List[str]], fun_kwargs: dict) -> None:
|
|
206
|
+
signature = inspect.signature(fun)
|
|
207
|
+
parameters = signature.parameters
|
|
208
|
+
accepts_var_kwargs = any(parameter.kind == inspect.Parameter.VAR_KEYWORD for parameter in parameters.values())
|
|
209
|
+
|
|
210
|
+
sweep_names = _flatten_param_names(param_names)
|
|
211
|
+
|
|
212
|
+
duplicated = sorted({name for name in sweep_names if sweep_names.count(name) > 1})
|
|
213
|
+
if duplicated:
|
|
214
|
+
raise ValueError(f'Duplicate parameter names are not allowed: {duplicated}')
|
|
215
|
+
|
|
216
|
+
overlapping_kwargs = sorted(set(sweep_names).intersection(fun_kwargs))
|
|
217
|
+
if overlapping_kwargs:
|
|
218
|
+
raise ValueError(f'Keyword arguments conflict with swept parameters: {overlapping_kwargs}. '
|
|
219
|
+
f'Remove them from kwargs or param_names.')
|
|
220
|
+
|
|
221
|
+
if accepts_var_kwargs:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
known_names = set(parameters)
|
|
225
|
+
unknown_sweep = sorted(name for name in sweep_names if name not in known_names)
|
|
226
|
+
if unknown_sweep:
|
|
227
|
+
raise ValueError(f'Unknown swept parameter names for function {fun.__name__}: {unknown_sweep}')
|
|
228
|
+
|
|
229
|
+
unknown_kwargs = sorted(name for name in fun_kwargs if name not in known_names)
|
|
230
|
+
if unknown_kwargs:
|
|
231
|
+
raise ValueError(f'Unknown keyword arguments for function {fun.__name__}: {unknown_kwargs}')
|
|
232
|
+
|
|
233
|
+
|
|
200
234
|
def _format_input(param_names: Union[str, list], param_values: list) -> Tuple[List[List[str]], List[List[List]]]:
|
|
201
235
|
# Check the depth of the parameters
|
|
202
236
|
|
|
203
|
-
if
|
|
237
|
+
if isinstance(param_names, str):
|
|
238
|
+
if not param_names:
|
|
239
|
+
raise ValueError('Parameter names must be non-empty strings')
|
|
240
|
+
if not isinstance(param_values, list):
|
|
241
|
+
raise ValueError('The parameters values must be a list of values')
|
|
204
242
|
depth = 1
|
|
205
|
-
elif
|
|
243
|
+
elif isinstance(param_names, list) and len(param_names) > 0 and all(isinstance(name, str) for name in param_names):
|
|
244
|
+
if any(not name for name in param_names):
|
|
245
|
+
raise ValueError('Parameter names must be non-empty strings')
|
|
206
246
|
depth = 2
|
|
207
|
-
if
|
|
247
|
+
if not isinstance(param_values, list):
|
|
208
248
|
raise ValueError('The parameters values must be a list of values')
|
|
209
|
-
elif
|
|
249
|
+
elif isinstance(param_names, list) and len(param_names) > 0 and all(
|
|
250
|
+
isinstance(group, list) for group in param_names):
|
|
251
|
+
if any(len(group) == 0 for group in param_names):
|
|
252
|
+
raise ValueError('Parameter name groups must contain at least one parameter')
|
|
253
|
+
if any(any(not isinstance(name, str) or not name for name in group) for group in param_names):
|
|
254
|
+
raise ValueError('Parameter names must be non-empty strings')
|
|
210
255
|
depth = 3
|
|
211
|
-
if
|
|
256
|
+
if not isinstance(param_values, list) or len(param_values) == 0 or not isinstance(param_values[0], list):
|
|
212
257
|
raise ValueError('The parameters values must be a list of values')
|
|
213
258
|
else:
|
|
214
259
|
raise ValueError('Unknown input format for the parameters names')
|
|
@@ -227,14 +272,36 @@ def _format_input(param_names: Union[str, list], param_values: list) -> Tuple[Li
|
|
|
227
272
|
if len(param_names) != len(param_values):
|
|
228
273
|
raise ValueError('The number of groups of parameters must be the same as the number of groups of values')
|
|
229
274
|
|
|
275
|
+
for names_group, values_group in zip(param_names, param_values):
|
|
276
|
+
if not isinstance(values_group, list):
|
|
277
|
+
raise ValueError('Each parameter group values must be provided as a list')
|
|
278
|
+
if len(names_group) != len(values_group):
|
|
279
|
+
raise ValueError('Each parameter group must include one values list per parameter')
|
|
280
|
+
if any(not isinstance(parameter_values, list) for parameter_values in values_group):
|
|
281
|
+
raise ValueError('Each parameter values entry must be a list')
|
|
282
|
+
|
|
230
283
|
# Check that all parameters in the same group have the same length
|
|
231
284
|
for group in param_values:
|
|
232
285
|
if len(set([len(param) for param in group])) != 1:
|
|
233
286
|
raise ValueError('All parameters in the same group must have the same length')
|
|
234
287
|
|
|
288
|
+
all_names = _flatten_param_names(param_names) # type: ignore[arg-type]
|
|
289
|
+
duplicated = sorted({name for name in all_names if all_names.count(name) > 1})
|
|
290
|
+
if duplicated:
|
|
291
|
+
raise ValueError(f'Duplicate parameter names are not allowed: {duplicated}')
|
|
292
|
+
|
|
235
293
|
return param_names, param_values # type: ignore[return-value]
|
|
236
294
|
|
|
237
295
|
|
|
296
|
+
def _reshape_channel_result(channel_values: List[Any], n_values: List[int]) -> np.ndarray:
|
|
297
|
+
array_result = np.array(channel_values)
|
|
298
|
+
if array_result.ndim == 0:
|
|
299
|
+
raise ValueError('Could not reshape scalar output')
|
|
300
|
+
|
|
301
|
+
target_shape = tuple(n_values) + tuple(array_result.shape[1:])
|
|
302
|
+
return array_result.reshape(target_shape)
|
|
303
|
+
|
|
304
|
+
|
|
238
305
|
def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], List[List[str]]],
|
|
239
306
|
param_values: Union[List, List[List], List[List[List]]], n_workers: Optional[int] = -1,
|
|
240
307
|
pbar_bool: bool = True, verbose: Optional[bool] = False, pbar_kwargs: Optional[dict] = None,
|
|
@@ -298,6 +365,7 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
298
365
|
time_start = time()
|
|
299
366
|
|
|
300
367
|
param_names, param_values = _format_input(param_names, param_values)
|
|
368
|
+
_validate_function_arguments(fun, param_names, kwargs)
|
|
301
369
|
|
|
302
370
|
# Create the list of dictionaries with the parameters in a nested loop
|
|
303
371
|
n_values = [len(group[0]) for group in param_values]
|
|
@@ -362,9 +430,9 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
362
430
|
if reshape:
|
|
363
431
|
try:
|
|
364
432
|
if n_outputs == 1:
|
|
365
|
-
result =
|
|
433
|
+
result = _reshape_channel_result(result[0], n_values)
|
|
366
434
|
else:
|
|
367
|
-
result = [
|
|
435
|
+
result = [_reshape_channel_result(result[i], n_values) for i in range(n_outputs)]
|
|
368
436
|
except ValueError:
|
|
369
437
|
print('Could not reshape the result')
|
|
370
438
|
|
|
@@ -18,3 +18,19 @@ def test_format_input_multiple_parameters():
|
|
|
18
18
|
def test_format_input_group_length_mismatch_raises():
|
|
19
19
|
with pytest.raises(ValueError, match="same length"):
|
|
20
20
|
_format_input([["x", "y"]], [[[1, 2], [10]]])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_format_input_rejects_empty_parameter_name():
|
|
24
|
+
with pytest.raises(ValueError, match="non-empty"):
|
|
25
|
+
_format_input("", [1, 2])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_format_input_rejects_non_list_group_values():
|
|
29
|
+
with pytest.raises(ValueError, match="values entry must be a list"):
|
|
30
|
+
_format_input([["x"]], [[(1, 2)]])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_format_input_rejects_duplicate_names():
|
|
34
|
+
with pytest.raises(ValueError, match="Duplicate parameter names"):
|
|
35
|
+
_format_input(["x", "x"], [[1, 2], [3, 4]])
|
|
36
|
+
|
|
@@ -9,7 +9,7 @@ def test_parameterrun_single_parameter_returns_1d_array():
|
|
|
9
9
|
return x ** 2
|
|
10
10
|
|
|
11
11
|
result = pu.parameterrun(square, param_names="x", param_values=[0, 1, 2, 3], n_workers=1, pbar_bool=False,
|
|
12
|
-
|
|
12
|
+
backend="joblib", )
|
|
13
13
|
|
|
14
14
|
assert isinstance(result, np.ndarray)
|
|
15
15
|
np.testing.assert_array_equal(result, np.array([0, 1, 4, 9]))
|
|
@@ -20,11 +20,9 @@ def test_parameterrun_grouped_parameters_builds_expected_grid():
|
|
|
20
20
|
return x + y + z + offset
|
|
21
21
|
|
|
22
22
|
result = pu.parameterrun(f, param_names=[["x", "y"], ["z"]],
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
result = result.reshape(2, 3) # Reshape to match the expected output shape
|
|
23
|
+
param_values=[[[1, 2], [10, 20]], # grouped pairs: (1,10), (2,20)
|
|
24
|
+
[[100, 200, 300]], # second group scanned independently
|
|
25
|
+
], n_workers=1, pbar_bool=False, backend="joblib", offset=5, )
|
|
28
26
|
|
|
29
27
|
expected = np.array([[116, 216, 316], # (x,y) = (1,10)
|
|
30
28
|
[127, 227, 327], # (x,y) = (2,20)
|
|
@@ -40,7 +38,7 @@ def test_parameterrun_multiple_outputs_returns_list_of_arrays():
|
|
|
40
38
|
return x + 1, x ** 2
|
|
41
39
|
|
|
42
40
|
result = pu.parameterrun(f, param_names="x", param_values=[1, 2, 3], n_workers=1, pbar_bool=False,
|
|
43
|
-
|
|
41
|
+
backend="joblib", )
|
|
44
42
|
|
|
45
43
|
assert isinstance(result, list)
|
|
46
44
|
assert len(result) == 2
|
|
@@ -49,10 +47,25 @@ def test_parameterrun_multiple_outputs_returns_list_of_arrays():
|
|
|
49
47
|
np.testing.assert_array_equal(result[1], np.array([1, 4, 9]))
|
|
50
48
|
|
|
51
49
|
|
|
50
|
+
def test_parameterrun_multiple_outputs_keep_grid_shape():
|
|
51
|
+
def f(x, y):
|
|
52
|
+
return x + y, x * y
|
|
53
|
+
|
|
54
|
+
result = pu.parameterrun(f, param_names=["x", "y"], param_values=[[1, 2], [10, 20, 30]], n_workers=1,
|
|
55
|
+
pbar_bool=False, backend="joblib", )
|
|
56
|
+
|
|
57
|
+
assert isinstance(result, list)
|
|
58
|
+
assert len(result) == 2
|
|
59
|
+
assert result[0].shape == (2, 3)
|
|
60
|
+
assert result[1].shape == (2, 3)
|
|
61
|
+
|
|
62
|
+
np.testing.assert_array_equal(result[0], np.array([[11, 21, 31], [12, 22, 32]]))
|
|
63
|
+
np.testing.assert_array_equal(result[1], np.array([[10, 20, 30], [20, 40, 60]]))
|
|
64
|
+
|
|
65
|
+
|
|
52
66
|
def test_parameterrun_rejects_empty_parameter_groups():
|
|
53
67
|
with pytest.raises(ValueError, match="at least one value"):
|
|
54
|
-
pu.parameterrun(lambda x: x, param_names="x", param_values=[], n_workers=1, pbar_bool=False,
|
|
55
|
-
backend="joblib", )
|
|
68
|
+
pu.parameterrun(lambda x: x, param_names="x", param_values=[], n_workers=1, pbar_bool=False, backend="joblib", )
|
|
56
69
|
|
|
57
70
|
|
|
58
71
|
def test_parameterrun_explicit_mpi_requires_mpi4py(monkeypatch):
|
|
@@ -74,3 +87,28 @@ def test_parameterrun_explicit_mpi_requires_mpi4py(monkeypatch):
|
|
|
74
87
|
def test_parameterrun_invalid_backend():
|
|
75
88
|
with pytest.raises(ValueError, match="Unknown backend"):
|
|
76
89
|
pu.parameterrun(lambda x: x, "x", [1, 2], backend="bad", pbar_bool=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_parameterrun_rejects_unknown_swept_parameters():
|
|
93
|
+
def f(x):
|
|
94
|
+
return x
|
|
95
|
+
|
|
96
|
+
with pytest.raises(ValueError, match="Unknown swept parameter"):
|
|
97
|
+
pu.parameterrun(f, param_names="y", param_values=[1, 2], n_workers=1, pbar_bool=False, backend="joblib", )
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_parameterrun_rejects_unknown_kwargs():
|
|
101
|
+
def f(x):
|
|
102
|
+
return x
|
|
103
|
+
|
|
104
|
+
with pytest.raises(ValueError, match="Unknown keyword arguments"):
|
|
105
|
+
pu.parameterrun(f, param_names="x", param_values=[1, 2], extra=3, n_workers=1, pbar_bool=False,
|
|
106
|
+
backend="joblib", )
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_parameterrun_rejects_kwargs_conflicting_with_swept_parameters():
|
|
110
|
+
def f(x):
|
|
111
|
+
return x
|
|
112
|
+
|
|
113
|
+
with pytest.raises(ValueError, match="conflict with swept parameters"):
|
|
114
|
+
pu.parameterrun(f, param_names="x", param_values=[1, 2], x=99, n_workers=1, pbar_bool=False, backend="joblib", )
|
parameterrun-0.1.0/CHANGELOG.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|