parameterrun 0.3.0__tar.gz → 0.4.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.4.1/.github/workflows/ci.yml +65 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/.github/workflows/publish.yml +1 -1
- {parameterrun-0.3.0 → parameterrun-0.4.1}/CHANGELOG.md +18 -8
- {parameterrun-0.3.0 → parameterrun-0.4.1}/CONTRIBUTING.md +10 -1
- {parameterrun-0.3.0 → parameterrun-0.4.1}/PKG-INFO +11 -9
- {parameterrun-0.3.0 → parameterrun-0.4.1}/README.md +10 -8
- parameterrun-0.4.1/examples/long_example.py +16 -0
- parameterrun-0.4.1/examples/minimal_example_mpi.py +21 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/pyproject.toml +3 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/src/parameterrun/__init__.py +1 -1
- {parameterrun-0.3.0 → parameterrun-0.4.1}/src/parameterrun/parallel_utils.py +47 -28
- parameterrun-0.4.1/tests/test_mpi_integration.py +30 -0
- parameterrun-0.4.1/tests/test_pbar_format.py +106 -0
- parameterrun-0.3.0/.github/workflows/ci.yml +0 -40
- {parameterrun-0.3.0 → parameterrun-0.4.1}/.github/workflows/release.yml +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/.gitignore +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/LICENSE +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/examples/minimal_example.py +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/scripts/bump_version.py +0 -0
- /parameterrun-0.3.0/tests/test_dephs.py → /parameterrun-0.4.1/tests/test_depths.py +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_format_input.py +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_get_iteration.py +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_joblib.py +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_normalized_time.py +0 -0
- {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_parameter_run.py +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
name: non-mpi / ${{ matrix.os }} / py${{ matrix.python-version }}
|
|
13
|
+
runs-on: ${{ matrix.os }}
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
|
18
|
+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Install uv
|
|
28
|
+
run: python -m pip install uv
|
|
29
|
+
|
|
30
|
+
- name: Sync environment
|
|
31
|
+
run: uv sync
|
|
32
|
+
|
|
33
|
+
- name: Tests
|
|
34
|
+
run: uv run pytest -m "not mpi"
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: uv run ruff check .
|
|
38
|
+
|
|
39
|
+
- name: Type check
|
|
40
|
+
run: uv run mypy
|
|
41
|
+
|
|
42
|
+
test-mpi-linux:
|
|
43
|
+
name: mpi / ubuntu-latest / py3.12 / 4 ranks
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v6
|
|
48
|
+
|
|
49
|
+
- uses: actions/setup-python@v6
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.12"
|
|
52
|
+
|
|
53
|
+
- name: Setup MPI
|
|
54
|
+
uses: mpi4py/setup-mpi@v1
|
|
55
|
+
with:
|
|
56
|
+
mpi: 'openmpi'
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
run: python -m pip install uv
|
|
60
|
+
|
|
61
|
+
- name: Sync environment with MPI extra
|
|
62
|
+
run: uv sync --extra mpi
|
|
63
|
+
|
|
64
|
+
- name: MPI tests (4 ranks)
|
|
65
|
+
run: mpirun -n 4 uv run pytest -m mpi -o addopts="-q -m mpi"
|
|
@@ -4,22 +4,32 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on Keep a Changelog and the project follows Semantic Versioning.
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- (Your changes here)
|
|
9
|
+
## [0.4.1] - 2026-04-21
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fix the display of the progress bar when running with `backend="joblib"`, since the previous implementation didn't
|
|
14
|
+
have access to the total number of runs to compute, which is required for the progress bar to work properly.
|
|
13
15
|
|
|
14
16
|
### Added
|
|
15
|
-
- (Your changes here)
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
- Added a test for the progress bar display when running with `backend="joblib"`.
|
|
19
|
+
|
|
20
|
+
## [0.4.0] - 2026-04-21
|
|
18
21
|
|
|
19
22
|
### Added
|
|
20
|
-
- (Your changes here)
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
- Added `tests/test_mpi_integration.py` with MPI numerical-equivalence coverage for `parameterrun(..., backend="mpi")`.
|
|
25
|
+
- Added a dedicated Linux PR CI job that runs MPI tests with 4 ranks.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Updated default test commands in docs and CI to run non-MPI tests with `-m "not mpi"`.
|
|
30
|
+
- Registered a pytest `mpi` marker in `pyproject.toml` for explicit MPI test selection.
|
|
31
|
+
- Update how the indices to compute are shared among workers. Previously, all the indices where computed by all workers,
|
|
32
|
+
which caused a significan overhead in the memory usage. Now, the indices are computed on the fly.
|
|
23
33
|
|
|
24
34
|
## [0.3.0] - 2026-04-01
|
|
25
35
|
|
|
@@ -13,13 +13,22 @@ If you do not use `uv`, create a virtual environment and install equivalent dev
|
|
|
13
13
|
## Run quality checks
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
uv run pytest
|
|
16
|
+
uv run pytest -m "not mpi"
|
|
17
17
|
uv run ruff check .
|
|
18
18
|
uv run mypy
|
|
19
19
|
uv run python -m build
|
|
20
20
|
uv run python -m twine check dist/*
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## Run MPI checks (required on PR CI)
|
|
24
|
+
|
|
25
|
+
The PR workflow includes a dedicated Linux MPI job that runs with 4 ranks.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv sync --extra mpi
|
|
29
|
+
mpirun -n 4 uv run pytest -m mpi -o addopts="-q -m mpi"
|
|
30
|
+
```
|
|
31
|
+
|
|
23
32
|
## Pull request guidelines
|
|
24
33
|
|
|
25
34
|
- Keep PRs focused and small when possible.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parameterrun
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -210,12 +210,6 @@ By default, progress bars are enabled:
|
|
|
210
210
|
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_bool=True, )
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
-
You can also pass extra keyword arguments to `tqdm` through `pbar_kwargs`:
|
|
214
|
-
|
|
215
|
-
```python
|
|
216
|
-
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_kwargs={"leave": False}, )
|
|
217
|
-
```
|
|
218
|
-
|
|
219
213
|
Verbose logging can be enabled with:
|
|
220
214
|
|
|
221
215
|
```python
|
|
@@ -279,9 +273,17 @@ param_values = [[[1, 2], [10, 20]], [[100, 200, 300]], ]
|
|
|
279
273
|
Before opening a PR or publishing a release, run:
|
|
280
274
|
|
|
281
275
|
```bash
|
|
282
|
-
pytest
|
|
276
|
+
pytest -m "not mpi"
|
|
283
277
|
ruff check .
|
|
284
|
-
mypy
|
|
278
|
+
mypy
|
|
285
279
|
python -m build
|
|
286
280
|
python -m twine check dist/*
|
|
287
281
|
```
|
|
282
|
+
|
|
283
|
+
For MPI numerical-equivalence checks (Linux/OpenMPI, 4 ranks):
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
uv sync --extra mpi
|
|
287
|
+
mpirun -n 4 uv run pytest -m mpi -o addopts="-q -m mpi"
|
|
288
|
+
```
|
|
289
|
+
|
|
@@ -178,12 +178,6 @@ By default, progress bars are enabled:
|
|
|
178
178
|
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_bool=True, )
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
You can also pass extra keyword arguments to `tqdm` through `pbar_kwargs`:
|
|
182
|
-
|
|
183
|
-
```python
|
|
184
|
-
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_kwargs={"leave": False}, )
|
|
185
|
-
```
|
|
186
|
-
|
|
187
181
|
Verbose logging can be enabled with:
|
|
188
182
|
|
|
189
183
|
```python
|
|
@@ -247,9 +241,17 @@ param_values = [[[1, 2], [10, 20]], [[100, 200, 300]], ]
|
|
|
247
241
|
Before opening a PR or publishing a release, run:
|
|
248
242
|
|
|
249
243
|
```bash
|
|
250
|
-
pytest
|
|
244
|
+
pytest -m "not mpi"
|
|
251
245
|
ruff check .
|
|
252
|
-
mypy
|
|
246
|
+
mypy
|
|
253
247
|
python -m build
|
|
254
248
|
python -m twine check dist/*
|
|
255
249
|
```
|
|
250
|
+
|
|
251
|
+
For MPI numerical-equivalence checks (Linux/OpenMPI, 4 ranks):
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
uv sync --extra mpi
|
|
255
|
+
mpirun -n 4 uv run pytest -m mpi -o addopts="-q -m mpi"
|
|
256
|
+
```
|
|
257
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from parameterrun import parameterrun
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def f(x, y, scale=1):
|
|
9
|
+
sleep(0.1) # Simulate a long computation
|
|
10
|
+
return scale * (x + y)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
xs = np.linspace(0, 10, 1000)
|
|
14
|
+
ys = np.linspace(0, 20, 500)
|
|
15
|
+
|
|
16
|
+
result = parameterrun(f, param_names=[["x"], ["y"]], param_values=[[xs], [ys]], n_workers=1, scale=2, )
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from mpi4py import MPI
|
|
3
|
+
|
|
4
|
+
from parameterrun import parameterrun
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def f(x, y, scale=1):
|
|
8
|
+
return scale * (x + y)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
comm = MPI.COMM_WORLD
|
|
12
|
+
rank = comm.Get_rank()
|
|
13
|
+
size = comm.Get_size()
|
|
14
|
+
|
|
15
|
+
xs = np.linspace(0, 10, 100)
|
|
16
|
+
ys = np.linspace(0, 20, 50)
|
|
17
|
+
|
|
18
|
+
result = parameterrun(f, param_names=[["x"], ["y"]], param_values=[[xs], [ys]], scale=2, backend='mpi')
|
|
19
|
+
|
|
20
|
+
if rank == 0:
|
|
21
|
+
print(np.all(result == 2 * (xs[:, None] + ys[None, :])))
|
|
@@ -64,6 +64,9 @@ packages = ["src/parameterrun"]
|
|
|
64
64
|
[tool.pytest.ini_options]
|
|
65
65
|
testpaths = ["tests"]
|
|
66
66
|
addopts = "-q --cov=parameterrun --cov-report=term-missing --cov-fail-under=70"
|
|
67
|
+
markers = [
|
|
68
|
+
"mpi: tests that require mpi4py and an MPI launcher",
|
|
69
|
+
]
|
|
67
70
|
|
|
68
71
|
[tool.ruff]
|
|
69
72
|
target-version = "py39"
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from itertools import product
|
|
7
|
+
from math import prod
|
|
7
8
|
from time import time
|
|
8
9
|
from typing import Any, Callable, List, Optional, Tuple, Union, cast
|
|
9
10
|
|
|
@@ -58,19 +59,18 @@ def tqdm_joblib(tqdm_object: tqdm):
|
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
|
|
61
|
-
indices_iterate:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
raise ValueError('No parameter combinations generated. Please provide at least one value per group.')
|
|
62
|
+
indices_iterate: Iterable, desc, n_workers: Optional[int] = -1, pbar_bool: bool = True,
|
|
63
|
+
pbar_leave: Optional[bool] = True, **fun_kwargs) -> Tuple[List[List], int]:
|
|
64
|
+
n_values = [len(group[0]) for group in param_values]
|
|
65
|
+
n_total = prod(n_values) # Compute the total number of iterations
|
|
66
66
|
|
|
67
67
|
result = []
|
|
68
68
|
if n_workers == 1: # If only one worker, do not use joblib
|
|
69
|
-
pbar = tqdm(indices_iterate, desc=desc, leave=
|
|
69
|
+
pbar = tqdm(indices_iterate, total=n_total, desc=desc, leave=pbar_leave, disable=not pbar_bool)
|
|
70
70
|
for index in pbar:
|
|
71
71
|
result.append(fun(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}))
|
|
72
72
|
else:
|
|
73
|
-
with tqdm_joblib(tqdm(indices_iterate, desc=desc, leave=
|
|
73
|
+
with tqdm_joblib(tqdm(indices_iterate, total=n_total, desc=desc, leave=pbar_leave, disable=not pbar_bool)) as _:
|
|
74
74
|
result = Parallel(n_jobs=n_workers)(
|
|
75
75
|
delayed(fun)(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}) for index in
|
|
76
76
|
indices_iterate)
|
|
@@ -86,9 +86,19 @@ def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]],
|
|
|
86
86
|
return result_temp, n_output
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
def _product_element_from_index(lists: List[Any], k: int) -> List:
|
|
90
|
+
lengths = [len(L) for L in lists]
|
|
91
|
+
|
|
92
|
+
indices = [0] * len(lists)
|
|
93
|
+
for i in range(len(lists) - 1, -1, -1):
|
|
94
|
+
k, indices[i] = divmod(k, lengths[i])
|
|
95
|
+
|
|
96
|
+
return list(L[j] for L, j in zip(lists, indices))
|
|
97
|
+
|
|
98
|
+
|
|
89
99
|
def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
desc: Optional[str], pbar_bool: bool = True, verbose: Optional[bool] = False,
|
|
101
|
+
pbar_leave: Optional[bool] = True, seed=None, **fun_kwargs) -> Tuple[
|
|
92
102
|
List[List], int]: # pragma: no cover
|
|
93
103
|
from mpi4py import MPI
|
|
94
104
|
|
|
@@ -96,21 +106,26 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
|
|
|
96
106
|
rank = comm.Get_rank()
|
|
97
107
|
size = comm.Get_size()
|
|
98
108
|
|
|
99
|
-
|
|
109
|
+
n_values = [len(group[0]) for group in param_values]
|
|
110
|
+
indices = [list(range(n_value)) for n_value in n_values]
|
|
111
|
+
n_total = prod(n_values) # Compute the total number of iterations
|
|
100
112
|
|
|
101
113
|
# Shuffle the indices to avoid bias in the computation
|
|
102
114
|
if rank == 0:
|
|
115
|
+
rng = np.random.default_rng(seed=seed)
|
|
103
116
|
total_indices = np.arange(n_total)
|
|
104
|
-
|
|
117
|
+
rng.shuffle(total_indices)
|
|
105
118
|
else:
|
|
106
119
|
total_indices = None
|
|
107
120
|
|
|
108
121
|
total_indices = comm.bcast(total_indices, root=0)
|
|
109
122
|
indices_compute = np.array_split(total_indices, size)[rank]
|
|
110
123
|
|
|
111
|
-
groups_compute =
|
|
124
|
+
groups_compute = []
|
|
125
|
+
for index in indices_compute:
|
|
126
|
+
groups_compute.append(_product_element_from_index(indices, index))
|
|
112
127
|
|
|
113
|
-
pbar = tqdm(groups_compute, desc=desc, leave=
|
|
128
|
+
pbar = tqdm(groups_compute, desc=desc, leave=pbar_leave, disable=not (pbar_bool and rank == 0), file=sys.stdout)
|
|
114
129
|
|
|
115
130
|
n_outputs = 0
|
|
116
131
|
result: List[Any] = []
|
|
@@ -144,7 +159,8 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
|
|
|
144
159
|
# Notify root when each worker is done, and show a progress bar
|
|
145
160
|
if rank == 0:
|
|
146
161
|
progress = 0
|
|
147
|
-
pbar = tqdm(total=size - 1, desc='Waiting for workers', leave=
|
|
162
|
+
pbar = tqdm(total=size - 1, desc='Waiting for workers', leave=pbar_leave, disable=not pbar_bool,
|
|
163
|
+
file=sys.stdout)
|
|
148
164
|
|
|
149
165
|
while progress < size - 1:
|
|
150
166
|
if comm.Iprobe(source=MPI.ANY_SOURCE, tag=0):
|
|
@@ -158,7 +174,7 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
|
|
|
158
174
|
|
|
159
175
|
pbar.close()
|
|
160
176
|
else:
|
|
161
|
-
comm.send(
|
|
177
|
+
comm.send(True, dest=0, tag=0) # Notify root that the work is done
|
|
162
178
|
|
|
163
179
|
n_outputs = comm.bcast(n_outputs, root=0)
|
|
164
180
|
|
|
@@ -174,6 +190,7 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
|
|
|
174
190
|
_log('Results gathered', verbose and rank == 0, hostname=MPI.Get_processor_name())
|
|
175
191
|
|
|
176
192
|
if rank == 0:
|
|
193
|
+
assert result_gathered is not None # For type checker
|
|
177
194
|
result_gathered = [item for sublist in result_gathered for item in sublist] # Flatten the list
|
|
178
195
|
|
|
179
196
|
results_unshuffled = [None] * n_total
|
|
@@ -326,9 +343,10 @@ def _convert_result_to_lists(result: Any) -> Any:
|
|
|
326
343
|
|
|
327
344
|
def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], List[List[str]]],
|
|
328
345
|
param_values: Union[Iterable[Any], Iterable[Iterable[Any]], Iterable[Iterable[Iterable[Any]]]],
|
|
329
|
-
n_workers: Optional[int] = -1, pbar_bool: bool = True,
|
|
330
|
-
|
|
331
|
-
desc: Optional[str] = None, **
|
|
346
|
+
n_workers: Optional[int] = -1, pbar_bool: bool = True, pbar_leave: bool = True,
|
|
347
|
+
verbose: Optional[bool] = False, reshape: bool = True, result_as_array: bool = True,
|
|
348
|
+
backend: Optional[str] = None, desc: Optional[str] = None, **fun_kwargs) -> Union[
|
|
349
|
+
list, np.ndarray, None]: # noqa: E501
|
|
332
350
|
"""
|
|
333
351
|
Run a function with multiple parameters in parallel. To indentify the parameters of interest, user must provide its
|
|
334
352
|
name as is written in the function definition. If more parameters should be pass to the function, they can be
|
|
@@ -361,6 +379,8 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
361
379
|
Number of workers to use in parallel. If -1, the number of workers is the maximum number of cores available.
|
|
362
380
|
pbar_bool : bool, optional (default=True)
|
|
363
381
|
If True, show a progress bar.
|
|
382
|
+
pbar_leave : bool, optional (default=True)
|
|
383
|
+
If True, leave the progress bar after completion.
|
|
364
384
|
backend : str, optional (default=None)
|
|
365
385
|
Backend to use for the parallelization. If None, the backend is chosen automatically between joblib and mpi.
|
|
366
386
|
verbose : bool, optional (default=False)
|
|
@@ -373,7 +393,7 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
373
393
|
lists.
|
|
374
394
|
desc: str, optional (default=None)
|
|
375
395
|
Description of the progress bar. If None, the description denotes the function and parameters names.
|
|
376
|
-
|
|
396
|
+
fun_kwargs :
|
|
377
397
|
Additional parameters to pass to the function.
|
|
378
398
|
|
|
379
399
|
Returns
|
|
@@ -386,16 +406,13 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
386
406
|
time_start = time()
|
|
387
407
|
|
|
388
408
|
formatted_param_names, formatted_param_values = _format_input(param_names, param_values)
|
|
389
|
-
_validate_function_arguments(fun, formatted_param_names,
|
|
409
|
+
_validate_function_arguments(fun, formatted_param_names, fun_kwargs)
|
|
390
410
|
|
|
391
411
|
# Create the list of dictionaries with the parameters in a nested loop
|
|
392
412
|
n_values = [len(group[0]) for group in formatted_param_values]
|
|
393
413
|
if any(n_value == 0 for n_value in n_values):
|
|
394
414
|
raise ValueError('Parameter groups must contain at least one value.')
|
|
395
415
|
|
|
396
|
-
indices = [list(range(n_value)) for n_value in n_values]
|
|
397
|
-
indices_iterate = product(*indices)
|
|
398
|
-
|
|
399
416
|
# Choose the backed
|
|
400
417
|
if backend is not None:
|
|
401
418
|
available_backends = ['joblib', 'mpi']
|
|
@@ -437,16 +454,18 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
437
454
|
if backend == 'joblib':
|
|
438
455
|
_log('Running under joblib', verbose)
|
|
439
456
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
457
|
+
indices = [list(range(n_value)) for n_value in n_values]
|
|
458
|
+
indices_iterate = product(*indices)
|
|
459
|
+
result, n_outputs = _parameterrun_joblib(fun, formatted_param_names, formatted_param_values, indices_iterate,
|
|
460
|
+
desc, n_workers=n_workers, pbar_bool=pbar_bool, pbar_leave=pbar_leave,
|
|
461
|
+
**fun_kwargs)
|
|
443
462
|
|
|
444
463
|
elif backend == 'mpi':
|
|
445
464
|
_log(f'Running under mpi with {size} workers', verbose and rank == 0,
|
|
446
465
|
hostname=mpi_module.Get_processor_name() if mpi_module is not None else None, )
|
|
447
466
|
|
|
448
|
-
result, n_outputs = _parameterrun_mpi(fun, formatted_param_names, formatted_param_values,
|
|
449
|
-
|
|
467
|
+
result, n_outputs = _parameterrun_mpi(fun, formatted_param_names, formatted_param_values, desc,
|
|
468
|
+
pbar_bool=pbar_bool, pbar_leave=pbar_leave, verbose=verbose, **fun_kwargs)
|
|
450
469
|
|
|
451
470
|
else:
|
|
452
471
|
raise ValueError('Unknown backend')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import parameterrun.parallel_utils as pu
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.mpi
|
|
8
|
+
def test_parameterrun_mpi_numerical_equivalence_and_root_return():
|
|
9
|
+
MPI = pytest.importorskip("mpi4py.MPI")
|
|
10
|
+
comm = MPI.COMM_WORLD
|
|
11
|
+
|
|
12
|
+
if comm.Get_size() != 4:
|
|
13
|
+
pytest.skip("Run this test with mpirun -n 4")
|
|
14
|
+
|
|
15
|
+
xs = np.array([0.0, 1.0, 2.0])
|
|
16
|
+
ys = np.array([10.0, 20.0])
|
|
17
|
+
|
|
18
|
+
def f(x, y, scale=1.0):
|
|
19
|
+
return scale * (x + y)
|
|
20
|
+
|
|
21
|
+
result = pu.parameterrun(f, param_names=[["x"], ["y"]], param_values=[[xs], [ys]], scale=2.0, backend="mpi",
|
|
22
|
+
pbar_bool=False, )
|
|
23
|
+
|
|
24
|
+
expected = 2.0 * (xs[:, None] + ys[None, :])
|
|
25
|
+
|
|
26
|
+
if comm.Get_rank() == 0:
|
|
27
|
+
assert isinstance(result, np.ndarray)
|
|
28
|
+
np.testing.assert_allclose(result, expected)
|
|
29
|
+
else:
|
|
30
|
+
assert result is None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Test progress bar format for joblib backend."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from parameterrun import parameterrun
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def capture_pbar_output(n_workers: int, n_x: int = 10, n_y: int = 50) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Run parameterrun and capture progress bar output.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
n_workers : int
|
|
16
|
+
Number of workers to use
|
|
17
|
+
n_x : int
|
|
18
|
+
Number of x values
|
|
19
|
+
n_y : int
|
|
20
|
+
Number of y values
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
str
|
|
25
|
+
Captured progress bar output
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def dummy_func(x, y):
|
|
29
|
+
"""Simple dummy function with two grouped parameters."""
|
|
30
|
+
return x + y
|
|
31
|
+
|
|
32
|
+
xs = np.linspace(0, 10, n_x)
|
|
33
|
+
ys = np.linspace(0, 20, n_y)
|
|
34
|
+
|
|
35
|
+
# Use StringIO to capture stderr (where tqdm writes progress bars)
|
|
36
|
+
import sys
|
|
37
|
+
from io import StringIO
|
|
38
|
+
old_stderr = sys.stderr
|
|
39
|
+
sys.stderr = StringIO()
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
parameterrun(dummy_func, param_names=[["x"], ["y"]], param_values=[[xs], [ys]], n_workers=n_workers,
|
|
43
|
+
pbar_bool=True, backend="joblib", pbar_leave=True, # Keep the progress bar output for inspection
|
|
44
|
+
)
|
|
45
|
+
output = sys.stderr.getvalue()
|
|
46
|
+
finally:
|
|
47
|
+
sys.stderr = old_stderr
|
|
48
|
+
|
|
49
|
+
return output
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_pbar_format_single_worker():
|
|
53
|
+
"""Test progress bar format with n_workers=1."""
|
|
54
|
+
output = capture_pbar_output(n_workers=1, n_x=10, n_y=50)
|
|
55
|
+
|
|
56
|
+
# The format should contain:
|
|
57
|
+
# - Total count with progress: X/500 (e.g., "350/500" meaning 350/500 iterations)
|
|
58
|
+
# - Elapsed time: [MM:SS or HH:MM:SS]
|
|
59
|
+
# - Remaining time: <MM:SS or <HH:MM:SS
|
|
60
|
+
# - Rate: XX.XXit/s or YY.YYit/s
|
|
61
|
+
|
|
62
|
+
# Expected format: f: [['x'], ['y']]: 0%| | 350/500 [00:17<7:05:24, 19.58it/s]
|
|
63
|
+
|
|
64
|
+
# Check for presence of total count pattern (e.g., "500/500" at completion)
|
|
65
|
+
print(output)
|
|
66
|
+
assert re.search(r'\d+/500', output), f"Progress bar missing total count pattern. Output: {output}"
|
|
67
|
+
|
|
68
|
+
# Check for remaining time pattern (e.g., "<MM:SS" or "<HH:MM:SS")
|
|
69
|
+
assert re.search(r'<\d+:\d{2}:\d{2}|<\d+:\d{2}',
|
|
70
|
+
output), f"Progress bar missing remaining time pattern. Output: {output}"
|
|
71
|
+
|
|
72
|
+
# Check for rate pattern (XX.XXit/s or YY.YYit/s)
|
|
73
|
+
assert re.search(r'\d+\.\d+it/s|\d+\.\d+s/it', output), f"Progress bar missing rate pattern. Output: {output}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_pbar_format_multiple_workers():
|
|
77
|
+
"""Test progress bar format with n_workers=2."""
|
|
78
|
+
output = capture_pbar_output(n_workers=2, n_x=10, n_y=50)
|
|
79
|
+
|
|
80
|
+
# The format should be the same for multi-worker case
|
|
81
|
+
# Expected format: f: [['x'], ['y']]: 0%| | 350/500 [00:17<7:05:24, 19.58it/s]
|
|
82
|
+
|
|
83
|
+
# Check for presence of total count pattern (e.g., "500/500" at completion)
|
|
84
|
+
assert re.search(r'\d+/500', output), f"Progress bar missing total count pattern. Output: {output}"
|
|
85
|
+
|
|
86
|
+
# Check for remaining time pattern (e.g., "<MM:SS" or "<HH:MM:SS")
|
|
87
|
+
assert re.search(r'<\d+:\d{2}:\d{2}|<\d+:\d{2}',
|
|
88
|
+
output), f"Progress bar missing remaining time pattern. Output: {output}"
|
|
89
|
+
|
|
90
|
+
# Check for rate pattern (XX.XXit/s or YY.YYit/s)
|
|
91
|
+
assert re.search(r'\d+\.\d+it/s|\d+\.\d+s/it', output), f"Progress bar missing rate pattern. Output: {output}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_pbar_does_not_have_incorrect_format():
|
|
95
|
+
"""Test that progress bar does NOT use the incorrect format."""
|
|
96
|
+
output = capture_pbar_output(n_workers=1, n_x=5, n_y=10)
|
|
97
|
+
|
|
98
|
+
# Incorrect format would look like: "35it [00:03, 9.75it/s]"
|
|
99
|
+
# which has no total count or remaining time
|
|
100
|
+
|
|
101
|
+
# Check that we DON'T have the incorrect pattern
|
|
102
|
+
# The incorrect pattern is: digit(s) + "it [" with no "/" before
|
|
103
|
+
incorrect_pattern = r'(?<![/\d])\d+it \['
|
|
104
|
+
|
|
105
|
+
assert not re.search(incorrect_pattern,
|
|
106
|
+
output), f"Progress bar has incorrect format (missing total count). Output: {output}"
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [ main ]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
test:
|
|
12
|
-
name: ${{ matrix.os }} / py${{ matrix.python-version }}
|
|
13
|
-
runs-on: ${{ matrix.os }}
|
|
14
|
-
strategy:
|
|
15
|
-
fail-fast: false
|
|
16
|
-
matrix:
|
|
17
|
-
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
|
18
|
-
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
|
19
|
-
|
|
20
|
-
steps:
|
|
21
|
-
- uses: actions/checkout@v6
|
|
22
|
-
|
|
23
|
-
- uses: actions/setup-python@v5
|
|
24
|
-
with:
|
|
25
|
-
python-version: ${{ matrix.python-version }}
|
|
26
|
-
|
|
27
|
-
- name: Install uv
|
|
28
|
-
run: python -m pip install uv
|
|
29
|
-
|
|
30
|
-
- name: Sync environment
|
|
31
|
-
run: uv sync
|
|
32
|
-
|
|
33
|
-
- name: Tests
|
|
34
|
-
run: uv run pytest
|
|
35
|
-
|
|
36
|
-
- name: Lint
|
|
37
|
-
run: uv run ruff check .
|
|
38
|
-
|
|
39
|
-
- name: Type check
|
|
40
|
-
run: uv run mypy
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|