Skip to content

Commit 7134273

Browse files
authored
Merge pull request #3727 from djlaky/add-greybox
Pyomo.DoE: Bugfix for GreyBox
2 parents 56a503b + 3424f1a commit 7134273

File tree

3 files changed

+57
-19
lines changed

3 files changed

+57
-19
lines changed

pyomo/contrib/doe/doe.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,7 @@ def run_doe(self, model=None, results_file=None):
359359
)
360360
# Set objective value
361361
if self.objective_option == ObjectiveLib.trace:
362-
# Do safe inverse here?
363-
trace_val = 1 / np.trace(np.array(self.get_FIM()))
362+
trace_val = np.trace(np.linalg.pinv(self.get_FIM()))
364363
model.obj_cons.egb_fim_block.outputs["A-opt"].set_value(trace_val)
365364
elif self.objective_option == ObjectiveLib.determinant:
366365
det_val = np.linalg.det(np.array(self.get_FIM()))

pyomo/contrib/doe/grey_box_utilities.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,10 @@ def evaluate_jacobian_outputs(self):
310310
else:
311311
ObjectiveLib(self.objective_option)
312312

313+
# We are only using the triangle,
314+
# so we need to add the off-diagonals
315+
# twice.
316+
jac_M = 2 * jac_M - np.diag(np.diag(jac_M))
313317
# Filter jac_M using the
314318
# masking matrix
315319
jac_M = jac_M[self._masking_matrix > 0]
@@ -402,6 +406,24 @@ def evaluate_hessian_outputs(self, FIM=None):
402406
hess_rows.append(row)
403407
hess_cols.append(col)
404408

409+
# Hessian needs to be handled carefully because of
410+
# the ``missing`` components when only passing
411+
# a triangular version of the FIM.
412+
#
413+
# The general format is that fully diagonal elements
414+
# (i == j AND k == l) require no additional counting
415+
# of the computed term. Off-diagonal elements
416+
# (i != j OR k != l) add one instance. Having two
417+
# off-diagonal elements (i != j AND k != l) will add
418+
# two instances. Finally, if the off-diagonal elements
419+
# are not the same ((i != j AND k != l) AND (i != k))
420+
# there is an additional element. A more detailed
421+
# explanation is included in the documentation.
422+
multiplier = ((i != j) + (k != l)) + ((i != j) and (k != l)) * (i != k)
423+
hess_vals[-1] += multiplier * (
424+
(Minv[i, l] * Minv_sq[k, j]) + (Minv_sq[i, l] * Minv[k, j])
425+
)
426+
405427
elif self.objective_option == ObjectiveLib.determinant:
406428
# Grab inverse
407429
Minv = np.linalg.pinv(M)
@@ -424,6 +446,13 @@ def evaluate_hessian_outputs(self, FIM=None):
424446
hess_vals.append(-(Minv[i, l] * Minv[k, j]))
425447
hess_rows.append(row)
426448
hess_cols.append(col)
449+
450+
# See explanation above in trace hessian code
451+
# for more brief information. Also, you may
452+
# consult the documentation for a detailed
453+
# description.
454+
multiplier = ((i != j) + (k != l)) + ((i != j) and (k != l)) * (i != k)
455+
hess_vals[-1] += -multiplier * (Minv[i, l] * Minv[k, j])
427456
elif self.objective_option == ObjectiveLib.minimum_eigenvalue:
428457
# Grab eigenvalues and eigenvectors
429458
# Also need the min location
@@ -482,6 +511,13 @@ def evaluate_hessian_outputs(self, FIM=None):
482511
hess_vals.append(curr_hess_val)
483512
hess_rows.append(row)
484513
hess_cols.append(col)
514+
515+
# See explanation above in trace hessian code
516+
# for more brief information. Also, you may
517+
# consult the documentation for a detailed
518+
# description.
519+
multiplier = ((i != j) + (k != l)) + ((i != j) and (k != l)) * (i != k)
520+
hess_vals[-1] += multiplier * curr_hess_val
485521
elif self.objective_option == ObjectiveLib.condition_number:
486522
pass
487523
else:

pyomo/contrib/doe/tests/test_greybox.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,11 @@ def get_numerical_second_derivative(grey_box_object=None, return_reduced=True):
215215
for i in ordered_quads:
216216
row = ordered_pairs_list.index(i[0])
217217
col = ordered_pairs_list.index(i[1])
218-
numerical_derivative_reduced[row, col] = numerical_derivative[
219-
i[0][0], i[0][1], i[1][0], i[1][1]
220-
]
218+
i, j, k, l = i[0][0], i[0][1], i[1][0], i[1][1]
219+
multiplier = 1 + ((i != j) + (k != l)) + ((i != j) and (k != l)) * (i != k)
220+
numerical_derivative_reduced[row, col] = (
221+
multiplier * numerical_derivative[i, j, k, l]
222+
)
221223

222224
numerical_derivative_reduced += (
223225
numerical_derivative_reduced.transpose()
@@ -524,7 +526,8 @@ def test_jacobian_A_opt(self):
524526
# Recover the Jacobian in Matrix Form
525527
jac = np.zeros_like(grey_box_object._get_FIM())
526528
jac[np.triu_indices_from(jac)] = utri_vals_jac
527-
jac += jac.transpose() - np.diag(np.diag(jac))
529+
jac += jac.transpose()
530+
jac = jac / 2
528531

529532
# Get numerical derivative matrix
530533
jac_FD = get_numerical_derivative(grey_box_object)
@@ -547,7 +550,8 @@ def test_jacobian_D_opt(self):
547550
# Recover the Jacobian in Matrix Form
548551
jac = np.zeros_like(grey_box_object._get_FIM())
549552
jac[np.triu_indices_from(jac)] = utri_vals_jac
550-
jac += jac.transpose() - np.diag(np.diag(jac))
553+
jac += jac.transpose()
554+
jac = jac / 2
551555

552556
# Get numerical derivative matrix
553557
jac_FD = get_numerical_derivative(grey_box_object)
@@ -570,7 +574,8 @@ def test_jacobian_E_opt(self):
570574
# Recover the Jacobian in Matrix Form
571575
jac = np.zeros_like(grey_box_object._get_FIM())
572576
jac[np.triu_indices_from(jac)] = utri_vals_jac
573-
jac += jac.transpose() - np.diag(np.diag(jac))
577+
jac += jac.transpose()
578+
jac = jac / 2
574579

575580
# Get numerical derivative matrix
576581
jac_FD = get_numerical_derivative(grey_box_object)
@@ -593,7 +598,8 @@ def test_jacobian_ME_opt(self):
593598
# Recover the Jacobian in Matrix Form
594599
jac = np.zeros_like(grey_box_object._get_FIM())
595600
jac[np.triu_indices_from(jac)] = utri_vals_jac
596-
jac += jac.transpose() - np.diag(np.diag(jac))
601+
jac += jac.transpose()
602+
jac = jac / 2
597603

598604
# Get numerical derivative matrix
599605
jac_FD = get_numerical_derivative(grey_box_object)
@@ -990,7 +996,10 @@ def test_solve_D_optimality_log_determinant(self):
990996
# (time, optimal objective value)
991997
# Here, the objective value is
992998
# log-10(determinant) of the FIM
993-
optimal_experimental_designs = [np.array([2.24, 4.33]), np.array([10.00, 4.35])]
999+
optimal_experimental_designs = [
1000+
np.array([1.782, 4.34]),
1001+
np.array([10.00, 4.35]),
1002+
]
9941003
objective_option = "determinant"
9951004
doe_object, grey_box_object = make_greybox_and_doe_objects_rooney_biegler(
9961005
objective_option=objective_option
@@ -1002,12 +1011,6 @@ def test_solve_D_optimality_log_determinant(self):
10021011
# Solve the model
10031012
doe_object.run_doe()
10041013

1005-
print("Termination Message")
1006-
print(doe_object.results["Termination Message"])
1007-
print(cyipopt_call_working)
1008-
print(bad_message in doe_object.results["Termination Message"])
1009-
print("End Message")
1010-
10111014
optimal_time_val = doe_object.results["Experiment Design"][0]
10121015
optimal_obj_val = np.log10(np.exp(pyo.value(doe_object.model.objective)))
10131016

@@ -1035,7 +1038,7 @@ def test_solve_A_optimality_trace_of_inverse(self):
10351038
# Here, the objective value is
10361039
# trace(inverse(FIM))
10371040
optimal_experimental_designs = [
1038-
np.array([1.94, 0.0295]),
1041+
np.array([1.33, 0.0277]),
10391042
np.array([9.9, 0.0366]),
10401043
]
10411044
objective_option = "trace"
@@ -1076,7 +1079,7 @@ def test_solve_E_optimality_minimum_eigenvalue(self):
10761079
# Here, the objective value is
10771080
# minimum eigenvalue of the FIM
10781081
optimal_experimental_designs = [
1079-
np.array([1.92, 36.018]),
1082+
np.array([1.30, 38.70]),
10801083
np.array([10.00, 28.349]),
10811084
]
10821085
objective_option = "minimum_eigenvalue"
@@ -1117,7 +1120,7 @@ def test_solve_ME_optimality_condition_number(self):
11171120
# Here, the objective value is
11181121
# condition number of the FIM
11191122
optimal_experimental_designs = [
1120-
np.array([1.59, 15.22]),
1123+
np.array([0.943, 13.524]),
11211124
np.array([10.00, 27.675]),
11221125
]
11231126
objective_option = "condition_number"

0 commit comments

Comments
 (0)