parameterrun 0.3.0__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.
Files changed (23) hide show
  1. parameterrun-0.4.0/.github/workflows/ci.yml +65 -0
  2. {parameterrun-0.3.0 → parameterrun-0.4.0}/.github/workflows/publish.yml +1 -1
  3. {parameterrun-0.3.0 → parameterrun-0.4.0}/CHANGELOG.md +9 -10
  4. {parameterrun-0.3.0 → parameterrun-0.4.0}/CONTRIBUTING.md +10 -1
  5. {parameterrun-0.3.0 → parameterrun-0.4.0}/PKG-INFO +11 -9
  6. {parameterrun-0.3.0 → parameterrun-0.4.0}/README.md +10 -8
  7. parameterrun-0.4.0/examples/minimal_example_mpi.py +21 -0
  8. {parameterrun-0.3.0 → parameterrun-0.4.0}/pyproject.toml +3 -0
  9. {parameterrun-0.3.0 → parameterrun-0.4.0}/src/parameterrun/__init__.py +1 -1
  10. {parameterrun-0.3.0 → parameterrun-0.4.0}/src/parameterrun/parallel_utils.py +33 -20
  11. parameterrun-0.4.0/tests/test_mpi_integration.py +30 -0
  12. parameterrun-0.3.0/.github/workflows/ci.yml +0 -40
  13. {parameterrun-0.3.0 → parameterrun-0.4.0}/.github/workflows/release.yml +0 -0
  14. {parameterrun-0.3.0 → parameterrun-0.4.0}/.gitignore +0 -0
  15. {parameterrun-0.3.0 → parameterrun-0.4.0}/LICENSE +0 -0
  16. {parameterrun-0.3.0 → parameterrun-0.4.0}/examples/minimal_example.py +0 -0
  17. {parameterrun-0.3.0 → parameterrun-0.4.0}/scripts/bump_version.py +0 -0
  18. /parameterrun-0.3.0/tests/test_dephs.py → /parameterrun-0.4.0/tests/test_depths.py +0 -0
  19. {parameterrun-0.3.0 → parameterrun-0.4.0}/tests/test_format_input.py +0 -0
  20. {parameterrun-0.3.0 → parameterrun-0.4.0}/tests/test_get_iteration.py +0 -0
  21. {parameterrun-0.3.0 → parameterrun-0.4.0}/tests/test_joblib.py +0 -0
  22. {parameterrun-0.3.0 → parameterrun-0.4.0}/tests/test_normalized_time.py +0 -0
  23. {parameterrun-0.3.0 → parameterrun-0.4.0}/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"
@@ -19,7 +19,7 @@ jobs:
19
19
  with:
20
20
  ref: ${{ github.event.workflow_run.head_sha }}
21
21
 
22
- - uses: actions/setup-python@v5
22
+ - uses: actions/setup-python@v6
23
23
  with:
24
24
  python-version: "3.9"
25
25
 
@@ -4,22 +4,21 @@ 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
- ## [0.3.1] - 2026-04-01
8
-
9
- ### Added
10
- - (Your changes here)
7
+ ## [Unreleased]
11
8
 
12
- ## [0.3.0] - 2026-04-01
9
+ ## [0.4.0] - 2026-04-21
13
10
 
14
11
  ### Added
15
- - (Your changes here)
16
12
 
17
- ## [0.3.0] - 2026-04-01
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.
18
15
 
19
- ### Added
20
- - (Your changes here)
16
+ ### Changed
21
17
 
22
- ## [Unreleased]
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.
23
22
 
24
23
  ## [0.3.0] - 2026-04-01
25
24
 
@@ -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.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
@@ -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 src/parameterrun
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 src/parameterrun
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,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,4 +1,4 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.4.0"
2
2
 
3
3
  from .parallel_utils import parameterrun
4
4
 
@@ -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,12 +59,8 @@ 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: list, desc, n_workers: Optional[int] = -1, pbar_bool: bool = True,
62
+ indices_iterate: Iterable, desc, n_workers: Optional[int] = -1, pbar_bool: bool = True,
62
63
  leave: Optional[bool] = True, **fun_kwargs) -> Tuple[List[List], int]:
63
- n_total = len(indices_iterate)
64
- if n_total == 0:
65
- raise ValueError('No parameter combinations generated. Please provide at least one value per group.')
66
-
67
64
  result = []
68
65
  if n_workers == 1: # If only one worker, do not use joblib
69
66
  pbar = tqdm(indices_iterate, desc=desc, leave=leave, disable=not pbar_bool)
@@ -75,6 +72,8 @@ def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]],
75
72
  delayed(fun)(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}) for index in
76
73
  indices_iterate)
77
74
 
75
+ n_total = len(result)
76
+
78
77
  n_output = len(result[0]) if isinstance(result[0], tuple) else 1
79
78
  if n_output == 1:
80
79
  result = [(result_i,) for result_i in result]
@@ -86,9 +85,19 @@ def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]],
86
85
  return result_temp, n_output
87
86
 
88
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
+
89
98
  def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
90
- groups_iterate: list, desc: Optional[str], pbar_bool: bool = True,
91
- verbose: Optional[bool] = False, 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[
92
101
  List[List], int]: # pragma: no cover
93
102
  from mpi4py import MPI
94
103
 
@@ -96,19 +105,25 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
96
105
  rank = comm.Get_rank()
97
106
  size = comm.Get_size()
98
107
 
99
- n_total = len(groups_iterate)
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
100
111
 
101
112
  # Shuffle the indices to avoid bias in the computation
102
113
  if rank == 0:
114
+ rng = np.random.default_rng(seed=seed)
103
115
  total_indices = np.arange(n_total)
104
- np.random.shuffle(total_indices)
116
+ rng.shuffle(total_indices)
105
117
  else:
106
118
  total_indices = None
107
119
 
108
120
  total_indices = comm.bcast(total_indices, root=0)
109
121
  indices_compute = np.array_split(total_indices, size)[rank]
110
122
 
111
- 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))
112
127
 
113
128
  pbar = tqdm(groups_compute, desc=desc, leave=leave, disable=not (pbar_bool and rank == 0), file=sys.stdout)
114
129
 
@@ -158,7 +173,7 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
158
173
 
159
174
  pbar.close()
160
175
  else:
161
- comm.send(1, dest=0, tag=0) # Notify root that the work is done
176
+ comm.send(True, dest=0, tag=0) # Notify root that the work is done
162
177
 
163
178
  n_outputs = comm.bcast(n_outputs, root=0)
164
179
 
@@ -327,7 +342,7 @@ def _convert_result_to_lists(result: Any) -> Any:
327
342
  def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], List[List[str]]],
328
343
  param_values: Union[Iterable[Any], Iterable[Iterable[Any]], Iterable[Iterable[Iterable[Any]]]],
329
344
  n_workers: Optional[int] = -1, pbar_bool: bool = True, verbose: Optional[bool] = False,
330
- reshape: Optional[bool] = True, result_as_array: Optional[bool] = True, backend: Optional[str] = None,
345
+ reshape: bool = True, result_as_array: bool = True, backend: Optional[str] = None,
331
346
  desc: Optional[str] = None, **kwargs) -> Union[list, np.ndarray, None]: # noqa: E501
332
347
  """
333
348
  Run a function with multiple parameters in parallel. To indentify the parameters of interest, user must provide its
@@ -393,9 +408,6 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
393
408
  if any(n_value == 0 for n_value in n_values):
394
409
  raise ValueError('Parameter groups must contain at least one value.')
395
410
 
396
- indices = [list(range(n_value)) for n_value in n_values]
397
- indices_iterate = product(*indices)
398
-
399
411
  # Choose the backed
400
412
  if backend is not None:
401
413
  available_backends = ['joblib', 'mpi']
@@ -437,16 +449,17 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
437
449
  if backend == 'joblib':
438
450
  _log('Running under joblib', verbose)
439
451
 
440
- result, n_outputs = _parameterrun_joblib(fun, formatted_param_names, formatted_param_values,
441
- list(indices_iterate), desc, n_workers=n_workers, pbar_bool=pbar_bool,
442
- **kwargs)
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)
443
456
 
444
457
  elif backend == 'mpi':
445
458
  _log(f'Running under mpi with {size} workers', verbose and rank == 0,
446
459
  hostname=mpi_module.Get_processor_name() if mpi_module is not None else None, )
447
460
 
448
- result, n_outputs = _parameterrun_mpi(fun, formatted_param_names, formatted_param_values, list(indices_iterate),
449
- desc, pbar_bool=pbar_bool, verbose=verbose, **kwargs)
461
+ result, n_outputs = _parameterrun_mpi(fun, formatted_param_names, formatted_param_values, desc,
462
+ pbar_bool=pbar_bool, verbose=verbose, **kwargs)
450
463
 
451
464
  else:
452
465
  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
@@ -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