Skip to content

Commit ea6a48d

Browse files
Phase 3: common/named product (#133)
* Add named_product implementation Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * Add test_named_product Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * Switch from unittest to pytest Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * pytest parametrize Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * Address some reviews Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * Fix kwargs Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * Add comments Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> * Fixed TODO comments Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com> --------- Signed-off-by: Maria Teresa Ortega <teresa.ortega0903@gmail.com>
1 parent 67bd917 commit ea6a48d

File tree

6 files changed

+162
-71
lines changed

6 files changed

+162
-71
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ convention = "google"
5353
[tool.pytest.ini_options]
5454
testpaths = ["test"]
5555
pythonpath = [".",
56-
"examples"]
56+
"examples", "src"]

src/lambkin/common/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@
1616
1717
Exposes named_product and all base exceptions for convenience.
1818
"""
19+
20+
from .named_product import named_product
21+
22+
__all__ = [
23+
"named_product",
24+
]

src/lambkin/common/named_product.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,44 @@
1717
Provides named_product(), a utility to generate combinatorial parameter sets.
1818
Used to define benchmark configurations in a readable and structured way.
1919
"""
20+
21+
import itertools
22+
23+
24+
def named_product(**parameters):
25+
"""Generate all combinations of named parameters.
26+
27+
Takes parameters where each value is a list of options
28+
and returns a list of dictionaries representing every possible
29+
combination, one dictionary per benchmark run configuration.
30+
31+
Args:
32+
**parameters: Named parameter lists to combine. Each value must be a list.
33+
34+
Returns:
35+
A list of dicts, each mapping parameter names to a specific value.
36+
"""
37+
# Example:
38+
# parameters == (sensor_model = [likelihood_field,beam],
39+
# num_particles = [1, 10, 1000, 2000])
40+
41+
# keys = ("sensor_model", "num_particles")
42+
keys = list(parameters.keys())
43+
44+
# values = ( [likelihood_field,beam],[1, 10, 1000, 2000] )
45+
values = list(parameters.values())
46+
47+
# TODO(teresa-ortega): Empty list exception
48+
49+
# Make combinations
50+
combinations = itertools.product(*values)
51+
52+
# Create the dictionary
53+
variants = []
54+
for combination in combinations:
55+
# Re-attach the parameter labels to the generated values.
56+
# zip() pairs keys with values; dict() creates the mapping.
57+
variant = dict(zip(keys, combination, strict=True))
58+
# TODO(teresa-ortega) : combination fails
59+
variants.append(variant)
60+
return variants

test/common/test_named_product.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright 2026 Ekumen, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for the named_product function in lambkin.common."""
16+
17+
import pytest
18+
19+
from lambkin.common.named_product import named_product
20+
21+
22+
@pytest.mark.parametrize(
23+
"parameters, expected_combinations , expected_len",
24+
[
25+
( # Two parameters with 2 values each -> 2×2 = 4 combinations
26+
{"sensor_model": ["likelihood_field", "beam"], "num_particles": [1, 10]},
27+
[
28+
{"sensor_model": "likelihood_field", "num_particles": 1},
29+
{"sensor_model": "likelihood_field", "num_particles": 10},
30+
{"sensor_model": "beam", "num_particles": 1},
31+
{"sensor_model": "beam", "num_particles": 10},
32+
],
33+
4,
34+
),
35+
( # Single parameter with 2 values -> 2 combinations
36+
{"sensor_model": ["likelihood_field", "beam"]},
37+
[
38+
{"sensor_model": "likelihood_field"},
39+
{"sensor_model": "beam"},
40+
],
41+
2,
42+
),
43+
( # No parameters at all -> 1 combination: a single empty dict
44+
{},
45+
[{}],
46+
1,
47+
),
48+
( # One parameter with an empty list -> no combinations can be formed
49+
# TODO(teresa-ortega): if the user introduce a empty list -> Exception
50+
{"sensor_model": [], "num_particles": [1]},
51+
[],
52+
0,
53+
),
54+
],
55+
)
56+
def test_named_product_combinations(parameters, expected_combinations, expected_len):
57+
"""named_product returns the correct number and combinations."""
58+
result = named_product(**parameters)
59+
assert result == expected_combinations
60+
assert len(result) == expected_len
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright 2026 Ekumen, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for the process management functions in lambkin.py."""
16+
17+
import subprocess
18+
import time
19+
20+
from examples.lambkin_benchmarking import execute_background_process, wait_for_processes
21+
22+
23+
def test_execute_background_process():
24+
"""Tests if execute_background_process correctly starts a process.
25+
26+
It verifies the process runs in the background without blocking
27+
the main thread.
28+
"""
29+
process = execute_background_process(["sleep", "3"])
30+
assert process.poll() is None, "The process should be running in background."
31+
process.terminate()
32+
process.wait()
33+
34+
35+
def test_wait_for_processes():
36+
"""Tests if wait_for_processes properly blocks until waitlist completes.
37+
38+
It verifies that the function waits for the processes in the waitlist
39+
and subsequently terminates the processes in the termination_list.
40+
"""
41+
p_play = subprocess.Popen(["sleep", "2"])
42+
p_infinite = subprocess.Popen(["sleep", "infinity"])
43+
44+
start_time = time.time()
45+
wait_for_processes(waitlist=[p_play], termination_list=[p_infinite])
46+
end_time = time.time()
47+
48+
total_time = end_time - start_time
49+
50+
assert total_time >= 2.0, "The function did not wait for the waitlist to finish."
51+
assert total_time < 5.0, "The function got stuck waiting for the infinite process."
52+
assert p_infinite.poll() is not None, (
53+
"The process in the termination_list was not closed properly."
54+
)

test/test_process_management.py

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)