parameterrun 0.2.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parameterrun-0.4.0/.github/workflows/ci.yml +65 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/.github/workflows/publish.yml +1 -1
- {parameterrun-0.2.1 → parameterrun-0.4.0}/CHANGELOG.md +24 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/CONTRIBUTING.md +11 -2
- {parameterrun-0.2.1 → parameterrun-0.4.0}/PKG-INFO +25 -13
- {parameterrun-0.2.1 → parameterrun-0.4.0}/README.md +24 -12
- {parameterrun-0.2.1 → parameterrun-0.4.0}/examples/minimal_example.py +0 -1
- parameterrun-0.4.0/examples/minimal_example_mpi.py +21 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/pyproject.toml +3 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/src/parameterrun/__init__.py +1 -1
- {parameterrun-0.2.1 → parameterrun-0.4.0}/src/parameterrun/parallel_utils.py +105 -61
- parameterrun-0.4.0/tests/test_depths.py +36 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/tests/test_format_input.py +22 -3
- parameterrun-0.4.0/tests/test_joblib.py +14 -0
- parameterrun-0.4.0/tests/test_mpi_integration.py +30 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/tests/test_parameter_run.py +22 -0
- parameterrun-0.2.1/.github/workflows/ci.yml +0 -48
- {parameterrun-0.2.1 → parameterrun-0.4.0}/.github/workflows/release.yml +0 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/.gitignore +0 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/LICENSE +0 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/scripts/bump_version.py +0 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/tests/test_get_iteration.py +0 -0
- {parameterrun-0.2.1 → parameterrun-0.4.0}/tests/test_normalized_time.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"
|
|
@@ -6,6 +6,30 @@ The format is based on Keep a Changelog and the project follows Semantic Version
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-04-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `tests/test_mpi_integration.py` with MPI numerical-equivalence coverage for `parameterrun(..., backend="mpi")`.
|
|
14
|
+
- Added a dedicated Linux PR CI job that runs MPI tests with 4 ranks.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Updated default test commands in docs and CI to run non-MPI tests with `-m "not mpi"`.
|
|
19
|
+
- Registered a pytest `mpi` marker in `pyproject.toml` for explicit MPI test selection.
|
|
20
|
+
- Update how the indices to compute are shared among workers. Previously, all the indices where computed by all workers,
|
|
21
|
+
which caused a significan overhead in the memory usage. Now, the indices are computed on the fly.
|
|
22
|
+
|
|
23
|
+
## [0.3.0] - 2026-04-01
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Added `result_as_array` to `parameterrun(...)`: outputs are numpy arrays by default and can be converted to Python
|
|
28
|
+
lists with `result_as_array=False`.
|
|
29
|
+
- Removed the `pbar_kwargs` optional argument from `parameterrun(...)`, as it was unused.
|
|
30
|
+
- Added more tests for the depth of the parameter grid.
|
|
31
|
+
- Added the possibility to add parameter values as numpy arrays, which are converted to lists internally.
|
|
32
|
+
|
|
9
33
|
## [0.2.1] - 2026-04-01
|
|
10
34
|
|
|
11
35
|
### Added
|
|
@@ -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
|
-
uv run mypy
|
|
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.0
|
|
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
|
|
@@ -146,9 +146,16 @@ print(mean_like)
|
|
|
146
146
|
# array([2, 3, 4])
|
|
147
147
|
|
|
148
148
|
print(square_like) # array([1, 4, 9])
|
|
149
|
+
|
|
150
|
+
# Ask for plain Python lists instead of numpy arrays
|
|
151
|
+
mean_like, square_like = parameterrun(stats, param_names="x", param_values=[1, 2, 3], n_workers=1,
|
|
152
|
+
result_as_array=False, )
|
|
153
|
+
|
|
154
|
+
print(mean_like) # [2, 3, 4]
|
|
149
155
|
```
|
|
150
156
|
|
|
151
157
|
If the function returns a single object, the result is returned as a single array when reshaping is possible.
|
|
158
|
+
Set `result_as_array=False` to convert outputs to Python lists.
|
|
152
159
|
|
|
153
160
|
For multi-dimensional sweeps, reshaping is automatic when `reshape=True` (default): the first dimensions always match
|
|
154
161
|
the parameter grid shape.
|
|
@@ -203,12 +210,6 @@ By default, progress bars are enabled:
|
|
|
203
210
|
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_bool=True, )
|
|
204
211
|
```
|
|
205
212
|
|
|
206
|
-
You can also pass extra keyword arguments to `tqdm` through `pbar_kwargs`:
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_kwargs={"leave": False}, )
|
|
210
|
-
```
|
|
211
|
-
|
|
212
213
|
Verbose logging can be enabled with:
|
|
213
214
|
|
|
214
215
|
```python
|
|
@@ -223,18 +224,18 @@ The progress-bar description is generated automatically from the function name a
|
|
|
223
224
|
`parameterrun`
|
|
224
225
|
|
|
225
226
|
```python
|
|
226
|
-
parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbose=False,
|
|
227
|
-
reshape=True, backend=None, desc=None, **kwargs, )
|
|
227
|
+
parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbose=False,
|
|
228
|
+
reshape=True, result_as_array=True, backend=None, desc=None, **kwargs, )
|
|
228
229
|
```
|
|
229
230
|
|
|
230
231
|
* `fun`: function to evaluate
|
|
231
232
|
* `param_names`: parameter name or grouped parameter names
|
|
232
|
-
* `param_values`: values corresponding to `param_names`
|
|
233
|
+
* `param_values`: values corresponding to `param_names` (any iterable, e.g. list, tuple, range, numpy array)
|
|
233
234
|
* `n_workers`: number of workers for `joblib`
|
|
234
235
|
* `pbar_bool`: enable or disable progress bars
|
|
235
236
|
* `verbose`: print backend and timing information
|
|
236
|
-
* `pbar_kwargs`: additional keyword arguments passed to tqdm
|
|
237
237
|
* `reshape`: reshape results into arrays when possible
|
|
238
|
+
* `result_as_array`: return numpy arrays by default; if `False`, convert outputs to Python lists
|
|
238
239
|
* `backend`: `"joblib"`, `"mpi"`, or `None`
|
|
239
240
|
* `desc`: custom progress-bar label
|
|
240
241
|
* `**kwargs`: extra fixed keyword arguments forwarded to `fun`
|
|
@@ -243,6 +244,9 @@ parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbo
|
|
|
243
244
|
|
|
244
245
|
`parameterrun` accepts three input styles.
|
|
245
246
|
|
|
247
|
+
At every style, value containers can be any iterable (for example lists, tuples, ranges, generators, or numpy
|
|
248
|
+
arrays).
|
|
249
|
+
|
|
246
250
|
### A. Single parameter
|
|
247
251
|
|
|
248
252
|
```python
|
|
@@ -269,9 +273,17 @@ param_values = [[[1, 2], [10, 20]], [[100, 200, 300]], ]
|
|
|
269
273
|
Before opening a PR or publishing a release, run:
|
|
270
274
|
|
|
271
275
|
```bash
|
|
272
|
-
pytest
|
|
276
|
+
pytest -m "not mpi"
|
|
273
277
|
ruff check .
|
|
274
|
-
mypy
|
|
278
|
+
mypy
|
|
275
279
|
python -m build
|
|
276
280
|
python -m twine check dist/*
|
|
277
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
|
+
|
|
@@ -114,9 +114,16 @@ print(mean_like)
|
|
|
114
114
|
# array([2, 3, 4])
|
|
115
115
|
|
|
116
116
|
print(square_like) # array([1, 4, 9])
|
|
117
|
+
|
|
118
|
+
# Ask for plain Python lists instead of numpy arrays
|
|
119
|
+
mean_like, square_like = parameterrun(stats, param_names="x", param_values=[1, 2, 3], n_workers=1,
|
|
120
|
+
result_as_array=False, )
|
|
121
|
+
|
|
122
|
+
print(mean_like) # [2, 3, 4]
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
If the function returns a single object, the result is returned as a single array when reshaping is possible.
|
|
126
|
+
Set `result_as_array=False` to convert outputs to Python lists.
|
|
120
127
|
|
|
121
128
|
For multi-dimensional sweeps, reshaping is automatic when `reshape=True` (default): the first dimensions always match
|
|
122
129
|
the parameter grid shape.
|
|
@@ -171,12 +178,6 @@ By default, progress bars are enabled:
|
|
|
171
178
|
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_bool=True, )
|
|
172
179
|
```
|
|
173
180
|
|
|
174
|
-
You can also pass extra keyword arguments to `tqdm` through `pbar_kwargs`:
|
|
175
|
-
|
|
176
|
-
```python
|
|
177
|
-
result = parameterrun(my_function, param_names="x", param_values=[1, 2, 3], pbar_kwargs={"leave": False}, )
|
|
178
|
-
```
|
|
179
|
-
|
|
180
181
|
Verbose logging can be enabled with:
|
|
181
182
|
|
|
182
183
|
```python
|
|
@@ -191,18 +192,18 @@ The progress-bar description is generated automatically from the function name a
|
|
|
191
192
|
`parameterrun`
|
|
192
193
|
|
|
193
194
|
```python
|
|
194
|
-
parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbose=False,
|
|
195
|
-
reshape=True, backend=None, desc=None, **kwargs, )
|
|
195
|
+
parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbose=False,
|
|
196
|
+
reshape=True, result_as_array=True, backend=None, desc=None, **kwargs, )
|
|
196
197
|
```
|
|
197
198
|
|
|
198
199
|
* `fun`: function to evaluate
|
|
199
200
|
* `param_names`: parameter name or grouped parameter names
|
|
200
|
-
* `param_values`: values corresponding to `param_names`
|
|
201
|
+
* `param_values`: values corresponding to `param_names` (any iterable, e.g. list, tuple, range, numpy array)
|
|
201
202
|
* `n_workers`: number of workers for `joblib`
|
|
202
203
|
* `pbar_bool`: enable or disable progress bars
|
|
203
204
|
* `verbose`: print backend and timing information
|
|
204
|
-
* `pbar_kwargs`: additional keyword arguments passed to tqdm
|
|
205
205
|
* `reshape`: reshape results into arrays when possible
|
|
206
|
+
* `result_as_array`: return numpy arrays by default; if `False`, convert outputs to Python lists
|
|
206
207
|
* `backend`: `"joblib"`, `"mpi"`, or `None`
|
|
207
208
|
* `desc`: custom progress-bar label
|
|
208
209
|
* `**kwargs`: extra fixed keyword arguments forwarded to `fun`
|
|
@@ -211,6 +212,9 @@ parameterrun(fun, param_names, param_values, n_workers=-1, pbar_bool=True, verbo
|
|
|
211
212
|
|
|
212
213
|
`parameterrun` accepts three input styles.
|
|
213
214
|
|
|
215
|
+
At every style, value containers can be any iterable (for example lists, tuples, ranges, generators, or numpy
|
|
216
|
+
arrays).
|
|
217
|
+
|
|
214
218
|
### A. Single parameter
|
|
215
219
|
|
|
216
220
|
```python
|
|
@@ -237,9 +241,17 @@ param_values = [[[1, 2], [10, 20]], [[100, 200, 300]], ]
|
|
|
237
241
|
Before opening a PR or publishing a release, run:
|
|
238
242
|
|
|
239
243
|
```bash
|
|
240
|
-
pytest
|
|
244
|
+
pytest -m "not mpi"
|
|
241
245
|
ruff check .
|
|
242
|
-
mypy
|
|
246
|
+
mypy
|
|
243
247
|
python -m build
|
|
244
248
|
python -m twine check dist/*
|
|
245
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,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"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import inspect
|
|
3
3
|
import sys
|
|
4
|
+
from collections.abc import Iterable
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from itertools import product
|
|
7
|
+
from math import prod
|
|
6
8
|
from time import time
|
|
7
|
-
from typing import Any, Callable, List, Optional, Tuple, Union
|
|
9
|
+
from typing import Any, Callable, List, Optional, Tuple, Union, cast
|
|
8
10
|
|
|
9
11
|
import joblib
|
|
10
12
|
import numpy as np
|
|
@@ -57,14 +59,10 @@ def tqdm_joblib(tqdm_object: tqdm):
|
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
|
|
60
|
-
indices_iterate:
|
|
62
|
+
indices_iterate: Iterable, desc, n_workers: Optional[int] = -1, pbar_bool: bool = True,
|
|
61
63
|
leave: Optional[bool] = True, **fun_kwargs) -> Tuple[List[List], int]:
|
|
62
|
-
|
|
63
|
-
if n_total == 0:
|
|
64
|
-
raise ValueError('No parameter combinations generated. Please provide at least one value per group.')
|
|
65
|
-
|
|
64
|
+
result = []
|
|
66
65
|
if n_workers == 1: # If only one worker, do not use joblib
|
|
67
|
-
result = []
|
|
68
66
|
pbar = tqdm(indices_iterate, desc=desc, leave=leave, disable=not pbar_bool)
|
|
69
67
|
for index in pbar:
|
|
70
68
|
result.append(fun(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}))
|
|
@@ -74,6 +72,8 @@ def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]],
|
|
|
74
72
|
delayed(fun)(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}) for index in
|
|
75
73
|
indices_iterate)
|
|
76
74
|
|
|
75
|
+
n_total = len(result)
|
|
76
|
+
|
|
77
77
|
n_output = len(result[0]) if isinstance(result[0], tuple) else 1
|
|
78
78
|
if n_output == 1:
|
|
79
79
|
result = [(result_i,) for result_i in result]
|
|
@@ -85,28 +85,45 @@ def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]],
|
|
|
85
85
|
return result_temp, n_output
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def _product_element_from_index(lists: List[Any], k: int) -> List:
|
|
89
|
+
lengths = [len(L) for L in lists]
|
|
90
|
+
|
|
91
|
+
indices = [0] * len(lists)
|
|
92
|
+
for i in range(len(lists) - 1, -1, -1):
|
|
93
|
+
k, indices[i] = divmod(k, lengths[i])
|
|
94
|
+
|
|
95
|
+
return list(L[j] for L, j in zip(lists, indices))
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
|
|
89
|
-
|
|
90
|
-
leave: Optional[bool] = True, **fun_kwargs) -> Tuple[
|
|
99
|
+
desc: Optional[str], pbar_bool: bool = True, verbose: Optional[bool] = False,
|
|
100
|
+
leave: Optional[bool] = True, seed=None, **fun_kwargs) -> Tuple[
|
|
101
|
+
List[List], int]: # pragma: no cover
|
|
91
102
|
from mpi4py import MPI
|
|
92
103
|
|
|
93
104
|
comm = MPI.COMM_WORLD
|
|
94
105
|
rank = comm.Get_rank()
|
|
95
106
|
size = comm.Get_size()
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
n_values = [len(group[0]) for group in param_values]
|
|
109
|
+
indices = [list(range(n_value)) for n_value in n_values]
|
|
110
|
+
n_total = prod(n_values) # Compute the total number of iterations
|
|
98
111
|
|
|
99
112
|
# Shuffle the indices to avoid bias in the computation
|
|
100
113
|
if rank == 0:
|
|
114
|
+
rng = np.random.default_rng(seed=seed)
|
|
101
115
|
total_indices = np.arange(n_total)
|
|
102
|
-
|
|
116
|
+
rng.shuffle(total_indices)
|
|
103
117
|
else:
|
|
104
118
|
total_indices = None
|
|
105
119
|
|
|
106
120
|
total_indices = comm.bcast(total_indices, root=0)
|
|
107
121
|
indices_compute = np.array_split(total_indices, size)[rank]
|
|
108
122
|
|
|
109
|
-
groups_compute = np.array(groups_iterate)[indices_compute]
|
|
123
|
+
# groups_compute = np.array(groups_iterate)[indices_compute]
|
|
124
|
+
groups_compute = []
|
|
125
|
+
for index in indices_compute:
|
|
126
|
+
groups_compute.append(_product_element_from_index(indices, index))
|
|
110
127
|
|
|
111
128
|
pbar = tqdm(groups_compute, desc=desc, leave=leave, disable=not (pbar_bool and rank == 0), file=sys.stdout)
|
|
112
129
|
|
|
@@ -156,7 +173,7 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
|
|
|
156
173
|
|
|
157
174
|
pbar.close()
|
|
158
175
|
else:
|
|
159
|
-
comm.send(
|
|
176
|
+
comm.send(True, dest=0, tag=0) # Notify root that the work is done
|
|
160
177
|
|
|
161
178
|
n_outputs = comm.bcast(n_outputs, root=0)
|
|
162
179
|
|
|
@@ -202,6 +219,12 @@ def _flatten_param_names(param_names: List[List[str]]) -> List[str]:
|
|
|
202
219
|
return [name for group in param_names for name in group]
|
|
203
220
|
|
|
204
221
|
|
|
222
|
+
def _to_list_iterable(values: Any, error_message: str) -> List[Any]:
|
|
223
|
+
if isinstance(values, (str, bytes)) or not isinstance(values, Iterable):
|
|
224
|
+
raise ValueError(error_message)
|
|
225
|
+
return list(values)
|
|
226
|
+
|
|
227
|
+
|
|
205
228
|
def _validate_function_arguments(fun: Callable[..., Any], param_names: List[List[str]], fun_kwargs: dict) -> None:
|
|
206
229
|
signature = inspect.signature(fun)
|
|
207
230
|
parameters = signature.parameters
|
|
@@ -231,21 +254,18 @@ def _validate_function_arguments(fun: Callable[..., Any], param_names: List[List
|
|
|
231
254
|
raise ValueError(f'Unknown keyword arguments for function {fun.__name__}: {unknown_kwargs}')
|
|
232
255
|
|
|
233
256
|
|
|
234
|
-
def _format_input(param_names: Union[str,
|
|
257
|
+
def _format_input(param_names: Union[str, List[str], List[List[str]]], param_values: Any) -> Tuple[
|
|
258
|
+
List[List[str]], List[List[List[Any]]]]:
|
|
235
259
|
# Check the depth of the parameters
|
|
236
260
|
|
|
237
261
|
if isinstance(param_names, str):
|
|
238
262
|
if not param_names:
|
|
239
263
|
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')
|
|
242
264
|
depth = 1
|
|
243
265
|
elif isinstance(param_names, list) and len(param_names) > 0 and all(isinstance(name, str) for name in param_names):
|
|
244
266
|
if any(not name for name in param_names):
|
|
245
267
|
raise ValueError('Parameter names must be non-empty strings')
|
|
246
268
|
depth = 2
|
|
247
|
-
if not isinstance(param_values, list):
|
|
248
|
-
raise ValueError('The parameters values must be a list of values')
|
|
249
269
|
elif isinstance(param_names, list) and len(param_names) > 0 and all(
|
|
250
270
|
isinstance(group, list) for group in param_names):
|
|
251
271
|
if any(len(group) == 0 for group in param_names):
|
|
@@ -253,44 +273,53 @@ def _format_input(param_names: Union[str, list], param_values: list) -> Tuple[Li
|
|
|
253
273
|
if any(any(not isinstance(name, str) or not name for name in group) for group in param_names):
|
|
254
274
|
raise ValueError('Parameter names must be non-empty strings')
|
|
255
275
|
depth = 3
|
|
256
|
-
if not isinstance(param_values, list) or len(param_values) == 0 or not isinstance(param_values[0], list):
|
|
257
|
-
raise ValueError('The parameters values must be a list of values')
|
|
258
276
|
else:
|
|
259
277
|
raise ValueError('Unknown input format for the parameters names')
|
|
260
278
|
|
|
279
|
+
normalized_param_names: List[List[str]]
|
|
280
|
+
normalized_param_values: List[List[List[Any]]]
|
|
281
|
+
|
|
261
282
|
# Correct the input if the depth is not equal to 3
|
|
262
283
|
if depth == 1:
|
|
263
284
|
# Only one parameter is provided
|
|
264
|
-
|
|
265
|
-
|
|
285
|
+
values = _to_list_iterable(param_values, 'The parameters values must be an iterable of values')
|
|
286
|
+
normalized_param_names = [[cast(str, param_names)]]
|
|
287
|
+
normalized_param_values = [[values]]
|
|
266
288
|
elif depth == 2:
|
|
267
289
|
# Groups with a single parameters are provided
|
|
268
|
-
|
|
269
|
-
|
|
290
|
+
values = _to_list_iterable(param_values, 'The parameters values must be an iterable of values')
|
|
291
|
+
normalized_param_names = [[param_name_i] for param_name_i in cast(List[str], param_names)]
|
|
292
|
+
normalized_param_values = [
|
|
293
|
+
[_to_list_iterable(param_values_i, 'Each parameter values entry must be an iterable')] for param_values_i in
|
|
294
|
+
values]
|
|
295
|
+
else:
|
|
296
|
+
groups_values = _to_list_iterable(param_values, 'The parameters values must be an iterable of values')
|
|
297
|
+
groups_values = [_to_list_iterable(values_group, 'Each parameter group values must be provided as an iterable')
|
|
298
|
+
for values_group in groups_values]
|
|
299
|
+
normalized_param_names = cast(List[List[str]], param_names)
|
|
300
|
+
normalized_param_values = [
|
|
301
|
+
[_to_list_iterable(parameter_values, 'Each parameter values entry must be an iterable') for parameter_values
|
|
302
|
+
in values_group] for values_group in groups_values]
|
|
270
303
|
|
|
271
304
|
# Check that the number of groups of parameters is the same as the number of groups of values
|
|
272
|
-
if len(
|
|
305
|
+
if len(normalized_param_names) != len(normalized_param_values):
|
|
273
306
|
raise ValueError('The number of groups of parameters must be the same as the number of groups of values')
|
|
274
307
|
|
|
275
|
-
for names_group, values_group in zip(
|
|
276
|
-
if not isinstance(values_group, list):
|
|
277
|
-
raise ValueError('Each parameter group values must be provided as a list')
|
|
308
|
+
for names_group, values_group in zip(normalized_param_names, normalized_param_values):
|
|
278
309
|
if len(names_group) != len(values_group):
|
|
279
310
|
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
311
|
|
|
283
312
|
# Check that all parameters in the same group have the same length
|
|
284
|
-
for group in
|
|
313
|
+
for group in normalized_param_values:
|
|
285
314
|
if len(set([len(param) for param in group])) != 1:
|
|
286
315
|
raise ValueError('All parameters in the same group must have the same length')
|
|
287
316
|
|
|
288
|
-
all_names = _flatten_param_names(
|
|
317
|
+
all_names = _flatten_param_names(normalized_param_names)
|
|
289
318
|
duplicated = sorted({name for name in all_names if all_names.count(name) > 1})
|
|
290
319
|
if duplicated:
|
|
291
320
|
raise ValueError(f'Duplicate parameter names are not allowed: {duplicated}')
|
|
292
321
|
|
|
293
|
-
return
|
|
322
|
+
return normalized_param_names, normalized_param_values
|
|
294
323
|
|
|
295
324
|
|
|
296
325
|
def _reshape_channel_result(channel_values: List[Any], n_values: List[int]) -> np.ndarray:
|
|
@@ -302,11 +331,19 @@ def _reshape_channel_result(channel_values: List[Any], n_values: List[int]) -> n
|
|
|
302
331
|
return array_result.reshape(target_shape)
|
|
303
332
|
|
|
304
333
|
|
|
334
|
+
def _convert_result_to_lists(result: Any) -> Any:
|
|
335
|
+
if isinstance(result, np.ndarray):
|
|
336
|
+
return result.tolist()
|
|
337
|
+
if isinstance(result, list):
|
|
338
|
+
return [_convert_result_to_lists(item) for item in result]
|
|
339
|
+
return result
|
|
340
|
+
|
|
341
|
+
|
|
305
342
|
def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], List[List[str]]],
|
|
306
|
-
param_values: Union[
|
|
307
|
-
|
|
308
|
-
reshape:
|
|
309
|
-
|
|
343
|
+
param_values: Union[Iterable[Any], Iterable[Iterable[Any]], Iterable[Iterable[Iterable[Any]]]],
|
|
344
|
+
n_workers: Optional[int] = -1, pbar_bool: bool = True, verbose: Optional[bool] = False,
|
|
345
|
+
reshape: bool = True, result_as_array: bool = True, backend: Optional[str] = None,
|
|
346
|
+
desc: Optional[str] = None, **kwargs) -> Union[list, np.ndarray, None]: # noqa: E501
|
|
310
347
|
"""
|
|
311
348
|
Run a function with multiple parameters in parallel. To indentify the parameters of interest, user must provide its
|
|
312
349
|
name as is written in the function definition. If more parameters should be pass to the function, they can be
|
|
@@ -332,8 +369,9 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
332
369
|
Function to run in parallel.
|
|
333
370
|
param_names : str or list
|
|
334
371
|
Name of the parameters to run in parallel.
|
|
335
|
-
param_values :
|
|
336
|
-
Values of the parameters to run in parallel.
|
|
372
|
+
param_values : iterable
|
|
373
|
+
Values of the parameters to run in parallel. Lists, tuples, ranges, generators, and numpy arrays are
|
|
374
|
+
accepted as long as they follow one of the supported input shapes.
|
|
337
375
|
n_workers : int, optional (default=-1)
|
|
338
376
|
Number of workers to use in parallel. If -1, the number of workers is the maximum number of cores available.
|
|
339
377
|
pbar_bool : bool, optional (default=True)
|
|
@@ -342,11 +380,12 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
342
380
|
Backend to use for the parallelization. If None, the backend is chosen automatically between joblib and mpi.
|
|
343
381
|
verbose : bool, optional (default=False)
|
|
344
382
|
If True, print information about the parallelization.
|
|
345
|
-
pbar_kwargs : dict, optional (default=None)
|
|
346
|
-
Dictionary with the parameters to pass to the tqdm such as desc and leave.
|
|
347
383
|
reshape : bool, optional (default=True)
|
|
348
384
|
If True, reshape the result as a (hyper)matrix. Sometimes, due to the nature of the output, the reshape is not
|
|
349
385
|
possible, in this case, the result is not reshaped.
|
|
386
|
+
result_as_array : bool, optional (default=True)
|
|
387
|
+
If True, return numpy arrays when possible (default behavior). If False, convert numpy-array outputs to Python
|
|
388
|
+
lists.
|
|
350
389
|
desc: str, optional (default=None)
|
|
351
390
|
Description of the progress bar. If None, the description denotes the function and parameters names.
|
|
352
391
|
kwargs :
|
|
@@ -359,22 +398,16 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
359
398
|
n_valuesN), where n_valuesI is the number of values in the I-th group.
|
|
360
399
|
"""
|
|
361
400
|
|
|
362
|
-
if pbar_kwargs is None:
|
|
363
|
-
pbar_kwargs = {}
|
|
364
|
-
|
|
365
401
|
time_start = time()
|
|
366
402
|
|
|
367
|
-
|
|
368
|
-
_validate_function_arguments(fun,
|
|
403
|
+
formatted_param_names, formatted_param_values = _format_input(param_names, param_values)
|
|
404
|
+
_validate_function_arguments(fun, formatted_param_names, kwargs)
|
|
369
405
|
|
|
370
406
|
# Create the list of dictionaries with the parameters in a nested loop
|
|
371
|
-
n_values = [len(group[0]) for group in
|
|
407
|
+
n_values = [len(group[0]) for group in formatted_param_values]
|
|
372
408
|
if any(n_value == 0 for n_value in n_values):
|
|
373
409
|
raise ValueError('Parameter groups must contain at least one value.')
|
|
374
410
|
|
|
375
|
-
indices = [list(range(n_value)) for n_value in n_values]
|
|
376
|
-
indices_iterate = product(*indices)
|
|
377
|
-
|
|
378
411
|
# Choose the backed
|
|
379
412
|
if backend is not None:
|
|
380
413
|
available_backends = ['joblib', 'mpi']
|
|
@@ -402,43 +435,54 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
|
|
|
402
435
|
backend = 'mpi'
|
|
403
436
|
|
|
404
437
|
if desc is None:
|
|
405
|
-
param_names_pbar = [parameters_names_i for parameters_names_i in
|
|
406
|
-
|
|
438
|
+
param_names_pbar = [parameters_names_i for parameters_names_i in formatted_param_names]
|
|
439
|
+
auto_desc = f'{fun.__name__}: {param_names_pbar}'
|
|
440
|
+
|
|
441
|
+
if len(auto_desc) > 40:
|
|
442
|
+
auto_desc = auto_desc[:40] + '(...)]'
|
|
407
443
|
|
|
408
|
-
|
|
409
|
-
desc = desc[:40] + '(...)'
|
|
444
|
+
desc = auto_desc
|
|
410
445
|
|
|
411
446
|
# Execute the parallel run
|
|
447
|
+
result: List[List[Any]]
|
|
448
|
+
|
|
412
449
|
if backend == 'joblib':
|
|
413
450
|
_log('Running under joblib', verbose)
|
|
414
451
|
|
|
415
|
-
|
|
416
|
-
|
|
452
|
+
indices = [list(range(n_value)) for n_value in n_values]
|
|
453
|
+
indices_iterate = product(*indices)
|
|
454
|
+
result, n_outputs = _parameterrun_joblib(fun, formatted_param_names, formatted_param_values, indices_iterate,
|
|
455
|
+
desc, n_workers=n_workers, pbar_bool=pbar_bool, **kwargs)
|
|
417
456
|
|
|
418
457
|
elif backend == 'mpi':
|
|
419
458
|
_log(f'Running under mpi with {size} workers', verbose and rank == 0,
|
|
420
459
|
hostname=mpi_module.Get_processor_name() if mpi_module is not None else None, )
|
|
421
460
|
|
|
422
|
-
result, n_outputs = _parameterrun_mpi(fun,
|
|
423
|
-
pbar_bool=pbar_bool, verbose=verbose, **
|
|
461
|
+
result, n_outputs = _parameterrun_mpi(fun, formatted_param_names, formatted_param_values, desc,
|
|
462
|
+
pbar_bool=pbar_bool, verbose=verbose, **kwargs)
|
|
424
463
|
|
|
425
464
|
else:
|
|
426
465
|
raise ValueError('Unknown backend')
|
|
427
466
|
|
|
428
467
|
if rank == 0:
|
|
468
|
+
final_result: Union[List[List[Any]], np.ndarray, List[np.ndarray]] = result
|
|
469
|
+
|
|
429
470
|
# Reshape the result to get the (hyper)matrix
|
|
430
471
|
if reshape:
|
|
431
472
|
try:
|
|
432
473
|
if n_outputs == 1:
|
|
433
|
-
|
|
474
|
+
final_result = _reshape_channel_result(result[0], n_values)
|
|
434
475
|
else:
|
|
435
|
-
|
|
476
|
+
final_result = [_reshape_channel_result(result[i], n_values) for i in range(n_outputs)]
|
|
436
477
|
except ValueError:
|
|
437
478
|
print('Could not reshape the result')
|
|
438
479
|
|
|
439
480
|
total_time = time() - time_start
|
|
440
481
|
_log(f'Total time: {_normalized_time(total_time)}', verbose)
|
|
441
482
|
|
|
442
|
-
|
|
483
|
+
if not result_as_array:
|
|
484
|
+
final_result = _convert_result_to_lists(final_result)
|
|
485
|
+
|
|
486
|
+
return final_result
|
|
443
487
|
else:
|
|
444
488
|
return None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from parameterrun import parameterrun
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_depth_1():
|
|
7
|
+
def fun(x):
|
|
8
|
+
return x ** 2
|
|
9
|
+
|
|
10
|
+
xs = np.linspace(0, 10, 5)
|
|
11
|
+
result = parameterrun(fun, 'x', xs, n_workers=1, pbar_bool=False)
|
|
12
|
+
|
|
13
|
+
assert isinstance(result, np.ndarray)
|
|
14
|
+
np.testing.assert_array_equal(result, xs ** 2)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_depth_2():
|
|
18
|
+
def fun(x):
|
|
19
|
+
return x ** 2
|
|
20
|
+
|
|
21
|
+
xs = np.linspace(0, 10, 5)
|
|
22
|
+
result = parameterrun(fun, ['x'], [xs], n_workers=1, pbar_bool=False)
|
|
23
|
+
|
|
24
|
+
assert isinstance(result, np.ndarray)
|
|
25
|
+
np.testing.assert_array_equal(result, xs ** 2)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_depth_3():
|
|
29
|
+
def fun(x):
|
|
30
|
+
return x ** 2
|
|
31
|
+
|
|
32
|
+
xs = np.linspace(0, 10, 5)
|
|
33
|
+
result = parameterrun(fun, [['x']], [[xs]], n_workers=1, pbar_bool=False)
|
|
34
|
+
|
|
35
|
+
assert isinstance(result, np.ndarray)
|
|
36
|
+
np.testing.assert_array_equal(result, xs ** 2)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import numpy as np
|
|
1
2
|
import pytest
|
|
2
3
|
|
|
3
4
|
from parameterrun.parallel_utils import _format_input
|
|
@@ -25,9 +26,27 @@ def test_format_input_rejects_empty_parameter_name():
|
|
|
25
26
|
_format_input("", [1, 2])
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
def test_format_input_accepts_numpy_array_values():
|
|
30
|
+
names, values = _format_input("x", np.array([1, 2, 3]))
|
|
31
|
+
assert names == [["x"]]
|
|
32
|
+
assert values == [[[1, 2, 3]]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_format_input_accepts_iterables_in_multiple_parameter_format():
|
|
36
|
+
names, values = _format_input(["x", "y"], (range(3), (10, 20, 30)))
|
|
37
|
+
assert names == [["x"], ["y"]]
|
|
38
|
+
assert values == [[[0, 1, 2]], [[10, 20, 30]]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_format_input_accepts_tuple_parameter_values_inside_group():
|
|
42
|
+
names, values = _format_input([["x"]], [[(1, 2)]])
|
|
43
|
+
assert names == [["x"]]
|
|
44
|
+
assert values == [[[1, 2]]]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_format_input_rejects_string_values():
|
|
48
|
+
with pytest.raises(ValueError, match="iterable of values"):
|
|
49
|
+
_format_input("x", "123")
|
|
31
50
|
|
|
32
51
|
|
|
33
52
|
def test_format_input_rejects_duplicate_names():
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from parameterrun import parameterrun
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_joblib_2_workers():
|
|
7
|
+
def f(x):
|
|
8
|
+
return x ** 2
|
|
9
|
+
|
|
10
|
+
xs = np.linspace(0, 10, 5)
|
|
11
|
+
result = parameterrun(f, 'x', xs, n_workers=2, pbar_bool=False, backend="joblib")
|
|
12
|
+
|
|
13
|
+
assert isinstance(result, np.ndarray)
|
|
14
|
+
np.testing.assert_array_equal(result, xs ** 2)
|
|
@@ -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
|
|
@@ -15,6 +15,17 @@ def test_parameterrun_single_parameter_returns_1d_array():
|
|
|
15
15
|
np.testing.assert_array_equal(result, np.array([0, 1, 4, 9]))
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def test_parameterrun_single_parameter_can_return_list():
|
|
19
|
+
def square(x):
|
|
20
|
+
return x ** 2
|
|
21
|
+
|
|
22
|
+
result = pu.parameterrun(square, param_names="x", param_values=[0, 1, 2, 3], n_workers=1, pbar_bool=False,
|
|
23
|
+
backend="joblib", result_as_array=False, )
|
|
24
|
+
|
|
25
|
+
assert isinstance(result, list)
|
|
26
|
+
assert result == [0, 1, 4, 9]
|
|
27
|
+
|
|
28
|
+
|
|
18
29
|
def test_parameterrun_grouped_parameters_builds_expected_grid():
|
|
19
30
|
def f(x, y, z, offset=0):
|
|
20
31
|
return x + y + z + offset
|
|
@@ -47,6 +58,17 @@ def test_parameterrun_multiple_outputs_returns_list_of_arrays():
|
|
|
47
58
|
np.testing.assert_array_equal(result[1], np.array([1, 4, 9]))
|
|
48
59
|
|
|
49
60
|
|
|
61
|
+
def test_parameterrun_multiple_outputs_can_return_list_of_lists():
|
|
62
|
+
def f(x):
|
|
63
|
+
return x + 1, x ** 2
|
|
64
|
+
|
|
65
|
+
result = pu.parameterrun(f, param_names="x", param_values=[1, 2, 3], n_workers=1, pbar_bool=False,
|
|
66
|
+
backend="joblib", result_as_array=False, )
|
|
67
|
+
|
|
68
|
+
assert isinstance(result, list)
|
|
69
|
+
assert result == [[2, 3, 4], [1, 4, 9]]
|
|
70
|
+
|
|
71
|
+
|
|
50
72
|
def test_parameterrun_multiple_outputs_keep_grid_shape():
|
|
51
73
|
def f(x, y):
|
|
52
74
|
return x + y, x * y
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
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
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
test:
|
|
17
|
-
name: ${{ matrix.os }} / py${{ matrix.python-version }}
|
|
18
|
-
runs-on: ${{ matrix.os }}
|
|
19
|
-
strategy:
|
|
20
|
-
fail-fast: false
|
|
21
|
-
matrix:
|
|
22
|
-
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
|
23
|
-
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
|
24
|
-
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v6
|
|
27
|
-
|
|
28
|
-
- uses: actions/setup-python@v5
|
|
29
|
-
with:
|
|
30
|
-
python-version: ${{ matrix.python-version }}
|
|
31
|
-
|
|
32
|
-
- name: Install uv
|
|
33
|
-
run: python -m pip install uv
|
|
34
|
-
|
|
35
|
-
- name: Sync environment
|
|
36
|
-
run: uv sync
|
|
37
|
-
|
|
38
|
-
- name: Lint
|
|
39
|
-
run: uv run ruff check .
|
|
40
|
-
|
|
41
|
-
# - name: Format check
|
|
42
|
-
# run: uv run ruff format --check .
|
|
43
|
-
|
|
44
|
-
# - name: Type check
|
|
45
|
-
# run: uv run mypy
|
|
46
|
-
|
|
47
|
-
- name: Tests
|
|
48
|
-
run: uv run pytest
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|