Skip to content

Commit 291dba4

Browse files
authored
Merge pull request usra-riacs#66 from bernalde/fix/issue-65-coverage-workflows
Improve coverage for workflow modules
2 parents 62db3b3 + 6f49364 commit 291dba4

10 files changed

Lines changed: 1210 additions & 14 deletions

src/cross_validation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,8 @@ def seq_search_evaluate(eval_test, parameter_names, response_col):
452452
"TotalBudget": "resource",
453453
response_col: "response",
454454
# base :'response',
455-
"ConfInt=upper_" + response_col: "response_lower",
456-
"ConfInt=lower_" + response_col: "response_upper",
455+
"ConfInt=lower_" + response_col: "response_lower",
456+
"ConfInt=upper_" + response_col: "response_upper",
457457
},
458458
inplace=True,
459459
)

src/utils_ws.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ def interpolate_df(
286286
if all_datapoints:
287287
# Create a dataframe with the interesting reads as index and all the columns
288288
dummy_df = pd.DataFrame(
289-
np.NaN, index=interpolate_resource, columns=df_index.columns
289+
np.nan, index=interpolate_resource, columns=df_index.columns
290290
)
291291
dummy_df.drop(columns=resource_column, inplace=True)
292292
# Fill out the values that we have certain

tests/test_bootstrap.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ def dummy_update_rule(self, df):
3737
pass
3838

3939

40+
class InlinePool:
41+
"""Small Pool stand-in that executes map calls in process for unit tests."""
42+
43+
def __enter__(self):
44+
return self
45+
46+
def __exit__(self, exc_type, exc, tb):
47+
return False
48+
49+
def map(self, func, iterable):
50+
return [func(item) for item in iterable]
51+
52+
4053
class TestBootstrapParameters:
4154
"""Test class for BootstrapParameters dataclass."""
4255

@@ -818,6 +831,47 @@ def test_bootstrap_with_list_input(self):
818831
result = Bootstrap([df1, df2], ['group'], [params])
819832

820833
assert isinstance(result, pd.DataFrame)
834+
835+
def test_bootstrap_with_list_of_pickle_paths(self):
836+
"""Test Bootstrap function with a list of serialized DataFrames."""
837+
with tempfile.NamedTemporaryFile(suffix='.pkl', delete=False) as tmp_file1:
838+
df1 = pd.DataFrame({
839+
'energy': [100, 80],
840+
'time': [10, 15],
841+
'group': ['A', 'A']
842+
})
843+
df1.to_pickle(tmp_file1.name)
844+
845+
with tempfile.NamedTemporaryFile(suffix='.pkl', delete=False) as tmp_file2:
846+
df2 = pd.DataFrame({
847+
'energy': [120, 90],
848+
'time': [8, 12],
849+
'group': ['B', 'B']
850+
})
851+
df2.to_pickle(tmp_file2.name)
852+
853+
shared_args = {'response_col': 'energy', 'resource_col': 'time'}
854+
params = BootstrapParameters(shared_args=shared_args, update_rule=dummy_update_rule, downsample=1)
855+
856+
try:
857+
with patch('bootstrap.Pool') as mock_pool:
858+
mock_result = pd.DataFrame({'result': [1], 'group': ['A'], 'boots': [1]})
859+
mock_pool.return_value.__enter__.return_value.map.return_value = [mock_result]
860+
861+
result = Bootstrap([tmp_file1.name, tmp_file2.name], ['group'], [params])
862+
finally:
863+
os.unlink(tmp_file1.name)
864+
os.unlink(tmp_file2.name)
865+
866+
assert isinstance(result, pd.DataFrame)
867+
868+
def test_bootstrap_rejects_unsupported_input_type(self):
869+
"""Test Bootstrap fails clearly for unsupported input objects."""
870+
shared_args = {'response_col': 'energy', 'resource_col': 'time'}
871+
params = BootstrapParameters(shared_args=shared_args, update_rule=dummy_update_rule)
872+
873+
with pytest.raises(TypeError, match="Expected DataFrame"):
874+
Bootstrap(42, ['group'], [params])
821875

822876
def test_bootstrap_with_progress_dir(self):
823877
"""Test Bootstrap function with progress directory."""
@@ -843,6 +897,69 @@ def test_bootstrap_with_progress_dir(self):
843897

844898
assert isinstance(result, pd.DataFrame)
845899

900+
def test_bootstrap_executes_grouped_work_in_process(self, monkeypatch):
901+
"""Test Bootstrap's grouped apply path without multiprocessing."""
902+
from pandas.core.groupby.generic import DataFrameGroupBy
903+
904+
monkeypatch.setattr(
905+
DataFrameGroupBy,
906+
'progress_apply',
907+
DataFrameGroupBy.apply,
908+
)
909+
910+
df = pd.DataFrame({
911+
'energy': [100, 80, 120, 90],
912+
'time': [10, 15, 8, 12],
913+
'group': ['A', 'A', 'B', 'B']
914+
})
915+
916+
shared_args = {'response_col': 'energy', 'resource_col': 'time'}
917+
params = BootstrapParameters(
918+
shared_args=shared_args,
919+
update_rule=dummy_update_rule,
920+
downsample=3,
921+
)
922+
923+
def grouped_result(group_df, bs_params):
924+
return pd.DataFrame({'rows_seen': [len(group_df)]})
925+
926+
with patch('bootstrap.Pool', InlinePool), patch(
927+
'bootstrap.BootstrapSingle', side_effect=grouped_result
928+
):
929+
result = Bootstrap(df, ['group'], [params])
930+
931+
assert set(result['group']) == {'A', 'B'}
932+
assert result['rows_seen'].tolist() == [2, 2]
933+
assert result['boots'].eq(3).all()
934+
935+
def test_bootstrap_uses_progress_file_without_recomputing(self):
936+
"""Test Bootstrap loads cached progress files before grouped work."""
937+
with tempfile.TemporaryDirectory() as temp_dir:
938+
df = pd.DataFrame({
939+
'energy': [100, 80],
940+
'time': [10, 15],
941+
'group': ['A', 'A']
942+
})
943+
shared_args = {'response_col': 'energy', 'resource_col': 'time'}
944+
params = BootstrapParameters(
945+
shared_args=shared_args,
946+
update_rule=dummy_update_rule,
947+
downsample=4,
948+
)
949+
cached = pd.DataFrame({'loaded': [True], 'boots': [4]})
950+
cached.to_pickle(
951+
os.path.join(temp_dir, 'bootstrapped_results_boots=4.pkl')
952+
)
953+
954+
with patch('bootstrap.Pool', InlinePool), patch(
955+
'bootstrap.BootstrapSingle'
956+
) as mock_bootstrap_single:
957+
result = Bootstrap(df, ['group'], [params], progress_dir=temp_dir)
958+
959+
mock_bootstrap_single.assert_not_called()
960+
assert result['loaded'].tolist() == [True]
961+
assert result['boots'].tolist() == [4]
962+
846963

847964
class TestBootstrapReduceMem:
848965
"""Test class for Bootstrap_reduce_mem function."""
@@ -977,4 +1094,4 @@ def test_init_bootstrap_single_row(self):
9771094

9781095

9791096
if __name__ == "__main__":
980-
pytest.main([__file__])
1097+
pytest.main([__file__])

tests/test_cross_validation.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
88

99
import stats
10+
import cross_validation as cv
1011

1112
from cross_validation import (
1213
baseline_evaluate,
@@ -17,6 +18,19 @@
1718
)
1819

1920

21+
@pytest.fixture(autouse=True)
22+
def clear_cross_validation_globals():
23+
cv.parameters_dict.clear()
24+
cv.performance_dict.clear()
25+
cv.parameters_summarized_dict.clear()
26+
cv.performance_summarized_dict.clear()
27+
yield
28+
cv.parameters_dict.clear()
29+
cv.performance_dict.clear()
30+
cv.parameters_summarized_dict.clear()
31+
cv.performance_summarized_dict.clear()
32+
33+
2034
class TestBaselineEvaluate:
2135
def test_basic(self):
2236
df = pd.DataFrame({
@@ -102,5 +116,104 @@ def test_seq_search_evaluate(self):
102116
params_df, eval_df = seq_search_evaluate(df, ['param1'], 'Resp')
103117
assert 'resource' in params_df.columns
104118
assert len(eval_df) == 2
119+
assert eval_df.loc[0, "response_lower"] == 10
120+
assert eval_df.loc[0, "response_upper"] == 12
121+
122+
123+
class TestCrossValidationFileWorkflows:
124+
def test_load_parameters_concatenates_splits_and_warns_for_missing_files(self, tmp_path):
125+
folders = [tmp_path / "split0", tmp_path / "split1"]
126+
for folder in folders:
127+
(folder / "params_plotting").mkdir(parents=True)
128+
129+
pd.DataFrame({
130+
"Unnamed: 0": [0],
131+
"resource": [1],
132+
"param1": [0.1],
133+
}).to_csv(folders[0] / "params_plotting" / "baseline.csv", index=False)
134+
pd.DataFrame({
135+
"resource": [1],
136+
"param1": [0.2],
137+
}).to_csv(folders[1] / "params_plotting" / "baseline.csv", index=False)
138+
pd.DataFrame({
139+
"resource": [1],
140+
"param1": [0.4],
141+
}).to_csv(folders[0] / "params_plotting" / "experiment.csv", index=False)
142+
143+
with pytest.warns(UserWarning, match="experiment.csv not found"):
144+
cv.load_parameters([str(folder) for folder in folders], ["experiment"])
145+
146+
baseline = cv.parameters_dict["baseline"]
147+
experiment = cv.parameters_dict["experiment"]
148+
assert "Unnamed: 0" not in baseline.columns
149+
assert baseline["split_ind"].tolist() == [0, 1]
150+
assert experiment["split_ind"].tolist() == [0]
151+
152+
def test_process_params_across_splits_summarizes_each_experiment(self):
153+
cv.parameters_dict["baseline"] = pd.DataFrame({
154+
"resource": [1, 1, 2, 2],
155+
"param1": [0.0, 2.0, 4.0, 6.0],
156+
})
157+
158+
cv.process_params_across_splits(["param1"], confidence_level=68)
159+
160+
summary = cv.parameters_summarized_dict["baseline"]["param1"]
161+
assert summary["resource"].tolist() == [1, 2]
162+
assert summary.loc[summary["resource"] == 1, "mean"].iloc[0] == pytest.approx(1.0)
163+
assert {"CI_l", "CI_u"} <= set(summary.columns)
164+
165+
def test_load_and_process_performance_across_splits(self, tmp_path):
166+
folders = [tmp_path / "split0", tmp_path / "split1"]
167+
for idx, folder in enumerate(folders):
168+
(folder / "performance_plotting").mkdir(parents=True)
169+
pd.DataFrame({
170+
"Unnamed: 0": [0],
171+
"resource": [1],
172+
"response": [0.5 + idx],
173+
}).to_csv(folder / "performance_plotting" / "baseline.csv", index=False)
174+
pd.DataFrame({
175+
"resource": [1],
176+
"response": [0.7 + idx],
177+
"response_lower": [0.6 + idx],
178+
"response_upper": [0.8 + idx],
179+
}).to_csv(folder / "performance_plotting" / "experiment.csv", index=False)
180+
181+
cv.load_performance(
182+
[str(folder) for folder in folders],
183+
["experiment"],
184+
interpolate_flag=False,
185+
)
186+
assert "Unnamed: 0" not in cv.performance_dict["baseline"].columns
187+
188+
cv.process_performance_across_splits(stats_measure="mean")
189+
190+
baseline = cv.performance_summarized_dict["baseline"]
191+
experiment = cv.performance_summarized_dict["experiment"]
192+
assert baseline.loc[0, "mean"] == pytest.approx(1.0)
193+
assert {"mean", "CI_l", "CI_u"} <= set(experiment.columns)
194+
195+
def test_create_eval_params_dfs_dispatches_projection_evaluator(self, tmp_path):
196+
response_col = "Resp"
197+
folders = [tmp_path / "split0", tmp_path / "split1"]
198+
for idx, folder in enumerate(folders):
199+
folder.mkdir()
200+
pd.DataFrame({
201+
"resource": [1],
202+
"param1": [idx + 0.5],
203+
response_col: [idx + 1.0],
204+
"ConfInt=lower_" + response_col: [idx + 0.8],
205+
"ConfInt=upper_" + response_col: [idx + 1.2],
206+
}).to_pickle(folder / "projection.pkl")
207+
208+
raw, params, perf = cv.create_eval_params_dfs(
209+
"projection.pkl",
210+
[str(folder) for folder in folders],
211+
["param1"],
212+
"proj_expt_evaluate",
213+
response_col,
214+
)
105215

216+
assert raw["split"].tolist() == [1, 2]
217+
assert params["split"].tolist() == [1, 2]
218+
assert perf["response_lower"].tolist() == [0.8, 1.8]
106219

0 commit comments

Comments
 (0)