Skip to content

Commit eb1e32e

Browse files
committed
Report errors in background correlation setup
Errors while reading the background sheet were swallowed by a broad try/except, silently leaving all background parameters out of the design matrix. Correlation sheet mismatches and missing background sheets are now reported to the user.
1 parent 580f742 commit eb1e32e

2 files changed

Lines changed: 187 additions & 17 deletions

File tree

src/semeio/fmudesign/_excel_to_dict.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -265,19 +265,17 @@ def parse_value(value: object) -> object:
265265
if isinstance(maybe_path, str) and Path(maybe_path).exists():
266266
output[key] = seeds_from_extern(maybe_path)
267267

268-
# If 'background' is a file, then read it
268+
# The 'background' key is either blank/'None', a reference to another file
269+
# or the name of a sheet in this workbook.
269270
key = "background"
270-
output[key] = {}
271-
try:
272-
value = str(generalinput[key])
273-
if value.endswith(("csv", "xlsx")):
274-
output[key]["extern"] = resolve_path(input_filename, value)
275-
else:
276-
output[key] = _read_background(input_filename, value)
277-
except KeyError:
271+
value = generalinput.get(key)
272+
background = "" if value is None else str(value).strip()
273+
if background.lower() in {"", "none"}:
278274
output[key] = None
279-
except ValueError:
280-
output[key] = generalinput[key]
275+
elif background.endswith(("csv", "xlsx")):
276+
output[key] = {"extern": resolve_path(input_filename, background)}
277+
else:
278+
output[key] = _read_background(input_filename, background)
281279

282280
output["defaultvalues"] = _read_defaultvalues(input_filename, default_values_sheet)
283281

@@ -464,6 +462,18 @@ def _read_background(inp_filename: str, bck_sheet: str) -> dict[str, Any]:
464462
"""
465463
backdict: dict[str, Any] = {}
466464
paramdict: dict[str, Any] = {}
465+
with pd.ExcelFile(inp_filename, engine="openpyxl") as workbook:
466+
sheet_names = [str(name) for name in workbook.sheet_names]
467+
try:
468+
bck_sheet = find_sheet(bck_sheet, names=sheet_names)
469+
except ValueError as err:
470+
raise ValueError(
471+
f"Sheet {bck_sheet!r} with background parameters, specified in the "
472+
f"general input sheet, was not found in {inp_filename!r}.\n"
473+
f"Sheets in workbook: {sheet_names}\n"
474+
"Use 'None' as background in the general input sheet if no "
475+
"background parameters are wanted."
476+
) from err
467477
bck_input = (
468478
pd.read_excel(inp_filename, bck_sheet, engine="openpyxl")
469479
.dropna(axis=0, how="all")
@@ -472,7 +482,9 @@ def _read_background(inp_filename: str, bck_sheet: str) -> dict[str, Any]:
472482

473483
backdict["correlations"] = None
474484
if "corr_sheet" in bck_input:
475-
backdict["correlations"] = _read_correlations(bck_input, inp_filename)
485+
backdict["correlations"] = _read_correlations(
486+
bck_input, inp_filename, group_description=f"background sheet {bck_sheet!r}"
487+
)
476488

477489
if "dist_param1" not in bck_input.columns.to_numpy():
478490
bck_input["dist_param1"] = float("NaN")
@@ -686,9 +698,17 @@ def _read_dist_sensitivity(sensgroup: pd.DataFrame) -> dict[str, Any]:
686698

687699

688700
def _read_correlations(
689-
sensgroup: pd.DataFrame, inputfile: str
701+
sensgroup: pd.DataFrame, inputfile: str, group_description: str | None = None
690702
) -> dict[str, Any] | None:
691-
"""Parse correlation information from a sensitivity group."""
703+
"""Parse correlation information from a sensitivity group.
704+
705+
Args:
706+
sensgroup: rows describing the parameters, either a sensitivity group
707+
from the designinput sheet or the background sheet.
708+
inputfile: name of the Excel workbook holding the correlation sheets.
709+
group_description: how to refer to `sensgroup` in error messages.
710+
Defaults to the sensname of the group.
711+
"""
692712

693713
# No correlation sheet column exists
694714
if "corr_sheet" not in sensgroup.columns:
@@ -698,6 +718,9 @@ def _read_correlations(
698718
if sensgroup["corr_sheet"].dropna().empty:
699719
return None
700720

721+
if group_description is None:
722+
group_description = f"sensitivity group {sensgroup['sensname'].iloc[0]!r}"
723+
701724
correlations: dict[str, Any] = {"inputfile": inputfile}
702725

703726
# Create a mapping 'corr_to_params' like:
@@ -714,11 +737,10 @@ def _read_correlations(
714737
for corr_sheet, parameters in corr_to_params.items():
715738
df_corr = read_correlations(excel_filename=inputfile, corr_sheet=corr_sheet)
716739
if set(df_corr.columns) != set(parameters):
717-
sensname = sensgroup["sensname"].iloc[0]
718-
msg = f"Mismatch between parameters in sensitivity group {sensname!r} "
740+
msg = f"Mismatch between parameters in {group_description} "
719741
msg += f"pointing to\ncorrelation sheet {corr_sheet!r} and "
720742
msg += "parameters specified in that correlation sheet.\n"
721-
msg += f"Parameters in sensitivity group: {sorted(set(parameters))}\n"
743+
msg += f"Parameters in {group_description}: {sorted(set(parameters))}\n"
722744
msg += f"Parameters in correlation sheet: {sorted(set(df_corr.columns))}\n"
723745
msg += "These parameters must be specified one-to-one."
724746
raise ValueError(msg)

tests/fmudesign/test_excel_to_dict.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,154 @@ def test_background_sheet(tmpdir, monkeypatch):
228228
assert dict_design["defaultvalues"]["extraseed"] == 0
229229

230230

231+
def _write_background_workbook(filename, background, corr_matrix, background_name):
232+
"""Write a workbook with a background sheet and a 'bgcorr' correlation sheet."""
233+
general_input = pd.DataFrame(
234+
data=[
235+
["designtype", "onebyone"],
236+
["repeats", 3],
237+
["rms_seeds", "default"],
238+
["background", background_name],
239+
["distribution_seed", 42],
240+
]
241+
)
242+
defaultvalues = pd.DataFrame(
243+
columns=["param_name", "default_value"], data=[["PARAM_A", 0], ["PARAM_B", 0]]
244+
)
245+
with pd.ExcelWriter(filename, engine="openpyxl") as writer:
246+
general_input.to_excel(
247+
writer, sheet_name="general_input", index=False, header=None
248+
)
249+
MOCK_DESIGNINPUT.to_excel(
250+
writer, sheet_name="designinput", index=False, header=None
251+
)
252+
defaultvalues.to_excel(writer, sheet_name="defaultvalues", index=False)
253+
background.to_excel(
254+
writer, sheet_name="backgroundsheet", index=False, header=None
255+
)
256+
if corr_matrix is not None:
257+
corr_matrix.to_excel(writer, sheet_name="bgcorr")
258+
259+
260+
BACKGROUND_WITH_CORR = pd.DataFrame(
261+
data=[
262+
["param_name", "dist_name", "dist_param1", "dist_param2", "corr_sheet"],
263+
["PARAM_A", "uniform", 0, 1, "bgcorr"],
264+
["PARAM_B", "uniform", 0, 1, "bgcorr"],
265+
]
266+
)
267+
268+
269+
def test_background_correlation_sheet_with_mismatching_index_and_columns(
270+
tmpdir, monkeypatch
271+
):
272+
"""Correlation matrices for background parameters must be validated the same
273+
way as correlation matrices for ordinary sensitivities."""
274+
monkeypatch.chdir(tmpdir)
275+
corr_matrix = pd.DataFrame(
276+
[[1.0, np.nan], [0.5, 1.0]],
277+
index=["PARAM_A", "PARAM_B"],
278+
columns=["PARAM_A", "PARAM_TYPO"],
279+
)
280+
_write_background_workbook(
281+
"designinput.xlsx", BACKGROUND_WITH_CORR, corr_matrix, "backgroundsheet"
282+
)
283+
284+
with pytest.raises(
285+
ValueError, match="Mismatch between column and index in correlation"
286+
):
287+
excel_to_dict("designinput.xlsx")
288+
289+
290+
def test_background_correlation_sheet_with_mismatching_parameters(tmpdir, monkeypatch):
291+
"""Parameters pointing to a correlation sheet from the background sheet must
292+
match the parameters in that correlation sheet exactly."""
293+
monkeypatch.chdir(tmpdir)
294+
corr_matrix = pd.DataFrame(
295+
[[1.0, np.nan], [0.5, 1.0]],
296+
index=["PARAM_A", "PARAM_TYPO"],
297+
columns=["PARAM_A", "PARAM_TYPO"],
298+
)
299+
_write_background_workbook(
300+
"designinput.xlsx", BACKGROUND_WITH_CORR, corr_matrix, "backgroundsheet"
301+
)
302+
303+
with pytest.raises(ValueError, match="Mismatch between parameters"):
304+
excel_to_dict("designinput.xlsx")
305+
306+
307+
def test_background_sheet_that_does_not_exist(tmpdir, monkeypatch):
308+
"""A background sheet name that does not exist must be reported, listing the
309+
sheets that are available."""
310+
monkeypatch.chdir(tmpdir)
311+
_write_background_workbook(
312+
"designinput.xlsx", BACKGROUND_WITH_CORR, None, "typo_sheet"
313+
)
314+
315+
with pytest.raises(ValueError, match="Sheets in workbook") as exc_info:
316+
excel_to_dict("designinput.xlsx")
317+
318+
message = str(exc_info.value)
319+
assert "typo_sheet" in message
320+
assert "backgroundsheet" in message
321+
assert "Use 'None' as background" in message
322+
323+
324+
@pytest.mark.parametrize(
325+
"background_name", ["Backgroundsheet", "background_sheet", " backgroundsheet "]
326+
)
327+
def test_background_sheet_name_is_matched_softly(tmpdir, monkeypatch, background_name):
328+
monkeypatch.chdir(tmpdir)
329+
corr_matrix = pd.DataFrame(
330+
[[1.0, np.nan], [0.5, 1.0]],
331+
index=["PARAM_A", "PARAM_B"],
332+
columns=["PARAM_A", "PARAM_B"],
333+
)
334+
_write_background_workbook(
335+
"designinput.xlsx", BACKGROUND_WITH_CORR, corr_matrix, background_name
336+
)
337+
338+
background = excel_to_dict("designinput.xlsx")["background"]
339+
assert list(background["parameters"]) == ["PARAM_A", "PARAM_B"]
340+
341+
342+
def test_background_file_that_does_not_exist(tmpdir, monkeypatch):
343+
monkeypatch.chdir(tmpdir)
344+
_write_background_workbook(
345+
"designinput.xlsx", BACKGROUND_WITH_CORR, None, "missing_background.csv"
346+
)
347+
348+
with pytest.raises(ValueError, match="Failed to resolve path"):
349+
excel_to_dict("designinput.xlsx")
350+
351+
352+
@pytest.mark.parametrize("background_name", ["None", "none", np.nan])
353+
def test_background_not_in_use(tmpdir, monkeypatch, background_name):
354+
"""Not specifying a background must not be an error."""
355+
monkeypatch.chdir(tmpdir)
356+
_write_background_workbook(
357+
"designinput.xlsx", BACKGROUND_WITH_CORR, None, background_name
358+
)
359+
360+
assert excel_to_dict("designinput.xlsx")["background"] is None
361+
362+
363+
def test_background_correlations_are_read(tmpdir, monkeypatch):
364+
monkeypatch.chdir(tmpdir)
365+
corr_matrix = pd.DataFrame(
366+
[[1.0, np.nan], [0.5, 1.0]],
367+
index=["PARAM_A", "PARAM_B"],
368+
columns=["PARAM_A", "PARAM_B"],
369+
)
370+
_write_background_workbook(
371+
"designinput.xlsx", BACKGROUND_WITH_CORR, corr_matrix, "backgroundsheet"
372+
)
373+
374+
background = excel_to_dict("designinput.xlsx")["background"]
375+
assert background["correlations"]["sheetnames"] == ["bgcorr"]
376+
assert list(background["parameters"]) == ["PARAM_A", "PARAM_B"]
377+
378+
231379
def test_assert_no_merged_cells(tmpdir, monkeypatch):
232380
"""Test that assert_no_merged_cells detects merged cells"""
233381
monkeypatch.chdir(tmpdir)

0 commit comments

Comments
 (0)