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.
- {parameterrun-0.4.0 → parameterrun-0.4.1}/CHANGELOG.md +11 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/PKG-INFO +1 -1
- parameterrun-0.4.1/examples/long_example.py +16 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/src/parameterrun/__init__.py +1 -1
- {parameterrun-0.4.0 → parameterrun-0.4.1}/src/parameterrun/parallel_utils.py +22 -16
- parameterrun-0.4.1/tests/test_pbar_format.py +106 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/.github/workflows/ci.yml +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/.github/workflows/publish.yml +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/.github/workflows/release.yml +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/.gitignore +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/CONTRIBUTING.md +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/LICENSE +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/README.md +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/examples/minimal_example.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/examples/minimal_example_mpi.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/pyproject.toml +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/scripts/bump_version.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_depths.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_format_input.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_get_iteration.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_joblib.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_mpi_integration.py +0 -0
- {parameterrun-0.4.0 → parameterrun-0.4.1}/tests/test_normalized_time.py +0 -0
- {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.
|
|
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, )
|
|
@@ -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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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,
|
|
345
|
-
|
|
346
|
-
desc: Optional[str] = None, **
|
|
346
|
+
n_workers: Optional[int] = -1, pbar_bool: bool = True, pbar_leave: bool = True,
|
|
347
|
+
verbose: Optional[bool] = False, reshape: bool = True, result_as_array: bool = True,
|
|
348
|
+
backend: Optional[str] = None, desc: Optional[str] = None, **fun_kwargs) -> Union[
|
|
349
|
+
list, np.ndarray, None]: # noqa: E501
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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, **
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|