Skip to content

Commit ed318ae

Browse files
committed
Tests for indexed params and full error cov in test_doe_solve.py
1 parent b710d5e commit ed318ae

1 file changed

Lines changed: 168 additions & 15 deletions

File tree

pyomo/contrib/doe/tests/test_doe_solve.py

Lines changed: 168 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,79 @@ def get_rooney_biegler_data():
7070
return data.iloc[0]
7171

7272

73-
def get_rooney_biegler_experiment():
73+
def get_rooney_biegler_experiment(index_vars=False):
7474
"""Get a fresh RooneyBieglerExperiment instance for testing.
7575
7676
Creates a new experiment instance to ensure test isolation.
7777
Each test gets its own instance to avoid state sharing.
78+
79+
Parameters
80+
----------
81+
index_vars : Boolean
82+
Specifies if the Rooney-Biegler model with indexed
83+
parameters is used
7884
"""
79-
return RooneyBieglerExperiment(
80-
data=get_rooney_biegler_data(),
81-
theta={'asymptote': 15, 'rate_constant': 0.5},
82-
measure_error=0.1,
83-
)
85+
86+
if index_vars:
87+
88+
class RooneyBieglerExperimentIndexed(RooneyBieglerExperiment):
89+
def create_model(self):
90+
data = self.data.to_frame().transpose()
91+
92+
model = pyo.ConcreteModel()
93+
94+
model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"])
95+
model.theta = pyo.Var(model.var_names, initialize=self.theta)
96+
model.theta["asymptote"].fix()
97+
model.theta["rate_constant"].fix()
98+
99+
# Add the experiment inputs
100+
model.hour = pyo.Var(initialize=data["hour"].iloc[0], bounds=(0, 10))
101+
102+
# Fix the experiment inputs
103+
model.hour.fix()
104+
105+
# Add experiment outputs
106+
model.y = pyo.Var(
107+
initialize=data['y'].iloc[0], within=pyo.PositiveReals
108+
)
109+
110+
# Define the model equations
111+
def response_rule(m):
112+
return m.y == m.theta["asymptote"] * (
113+
1 - pyo.exp(-m.theta["rate_constant"] * m.hour)
114+
)
115+
116+
model.response_con = pyo.Constraint(rule=response_rule)
117+
118+
self.model = model
119+
120+
def label_model(self):
121+
m = self.model
122+
123+
m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL)
124+
m.experiment_outputs.update([(m.y, self.data["y"])])
125+
126+
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
127+
m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta])
128+
129+
m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL)
130+
m.measurement_error.update([(m.y, self.measure_error)])
131+
132+
m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL)
133+
m.experiment_inputs.update([(m.hour, self.data['hour'])])
134+
135+
return RooneyBieglerExperimentIndexed(
136+
data=get_rooney_biegler_data(),
137+
theta={'asymptote': 15, 'rate_constant': 0.5},
138+
measure_error=0.1,
139+
)
140+
else:
141+
return RooneyBieglerExperiment(
142+
data=get_rooney_biegler_data(),
143+
theta={'asymptote': 15, 'rate_constant': 0.5},
144+
measure_error=0.1,
145+
)
84146

85147

86148
def get_FIM_Q_L(doe_obj=None):
@@ -111,9 +173,8 @@ def get_FIM_Q_L(doe_obj=None):
111173
for i in model.output_names
112174
for j in model.parameter_names
113175
]
114-
sigma_inv = [
115-
1 / v**2 for k, v in model.scenario_blocks[0].measurement_error.items()
116-
]
176+
sigma_inv = doe_obj.get_meas_error_covariance_matrix_inv(model.scenario_blocks[0])
177+
117178
FIM_vals_np = np.array(FIM_vals).reshape((n_param, n_param))
118179

119180
for i in range(n_param):
@@ -124,12 +185,7 @@ def get_FIM_Q_L(doe_obj=None):
124185
L_vals_np = np.array(L_vals).reshape((n_param, n_param))
125186
Q_vals_np = np.array(Q_vals).reshape((n_y, n_param))
126187

127-
sigma_inv_np = np.zeros((n_y, n_y))
128-
129-
for ind, v in enumerate(sigma_inv):
130-
sigma_inv_np[ind, ind] = v
131-
132-
return FIM_vals_np, Q_vals_np, L_vals_np, sigma_inv_np
188+
return FIM_vals_np, Q_vals_np, L_vals_np, sigma_inv
133189

134190

135191
def get_standard_args(experiment, fd_method, obj_used):
@@ -168,23 +224,36 @@ def test_rooney_biegler_fd_central_solve(self):
168224

169225
# Use RooneyBiegler for algorithm validation (faster)
170226
experiment = get_rooney_biegler_experiment()
227+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
171228

172229
DoE_args = get_standard_args(experiment, fd_method, obj_used)
230+
indx_param_DoE_args = get_standard_args(
231+
indx_param_experiment, fd_method, obj_used
232+
)
173233

174234
doe_obj = DesignOfExperiments(**DoE_args)
235+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
175236

176237
doe_obj.run_doe()
238+
indx_param_doe_obj.run_doe()
177239

178240
# assert model solves
179241
self.assertEqual(doe_obj.results["Solver Status"], "ok")
242+
self.assertEqual(indx_param_doe_obj.results["Solver Status"], "ok")
180243

181244
# assert that Q, F, and L are the same.
182245
FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj)
246+
FIM_indx, Q_indx, L_indx, sigma_inv_indx = get_FIM_Q_L(
247+
doe_obj=indx_param_doe_obj
248+
)
183249

184250
# Since Trace is used, no comparison for FIM and L.T @ L
185251

186252
# Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM)
187253
self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)))
254+
self.assertTrue(
255+
np.all(np.isclose(FIM_indx, Q_indx.T @ sigma_inv_indx @ Q_indx))
256+
)
188257

189258
@unittest.skipIf(not pandas_available, "pandas is not available")
190259
def test_rooney_biegler_fd_forward_solve(self):
@@ -230,22 +299,35 @@ def test_rooney_biegler_fd_backward_solve(self):
230299

231300
# Use RooneyBiegler for algorithm validation (faster)
232301
experiment = get_rooney_biegler_experiment()
302+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
233303

234304
DoE_args = get_standard_args(experiment, fd_method, obj_used)
305+
indx_param_DoE_args = get_standard_args(
306+
indx_param_experiment, fd_method, obj_used
307+
)
235308

236309
doe_obj = DesignOfExperiments(**DoE_args)
310+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
237311

238312
doe_obj.run_doe()
313+
indx_param_doe_obj.run_doe()
239314

240315
self.assertEqual(doe_obj.results["Solver Status"], "ok")
316+
self.assertEqual(indx_param_doe_obj.results["Solver Status"], "ok")
241317

242318
# assert that Q, F, and L are the same.
243319
FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj)
320+
FIM_indx, Q_indx, L_indx, sigma_inv_indx = get_FIM_Q_L(
321+
doe_obj=indx_param_doe_obj
322+
)
244323

245324
# Since Trace is used, no comparison for FIM and L.T @ L
246325

247326
# Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM)
248327
self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)))
328+
self.assertTrue(
329+
np.all(np.isclose(FIM_indx, Q_indx.T @ sigma_inv_indx @ Q_indx))
330+
)
249331

250332
@unittest.skipIf(not pandas_available, "pandas is not available")
251333
def test_rooney_biegler_obj_det_solve(self):
@@ -254,27 +336,43 @@ def test_rooney_biegler_obj_det_solve(self):
254336

255337
# Use RooneyBiegler for algorithm validation (faster)
256338
experiment = get_rooney_biegler_experiment()
339+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
257340

258341
DoE_args = get_standard_args(experiment, fd_method, obj_used)
342+
indx_param_DoE_args = get_standard_args(
343+
indx_param_experiment, fd_method, obj_used
344+
)
345+
259346
DoE_args["scale_nominal_param_value"] = (
260347
False # Vanilla determinant solve needs this
261348
)
262349
DoE_args["_Cholesky_option"] = False
263350
DoE_args["_only_compute_fim_lower"] = False
264351

352+
indx_param_DoE_args["scale_nominal_param_value"] = False
353+
indx_param_DoE_args["_Cholesky_option"] = False
354+
indx_param_DoE_args["_only_compute_fim_lower"] = False
355+
265356
doe_obj = DesignOfExperiments(**DoE_args)
357+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
266358

267359
# Increase numerical performance by adding a prior
268360
prior_FIM = doe_obj.compute_FIM()
361+
prior_FIM_indx = indx_param_doe_obj.compute_FIM()
269362
doe_obj.prior_FIM = prior_FIM
363+
indx_param_doe_obj.prior_FIM = prior_FIM_indx
270364

271365
doe_obj.run_doe()
366+
indx_param_doe_obj.run_doe()
272367

273368
self.assertEqual(doe_obj.results["Solver Status"], "ok")
369+
self.assertEqual(indx_param_doe_obj.results["Solver Status"], "ok")
274370

275371
expected_design = 9.999213890476453
276372
actual_design = doe_obj.results["Experiment Design"][0]
373+
actual_design_indx = indx_param_doe_obj.results["Experiment Design"][0]
277374
self.assertAlmostEqual(actual_design, expected_design, places=3)
375+
self.assertAlmostEqual(actual_design_indx, expected_design, places=3)
278376

279377
@unittest.skipIf(not pandas_available, "pandas is not available")
280378
def test_rooney_biegler_obj_cholesky_solve(self):
@@ -283,29 +381,50 @@ def test_rooney_biegler_obj_cholesky_solve(self):
283381

284382
# Use RooneyBiegler for algorithm validation (faster)
285383
experiment = get_rooney_biegler_experiment()
384+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
286385

287386
DoE_args = get_standard_args(experiment, fd_method, obj_used)
387+
indx_param_DoE_args = get_standard_args(
388+
indx_param_experiment, fd_method, obj_used
389+
)
288390

289391
# Add prior FIM for better numerical conditioning
290392
# This follows the pattern in rooney_biegler_doe_example.py
291393
doe_obj_prior = DesignOfExperiments(**DoE_args)
394+
indx_doe_obj_prior = DesignOfExperiments(**indx_param_DoE_args)
292395
prior_FIM = doe_obj_prior.compute_FIM()
396+
prior_FIM_indx = indx_doe_obj_prior.compute_FIM()
293397
DoE_args['prior_FIM'] = prior_FIM
398+
indx_param_DoE_args['prior_FIM'] = prior_FIM_indx
294399

295400
doe_obj = DesignOfExperiments(**DoE_args)
401+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
296402

297403
doe_obj.run_doe()
404+
indx_param_doe_obj.run_doe()
298405

299406
self.assertEqual(doe_obj.results["Solver Status"], "ok")
407+
self.assertEqual(indx_param_doe_obj.results["Solver Status"], "ok")
300408

301409
# assert that Q, F, and L are the same.
302410
FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj)
411+
FIM_indx, Q_indx, L_indx, sigma_inv_indx = get_FIM_Q_L(
412+
doe_obj=indx_param_doe_obj
413+
)
303414

304415
# Since Cholesky is used, there is comparison for FIM and L.T @ L
305416
self.assertTrue(np.all(np.isclose(FIM, L @ L.T)))
417+
self.assertTrue(np.all(np.isclose(FIM_indx, L_indx @ L_indx.T)))
306418

307419
# Note: When using prior_FIM, the relationship FIM = Q.T @ sigma_inv @ Q + prior_FIM
308420
self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q + prior_FIM)))
421+
self.assertTrue(
422+
np.all(
423+
np.isclose(
424+
FIM_indx, Q_indx.T @ sigma_inv_indx @ Q_indx + prior_FIM_indx
425+
)
426+
)
427+
)
309428

310429
def DISABLE_test_reactor_obj_cholesky_solve_bad_prior(self):
311430
# [10/2025] This test has been disabled because it frequently
@@ -349,10 +468,15 @@ def test_compute_FIM_seq_centr(self):
349468

350469
# Use RooneyBiegler for algorithm validation (faster)
351470
experiment = get_rooney_biegler_experiment()
471+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
352472

353473
DoE_args = get_standard_args(experiment, fd_method, obj_used)
474+
indx_param_DoE_args = get_standard_args(
475+
indx_param_experiment, fd_method, obj_used
476+
)
354477

355478
doe_obj = DesignOfExperiments(**DoE_args)
479+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
356480

357481
expected_FIM = np.array(
358482
[[18957.7788694, 4238.27606876], [4238.27606876, 947.52577076]]
@@ -361,6 +485,13 @@ def test_compute_FIM_seq_centr(self):
361485
self.assertTrue(
362486
np.all(np.isclose(doe_obj.compute_FIM(method="sequential"), expected_FIM))
363487
)
488+
self.assertTrue(
489+
np.all(
490+
np.isclose(
491+
indx_param_doe_obj.compute_FIM(method="sequential"), expected_FIM
492+
)
493+
)
494+
)
364495

365496
# This test ensure that compute FIM runs without error using the
366497
# `sequential` option with forward finite differences
@@ -371,12 +502,18 @@ def test_compute_FIM_seq_forward(self):
371502

372503
# Use RooneyBiegler for algorithm validation (faster)
373504
experiment = get_rooney_biegler_experiment()
505+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
374506

375507
DoE_args = get_standard_args(experiment, fd_method, obj_used)
508+
indx_param_DoE_args = get_standard_args(
509+
indx_param_experiment, fd_method, obj_used
510+
)
376511

377512
doe_obj = DesignOfExperiments(**DoE_args)
513+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
378514

379515
doe_obj.compute_FIM(method="sequential")
516+
indx_param_doe_obj.compute_FIM(method="sequential")
380517

381518
# This test ensure that compute FIM runs without error using the
382519
# `kaug` option. kaug computes the FIM directly so no finite difference
@@ -390,10 +527,15 @@ def test_compute_FIM_kaug(self):
390527
obj_used = "determinant"
391528

392529
experiment = get_rooney_biegler_experiment()
530+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
393531

394532
DoE_args = get_standard_args(experiment, fd_method, obj_used)
533+
indx_param_DoE_args = get_standard_args(
534+
indx_param_experiment, fd_method, obj_used
535+
)
395536

396537
doe_obj = DesignOfExperiments(**DoE_args)
538+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
397539

398540
expected_FIM = np.array(
399541
[[18957.7788694, 4238.27606876], [4238.27606876, 947.52577076]]
@@ -402,6 +544,11 @@ def test_compute_FIM_kaug(self):
402544
self.assertTrue(
403545
np.all(np.isclose(doe_obj.compute_FIM(method="kaug"), expected_FIM))
404546
)
547+
self.assertTrue(
548+
np.all(
549+
np.isclose(indx_param_doe_obj.compute_FIM(method="kaug"), expected_FIM)
550+
)
551+
)
405552

406553
# This test ensure that compute FIM runs without error using the
407554
# `sequential` option with backward finite differences
@@ -412,12 +559,18 @@ def test_compute_FIM_seq_backward(self):
412559

413560
# Use RooneyBiegler for algorithm validation (faster)
414561
experiment = get_rooney_biegler_experiment()
562+
indx_param_experiment = get_rooney_biegler_experiment(index_vars=True)
415563

416564
DoE_args = get_standard_args(experiment, fd_method, obj_used)
565+
indx_param_DoE_args = get_standard_args(
566+
indx_param_experiment, fd_method, obj_used
567+
)
417568

418569
doe_obj = DesignOfExperiments(**DoE_args)
570+
indx_param_doe_obj = DesignOfExperiments(**indx_param_DoE_args)
419571

420572
doe_obj.compute_FIM(method="sequential")
573+
indx_param_doe_obj.compute_FIM(method="sequential")
421574

422575
@unittest.skipIf(not pandas_available, "pandas is not available")
423576
def test_reactor_grid_search(self):

0 commit comments

Comments
 (0)