Repository: Pyomo/pyomo

Issue:
Return ContinuousSet values from parmest Estimator object using return_values option in theta_est()
## Summary
It is currently not possible to obtain `ContinuousSet` values of a Pyomo.DAE model using the `return_values` argument in `theta_est()`


<!-- Please add a concise summary of your suggestion here. -->

### Rationale

Adding this feature enables the user to access the continuous variable from a Pyomo.DAE model following parameter estimation using multiple scenarios (dataset) with `parmest`

<!-- Is your feature request related to a problem? Please describe it! -->

### Description


<!-- Describe the solution you'd like and the alternatives you have considered. -->


### Additional information
<!-- Add any other context about the feature request here. -->



<!-- If you want to ask a question (how to use Pyomo, what it can do, etc.), please refer to our [online documentation](https://github.com/Pyomo/pyomo#getting-help) on ways to get help. -->



Discussion:

Resolving patch:
diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py
--- a/pyomo/contrib/parmest/parmest.py
+++ b/pyomo/contrib/parmest/parmest.py
@@ -49,6 +49,7 @@
 
 import pyomo.contrib.parmest.utils as utils
 import pyomo.contrib.parmest.graphics as graphics
+from pyomo.dae import ContinuousSet
 
 parmest_available = numpy_available & pandas_available & scipy_available
 
@@ -468,7 +469,7 @@ def _Q_opt(self, ThetaVals=None, solver="ef_ipopt",
                     for key in self.solver_options:
                         solver.options[key] = self.solver_options[key]
 
-                solve_result = solver.solve(ef, tee = self.tee)
+                solve_result = solver.solve(self.ef_instance, tee = self.tee)
 
             # The import error will be raised when we attempt to use
             # inv_reduced_hessian_barrier below.
@@ -533,13 +534,21 @@ def _Q_opt(self, ThetaVals=None, solver="ef_ipopt",
 
             if len(return_values) > 0:
                 var_values = []
-                for exp_i in self.ef_instance.component_objects(Block, descend_into=False):
+                if len(scen_names) > 1:  # multiple scenarios
+                    block_objects = self.ef_instance.component_objects(Block, descend_into=False)
+                else:  # single scenario
+                    block_objects = [self.ef_instance]
+                for exp_i in block_objects:
                     vals = {}
                     for var in return_values:
                         exp_i_var = exp_i.find_component(str(var))
                         if exp_i_var is None:  # we might have a block such as _mpisppy_data
                             continue
-                        temp = [pyo.value(_) for _ in exp_i_var.values()]
+                        # if value to return is ContinuousSet
+                        if type(exp_i_var) == ContinuousSet:
+                            temp = list(exp_i_var)
+                        else:
+                            temp = [pyo.value(_) for _ in exp_i_var.values()]
                         if len(temp) == 1:
                             vals[var] = temp[0]
                         else:


Test patch:
diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py
--- a/pyomo/contrib/parmest/tests/test_parmest.py
+++ b/pyomo/contrib/parmest/tests/test_parmest.py
@@ -636,6 +636,10 @@ def total_cost_rule(model):
         self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names)
         self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names)
 
+        # Estimator object with multiple scenarios
+        self.pest_df_multiple = parmest.Estimator(ABC_model, [data_df,data_df], theta_names)
+        self.pest_dict_multiple = parmest.Estimator(ABC_model, [data_dict,data_dict], theta_names)
+
         # Create an instance of the model
         self.m_df = ABC_model(data_df)
         self.m_dict = ABC_model(data_dict)
@@ -649,6 +653,24 @@ def test_dataformats(self):
         self.assertAlmostEqual(obj1, obj2, places=6)
         self.assertAlmostEqual(theta1['k1'], theta2['k1'], places=6)
         self.assertAlmostEqual(theta1['k2'], theta2['k2'], places=6)
+    
+    def test_return_continuous_set(self):
+        '''
+        test if ContinuousSet elements are returned correctly from theta_est()
+        '''
+        obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=['time'])
+        obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=['time'])
+        self.assertAlmostEqual(return_vals1['time'].loc[0][18],2.368, places=3)
+        self.assertAlmostEqual(return_vals2['time'].loc[0][18],2.368, places=3)
+    
+    def test_return_continuous_set_multiple_datasets(self):
+        '''
+        test if ContinuousSet elements are returned correctly from theta_est()
+        '''
+        obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est(return_values=['time'])
+        obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est(return_values=['time'])
+        self.assertAlmostEqual(return_vals1['time'].loc[1][18],2.368, places=3)
+        self.assertAlmostEqual(return_vals2['time'].loc[1][18],2.368, places=3)
 
     def test_covariance(self):
 
@@ -677,8 +699,7 @@ def test_covariance(self):
         self.assertTrue(cov.loc['k1', 'k1'] > 0)
         self.assertTrue(cov.loc['k2', 'k2'] > 0)
         self.assertAlmostEqual(cov_diff, 0, places=6)
-
-
+        
 @unittest.skipIf(not parmest.parmest_available,
                  "Cannot test parmest: required dependencies are missing")
 @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")