@@ -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
86148def 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
135191def 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