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.
Files changed (25) hide show
  1. parameterrun-0.4.1/.github/workflows/ci.yml +65 -0
  2. {parameterrun-0.3.0 → parameterrun-0.4.1}/.github/workflows/publish.yml +1 -1
  3. {parameterrun-0.3.0 → parameterrun-0.4.1}/CHANGELOG.md +18 -8
  4. {parameterrun-0.3.0 → parameterrun-0.4.1}/CONTRIBUTING.md +10 -1
  5. {parameterrun-0.3.0 → parameterrun-0.4.1}/PKG-INFO +11 -9
  6. {parameterrun-0.3.0 → parameterrun-0.4.1}/README.md +10 -8
  7. parameterrun-0.4.1/examples/long_example.py +16 -0
  8. parameterrun-0.4.1/examples/minimal_example_mpi.py +21 -0
  9. {parameterrun-0.3.0 → parameterrun-0.4.1}/pyproject.toml +3 -0
  10. {parameterrun-0.3.0 → parameterrun-0.4.1}/src/parameterrun/__init__.py +1 -1
  11. {parameterrun-0.3.0 → parameterrun-0.4.1}/src/parameterrun/parallel_utils.py +47 -28
  12. parameterrun-0.4.1/tests/test_mpi_integration.py +30 -0
  13. parameterrun-0.4.1/tests/test_pbar_format.py +106 -0
  14. parameterrun-0.3.0/.github/workflows/ci.yml +0 -40
  15. {parameterrun-0.3.0 → parameterrun-0.4.1}/.github/workflows/release.yml +0 -0
  16. {parameterrun-0.3.0 → parameterrun-0.4.1}/.gitignore +0 -0
  17. {parameterrun-0.3.0 → parameterrun-0.4.1}/LICENSE +0 -0
  18. {parameterrun-0.3.0 → parameterrun-0.4.1}/examples/minimal_example.py +0 -0
  19. {parameterrun-0.3.0 → parameterrun-0.4.1}/scripts/bump_version.py +0 -0
  20. /parameterrun-0.3.0/tests/test_dephs.py → /parameterrun-0.4.1/tests/test_depths.py +0 -0
  21. {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_format_input.py +0 -0
  22. {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_get_iteration.py +0 -0
  23. {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_joblib.py +0 -0
  24. {parameterrun-0.3.0 → parameterrun-0.4.1}/tests/test_normalized_time.py +0 -0
  25. {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"
@@ -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,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
- ## [0.3.1] - 2026-04-01
7
+ ## [Unreleased]
8
8
 
9
- ### Added
10
- - (Your changes here)
9
+ ## [0.4.1] - 2026-04-21
11
10
 
12
- ## [0.3.0] - 2026-04-01
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
- ## [0.3.0] - 2026-04-01
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
- ## [Unreleased]
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.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 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,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"
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.4.1"
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,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: list, desc, n_workers: Optional[int] = -1, pbar_bool: bool = True,
62
- 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.')
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=leave, disable=not pbar_bool)
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=leave, disable=not pbar_bool)) as _:
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
- groups_iterate: list, desc: Optional[str], pbar_bool: bool = True,
91
- verbose: Optional[bool] = False, leave: Optional[bool] = True, **fun_kwargs) -> Tuple[
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
- n_total = len(groups_iterate)
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
- np.random.shuffle(total_indices)
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 = 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
- pbar = tqdm(groups_compute, desc=desc, leave=leave, disable=not (pbar_bool and rank == 0), file=sys.stdout)
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=leave, disable=not pbar_bool, file=sys.stdout)
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(1, dest=0, tag=0) # Notify root that the work is done
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, verbose: Optional[bool] = False,
330
- reshape: Optional[bool] = True, result_as_array: Optional[bool] = True, backend: Optional[str] = None,
331
- desc: Optional[str] = None, **kwargs) -> Union[list, np.ndarray, None]: # noqa: E501
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
- kwargs :
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, kwargs)
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
- 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)
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, list(indices_iterate),
449
- desc, pbar_bool=pbar_bool, verbose=verbose, **kwargs)
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