Skip to content

Commit 747af1a

Browse files
authored
Merge pull request #302 from POptUS/291TestM_Final
Finalize benchmark test script
2 parents abf917b + 5d19787 commit 747af1a

3 files changed

Lines changed: 85 additions & 108 deletions

File tree

pounders/py/tests/TestPoundersExtensive.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ def test_benchmark_pounders(self):
3838
factor = 10
3939

4040
for row, (nprob, n, m, factor_power) in enumerate(dfo):
41+
# TODO: Set nf_max to match values used in MATLAB to allow for
42+
# direct comparison. I suspect that many optimizations are running
43+
# down to delta_min. Therefore, we don't need a special nf_max,
44+
# but can add a check to confirm that at least one optimization did
45+
# run down to delta_min.
4146
if row == 0:
4247
nf_max = 500 # Testing delta_min stopping on first problem
4348
else:
@@ -100,6 +105,7 @@ def Ffun_batch(Y):
100105
assert hfun_name.startswith("h_")
101106
hfun_name = hfun_name.lstrip("h_")
102107

108+
# TODO: Need to make problem number 1-based to match filenaming scheme in MATLAB
103109
filename = RESULT_PATH.joinpath("pounders_nf_max=" + str(nf_max) + "_prob=" + str(row) + "_spsolver=" + str(spsolver) + "_hfun=" + hfun_name + ".mat")
104110
Opts = {"printf": printf, "spsolver": spsolver, "hfun": hfun, "combinemodels": combinemodels}
105111
Prior = {"nfs": 1, "F_init": F_init, "X_init": X_0, "xk_in": xind}
@@ -130,6 +136,13 @@ def Ffun_batch(Y):
130136
# implementations because, for instance, the MATLAB results
131137
# store the best approximation index as 1-based as opposed to
132138
# 0-based as this test does.
139+
#
140+
# TODO: Need to make problem number 1-based to match MATLAB's
141+
# value. Normally there should be no need to adjust this since
142+
# it's an internal value and we could adjust as needed when
143+
# loading the data when alg == POUNDERS_Py. However, we are
144+
# forced to use a 1-based problem number in the filename, so we
145+
# should make the value here match the value in the filename.
133146
Results = {"alg": "POUNDERS_Py", "problem": "problem " + str(row) + " from More/Wild", "Fvec": F, "H": hF, "X": X, "flag": flag, "xk_best": xk_best}
134147
# oct2py.kill_octave() # This is necessary to restart the octave instance,
135148
# # and thereby remove some caching of inside of oct2py,
Lines changed: 43 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,29 @@
1-
import scipy.io
2-
31
import numpy as np
42

53
from .load_results import load_results
64

5+
_FLAG_DELTA_MIN = -6
76

8-
def _load_results_m_v2(filename):
9-
"""
10-
POUNDERS/MATLAB v2 format established at commit 4336b866.
11-
"""
12-
EXPECTED_KEYS = {"alg", "problem", "H", "Fvec", "X", "flag", "xk_best"}
13-
14-
contents = scipy.io.loadmat(filename)
15-
keys = [k for k in contents.keys() if not k.startswith("__")]
16-
assert len(keys) == 1
17-
assert keys[0] == "Results"
18-
19-
# Only one valid set of results across all three known hfun cases.
20-
data = None
21-
tmp = contents[keys[0]]
22-
assert len(tmp) == 3
23-
for hfun in range(len(tmp)):
24-
# See if we can find that one valid result for this hfun case.
25-
tmp_i = [e for e in tmp[hfun] if np.squeeze(e).ndim == 0]
26-
if tmp_i:
27-
assert len(tmp_i) == 1
28-
assert data is None
29-
data = tmp_i[0][0]
30-
assert data is not None
31-
32-
assert set(data.dtype.names) == EXPECTED_KEYS
33-
34-
algorithm = data["alg"][0][0]
35-
problem = data["problem"][0][0]
36-
37-
H = np.squeeze(data["H"][0])
38-
assert H.ndim == 1
39-
n_evaluations = len(H)
40-
assert all(np.isreal(H))
41-
42-
Fvec = np.squeeze(data["Fvec"][0])
43-
assert Fvec.ndim == 2
44-
tmp, _ = Fvec.shape
45-
assert tmp == n_evaluations
46-
assert all(np.isreal(Fvec.flatten()))
47-
48-
X = np.squeeze(data["X"][0])
49-
assert X.ndim == 2
50-
tmp, _ = X.shape
51-
assert tmp == n_evaluations
52-
assert all(np.isreal(X.flatten()))
53-
assert all(np.isfinite(X.flatten()))
54-
55-
flag = np.squeeze(data["flag"][0])
56-
assert np.isreal(flag)
57-
assert np.isfinite(flag)
58-
# Stored as 1-based index, but needs to be 0-based index for working with
59-
# Python arrays in this code.
60-
xk_best = np.squeeze(data["xk_best"][0]) - 1
61-
assert xk_best in range(0, len(H))
62-
63-
return algorithm, problem, X, Fvec, H, xk_best, flag
7+
8+
def _failed(flag):
9+
# Having the optimization terminate due to the trust region radius
10+
# shrinking down to delta_min does not necessarily indicate a failure. If
11+
# delta_min is well-specified, it could be treated as a success.
12+
return (flag < 0) and (flag != _FLAG_DELTA_MIN)
6413

6514

6615
def compare_results(filename_benchmark, filename_result):
6716
"""
6817
.. todo::
6918
* Allow for users to specify nonzero tolerances if the use case arises.
70-
* Allow for checking Python and MATLAB results on a set of problems on
71-
which we expect all optimizations to find the same local minimizer.
72-
This would require nonzero tolerances.
7319
7420
:param filename_benchmark: Filename of |pounders| ``.mat``-format
7521
benchmarking result that calling code considers to be the accepted
7622
reference.
7723
:param filename_result: Filename of |pounders| ``.mat``-format benchmarking
7824
result that calling code wishes to check against the reference.
7925
:return: True if the files correspond to identical test setups and contain
80-
bitwise-identical results.
26+
valid, bitwise-identical results.
8127
"""
8228
# ----- HARDCODED VALUES
8329
RED = "\033[0;91;1m" # Bright Red/bold
@@ -95,7 +41,7 @@ def error(msg):
9541
error(f"New result has different filename ({filename_result.stem})")
9642
return False
9743

98-
ref_alg, ref_problem, X_ref, F_ref, H_ref, x_best_ref, flag_ref = _load_results_m_v2(filename_benchmark)
44+
ref_alg, ref_problem, X_ref, F_ref, H_ref, x_best_ref, flag_ref = load_results(filename_benchmark)
9945
if not all(np.isfinite(H_ref)):
10046
error("Non-finite h values in benchmark")
10147
return False
@@ -111,37 +57,27 @@ def error(msg):
11157
error("Non-finite Fvec values in new results")
11258
return False
11359

114-
# TODO: Once we are testing v3 against v3, remove this check since
115-
# load_result() error checks the algorithm name and we would like to check
116-
# MATLAB results against Python results.
117-
if ref_alg not in ["POUNDERs"]:
118-
error(f"Invalid algorithm name ({ref_alg}) for benchmark")
119-
return False
120-
elif new_alg != "POUNDERS_M":
121-
msg = "Benchmark and new result used different algorithms ({} != {})"
122-
error(msg.format(ref_alg, new_alg))
123-
return False
124-
12560
if new_problem != ref_problem:
12661
msg = "Benchmark and new result solve different problems ({} != {})"
12762
error(msg.format(ref_problem, new_problem))
12863
return False
12964

13065
# ----- COMPARE NEW RESULTS AGAINST BENCHMARK
131-
if len(H_new) != len(H_ref):
132-
error(f"H arrays have different lengths ({len(H_ref)} != {len(H_new)})")
133-
return False
134-
assert F_new.shape == F_ref.shape
135-
assert X_new.shape == X_ref.shape
136-
137-
# Don't fail immediately if values are different so that we can provide
138-
# users with all such differences in one go.
139-
msgs = []
66+
# These checks are designed under the assumption that the prime use of this
67+
# function is to detect if two results are not *identical*.
68+
#
69+
# Even so, we don't fail immediately if values are different so that we can
70+
# provide users with all such differences in one go.
71+
assert F_new.shape[1] == F_ref.shape[1]
72+
assert X_new.shape[1] == X_ref.shape[1]
73+
74+
errors = []
75+
warnings = []
14076
if x_best_new != x_best_ref:
141-
msgs += [f"Best approximation indices differ ({x_best_new} != {x_best_ref})"]
77+
errors += [f"Best approximation indices differ ({x_best_new} != {x_best_ref})"]
14278
if flag_new != flag_ref:
143-
msgs += [f"Flags differ ({flag_new} != {flag_ref})"]
144-
if (flag_new >= 0) and (flag_ref >= 0):
79+
errors += [f"Flags differ ({flag_new} != {flag_ref})"]
80+
if (not _failed(flag_new)) and (not _failed(flag_ref)):
14581
# Only show comparison if both ran without a hard failure. For
14682
# instance, I would like to see the these comparisons if one or both
14783
# were simply nonconvergent.
@@ -153,19 +89,34 @@ def error(msg):
15389
F_best_new = F_new[x_best_new]
15490
H_best_new = H_new[x_best_new]
15591

92+
if flag_ref == _FLAG_DELTA_MIN:
93+
warnings += ["Benchmark reached delta_min"]
94+
if flag_new == _FLAG_DELTA_MIN:
95+
warnings += ["New result reached delta_min"]
96+
15697
if H_best_new != H_best_ref:
15798
abs_diff = np.fabs(H_best_new - H_best_ref)
158-
msgs += [f"H absolute difference = {abs_diff}"]
99+
errors += [f"H absolute difference = {abs_diff}"]
159100
if any(F_best_new != F_best_ref):
160101
max_abs_diff = np.max(np.fabs(F_best_new - F_best_ref))
161-
msgs += [f"Fvec max absolute difference = {max_abs_diff}"]
102+
errors += [f"Fvec max absolute difference = {max_abs_diff}"]
162103
if any(X_best_new != X_best_ref):
163104
max_abs_diff = np.max(np.fabs(X_best_new - X_best_ref))
164-
msgs += [f"X max absolute difference = {max_abs_diff}"]
165-
166-
if msgs:
167-
error("\n\t".join(msgs))
105+
errors += [f"X max absolute difference = {max_abs_diff}"]
106+
else:
107+
# We've already reported an error if the flags differ and identical
108+
# "bad" flags is not necessarily a failure.
109+
if _failed(flag_ref):
110+
warnings += [f"Benchmark failed with flag={flag_ref}"]
111+
if _failed(flag_new):
112+
warnings += [f"New result failed with flag={flag_new}"]
113+
114+
if errors:
115+
error("\n\t".join(errors + warnings))
168116
return False
117+
elif warnings:
118+
print(f"{BLUE}PASS{NC}\n\t" + "\n\t".join(warnings))
119+
return True
169120

170121
print(f"{BLUE}PASS{NC}")
171122
return True

pounders/py/tests/load_results.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import copy
2+
13
import scipy.io
24

35
import numpy as np
@@ -11,18 +13,27 @@ def load_results(filename):
1113
"""
1214
EXPECTED_KEYS = {"alg", "problem", "H", "Fvec", "X", "flag", "xk_best"}
1315

14-
# When adapting this to loading from MATLAB-generated files on GCE, I was
15-
# forced to add the simplify_cells argument. Without this, some loaded
16-
# results had a "W" or a "V" field instead of the "X" field. When I loaded
17-
# those same files manually in Python using the exact same venv and
18-
# scipy.io.loadmat(filename), I always saw the "X" field and didn't see the
19-
# others. I don't understand why this was happening nor why the addition
20-
# of the flag solves it.
21-
data = scipy.io.loadmat(filename, simplify_cells=True)
16+
with open(filename, "rb") as fptr:
17+
# If I call this function twice in a row on the same MATLAB-generated
18+
# file (using both scipy v1.16.2 and v1.17.1), on some occasions the "X"
19+
# field will appear with a different name (e.g., "W") on the second
20+
# call. This is apparently related to the change to xk_best below. If
21+
# I rerun that same test after exiting Python, I get the same result,
22+
# which implies that the file itself is not being altered. I hope that
23+
# this is the case since we load in readonly mode. This even happens if
24+
# the two files loaded back-to-back are different.
25+
#
26+
# Therefore, I suspect that there is some sort of caching of or
27+
# accidental referencing to loaded data. Caching does not make sense
28+
# when loading two different files. However, using a deepcopy resolves
29+
# the issue. A simple copy is not sufficient, which makes sense since
30+
# data is a nested dictionary.
31+
data = copy.deepcopy(scipy.io.loadmat(fptr))
2232
keys = [k for k in data.keys() if not k.startswith("__")]
2333
assert set(keys) == EXPECTED_KEYS
2434

2535
algorithm = str(np.squeeze(data["alg"]))
36+
assert algorithm in ["POUNDERS_M", "POUNDERS_Py"]
2637

2738
problem = str(np.squeeze(data["problem"]))
2839
if (not problem.startswith("problem")) or (not problem.endswith("from More/Wild")):
@@ -32,37 +43,39 @@ def load_results(filename):
3243
except Exception:
3344
raise ValueError(f"Invalid problem spec ({problem})")
3445

35-
H = np.squeeze(data["H"])
46+
H = np.atleast_1d(np.squeeze(data["H"]))
3647
assert H.ndim == 1
3748
n_evaluations = len(H)
3849
assert all(np.isreal(H))
50+
# Non-finite can happen in our tests, so checking finiteness of H must be
51+
# handled by calling code.
3952

4053
# Fvec could be a scalar at each evaluation
41-
Fvec = np.atleast_2d(np.squeeze(data["Fvec"]))
54+
Fvec = np.atleast_2d(data["Fvec"])
4255
assert Fvec.ndim == 2
4356
tmp, _ = Fvec.shape
4457
assert tmp == n_evaluations
4558
assert all(np.isreal(Fvec.flatten()))
59+
# Non-finite can happen in our tests, so checking finiteness of Fvec must
60+
# be handled by calling code.
4661

4762
# X could be a scalar at each evaluation
48-
X = np.atleast_2d(np.squeeze(data["X"]))
63+
X = np.atleast_2d(data["X"])
4964
assert X.ndim == 2
5065
tmp, _ = X.shape
5166
assert tmp == n_evaluations
5267
assert all(np.isreal(X.flatten()))
5368
assert all(np.isfinite(X.flatten()))
5469

5570
flag = np.squeeze(data["flag"])
56-
assert np.isreal(flag)
57-
assert np.isfinite(flag)
71+
assert (flag >= 0.0) or (flag in [-6, -5, -4, -3, -2, -1])
72+
5873
xk_best = np.squeeze(data["xk_best"])
5974
if algorithm == "POUNDERS_M":
6075
# The MATLAB implementation's test suite saves the index of the best
6176
# approximation as a 1-based integer. However, we need to adjust it to
6277
# 0-based since we are returning Python arrays.
6378
xk_best -= 1
64-
elif algorithm != "POUNDERS_Py":
65-
raise ValueError(f"Unknown POUNDERS test algorithm {algorithm}")
66-
assert xk_best in range(0, len(H))
79+
assert xk_best in range(0, n_evaluations)
6780

6881
return algorithm, problem, X, Fvec, H, xk_best, flag

0 commit comments

Comments
 (0)