parameterrun 0.4.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 (24) hide show
  1. {parameterrun-0.4.0 → parameterrun-0.4.1}/CHANGELOG.md +11 -0
  2. {parameterrun-0.4.0 → parameterrun-0.4.1}/PKG-INFO +1 -1
  3. parameterrun-0.4.1/examples/long_example.py +16 -0
  4. {parameterrun-0.4.0 → parameterrun-0.4.1}/src/parameterrun/__init__.py +1 -1
  5. {parameterrun-0.4.0 → parameterrun-0.4.1}/src/parameterrun/parallel_utils.py +22 -16
  6. parameterrun-0.4.1/tests/test_pbar_format.py +106 -0
  7. {parameterrun-0.4.0 → parameterrun-0.4.1}/.github/workflows/ci.yml +0 -0
  8. {parameterrun-0.4.0 → parameterrun-0.4.1}/.github/workflows/publish.yml +0 -0
  9. {parameterrun-0.4.0 → parameterrun-0.4.1}/.github/workflows/release.yml +0 -0
  10. {parameterrun-0.4.0 → parameterrun-0.4.1}/.gitignore +0 -0
  11. {parameterrun-0.4.0 → parameterrun-0.4.1}/CONTRIBUTING.md +0 -0
  12. {parameterrun-0.4.0 → parameterrun-0.4.1}/LICENSE +0 -0
  13. {parameterrun-0.4.0 → parameterrun-0.4.1}/README.md +0 -0
  14. {parameterrun-0.4.0 → parameterrun-0.4.1}/examples/minimal_example.py +0 -0
  15. {parameterrun-0.4.0 → parameterrun-0.4.1}/examples/minimal_example_mpi.py +0 -0
  16. {parameterrun-0.4.0 → parameterrun-0.4.1}/pyproject.toml +0 -0
  17. {parameterrun-0.4.0 → parameterrun-0.4.1}/scripts/bump_version.py +0 -0
  18. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_depths.py +0 -0
  19. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_format_input.py +0 -0
  20. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_get_iteration.py +0 -0
  21. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_joblib.py +0 -0
  22. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_mpi_integration.py +0 -0
  23. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_normalized_time.py +0 -0
  24. {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_parameter_run.py +0 -0
@@ -6,6 +6,17 @@ The format is based on Keep a Changelog and the project follows Semantic Version
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.1] - 2026-04-21
10
+
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.
15
+
16
+ ### Added
17
+
18
+ - Added a test for the progress bar display when running with `backend="joblib"`.
19
+
9
20
  ## [0.4.0] - 2026-04-21
10
21
 
11
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: parameterrun
3
- Version: 0.4.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
@@ -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, )
@@ -1,4 +1,4 @@
1
- __version__ = "0.4.0"
1
+ __version__ = "0.4.1"
2
2
 
3
3
  from .parallel_utils import parameterrun
4
4
 
@@ -60,20 +60,21 @@ def tqdm_joblib(tqdm_object: tqdm):
60
60
 
61
61
  def _parameterrun_joblib(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
62
62
  indices_iterate: Iterable, desc, n_workers: Optional[int] = -1, pbar_bool: bool = True,
63
- leave: Optional[bool] = True, **fun_kwargs) -> Tuple[List[List], int]:
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
+
64
67
  result = []
65
68
  if n_workers == 1: # If only one worker, do not use joblib
66
- 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)
67
70
  for index in pbar:
68
71
  result.append(fun(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}))
69
72
  else:
70
- 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 _:
71
74
  result = Parallel(n_jobs=n_workers)(
72
75
  delayed(fun)(**{**_get_iteration(index, param_names, param_values), **fun_kwargs}) for index in
73
76
  indices_iterate)
74
77
 
75
- n_total = len(result)
76
-
77
78
  n_output = len(result[0]) if isinstance(result[0], tuple) else 1
78
79
  if n_output == 1:
79
80
  result = [(result_i,) for result_i in result]
@@ -97,7 +98,7 @@ def _product_element_from_index(lists: List[Any], k: int) -> List:
97
98
 
98
99
  def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], param_values: List[List[List]],
99
100
  desc: Optional[str], pbar_bool: bool = True, verbose: Optional[bool] = False,
100
- leave: Optional[bool] = True, seed=None, **fun_kwargs) -> Tuple[
101
+ pbar_leave: Optional[bool] = True, seed=None, **fun_kwargs) -> Tuple[
101
102
  List[List], int]: # pragma: no cover
102
103
  from mpi4py import MPI
103
104
 
@@ -120,12 +121,11 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
120
121
  total_indices = comm.bcast(total_indices, root=0)
121
122
  indices_compute = np.array_split(total_indices, size)[rank]
122
123
 
123
- # groups_compute = np.array(groups_iterate)[indices_compute]
124
124
  groups_compute = []
125
125
  for index in indices_compute:
126
126
  groups_compute.append(_product_element_from_index(indices, index))
127
127
 
128
- 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)
129
129
 
130
130
  n_outputs = 0
131
131
  result: List[Any] = []
@@ -159,7 +159,8 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
159
159
  # Notify root when each worker is done, and show a progress bar
160
160
  if rank == 0:
161
161
  progress = 0
162
- 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)
163
164
 
164
165
  while progress < size - 1:
165
166
  if comm.Iprobe(source=MPI.ANY_SOURCE, tag=0):
@@ -189,6 +190,7 @@ def _parameterrun_mpi(fun: Callable[..., Any], param_names: List[List[str]], par
189
190
  _log('Results gathered', verbose and rank == 0, hostname=MPI.Get_processor_name())
190
191
 
191
192
  if rank == 0:
193
+ assert result_gathered is not None # For type checker
192
194
  result_gathered = [item for sublist in result_gathered for item in sublist] # Flatten the list
193
195
 
194
196
  results_unshuffled = [None] * n_total
@@ -341,9 +343,10 @@ def _convert_result_to_lists(result: Any) -> Any:
341
343
 
342
344
  def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], List[List[str]]],
343
345
  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
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
347
350
  """
348
351
  Run a function with multiple parameters in parallel. To indentify the parameters of interest, user must provide its
349
352
  name as is written in the function definition. If more parameters should be pass to the function, they can be
@@ -376,6 +379,8 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
376
379
  Number of workers to use in parallel. If -1, the number of workers is the maximum number of cores available.
377
380
  pbar_bool : bool, optional (default=True)
378
381
  If True, show a progress bar.
382
+ pbar_leave : bool, optional (default=True)
383
+ If True, leave the progress bar after completion.
379
384
  backend : str, optional (default=None)
380
385
  Backend to use for the parallelization. If None, the backend is chosen automatically between joblib and mpi.
381
386
  verbose : bool, optional (default=False)
@@ -388,7 +393,7 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
388
393
  lists.
389
394
  desc: str, optional (default=None)
390
395
  Description of the progress bar. If None, the description denotes the function and parameters names.
391
- kwargs :
396
+ fun_kwargs :
392
397
  Additional parameters to pass to the function.
393
398
 
394
399
  Returns
@@ -401,7 +406,7 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
401
406
  time_start = time()
402
407
 
403
408
  formatted_param_names, formatted_param_values = _format_input(param_names, param_values)
404
- _validate_function_arguments(fun, formatted_param_names, kwargs)
409
+ _validate_function_arguments(fun, formatted_param_names, fun_kwargs)
405
410
 
406
411
  # Create the list of dictionaries with the parameters in a nested loop
407
412
  n_values = [len(group[0]) for group in formatted_param_values]
@@ -452,14 +457,15 @@ def parameterrun(fun: Callable[..., Any], param_names: Union[str, List[str], Lis
452
457
  indices = [list(range(n_value)) for n_value in n_values]
453
458
  indices_iterate = product(*indices)
454
459
  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)
460
+ desc, n_workers=n_workers, pbar_bool=pbar_bool, pbar_leave=pbar_leave,
461
+ **fun_kwargs)
456
462
 
457
463
  elif backend == 'mpi':
458
464
  _log(f'Running under mpi with {size} workers', verbose and rank == 0,
459
465
  hostname=mpi_module.Get_processor_name() if mpi_module is not None else None, )
460
466
 
461
467
  result, n_outputs = _parameterrun_mpi(fun, formatted_param_names, formatted_param_values, desc,
462
- pbar_bool=pbar_bool, verbose=verbose, **kwargs)
468
+ pbar_bool=pbar_bool, pbar_leave=pbar_leave, verbose=verbose, **fun_kwargs)
463
469
 
464
470
  else:
465
471
  raise ValueError('Unknown backend')
@@ -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}"
File without changes
File without changes
File without changes