Repository: Pyomo/pyomo

Issue:
pyomo/contrib/parmest/ Invalid index error for component
Hi

I'm trying to estimate the parameter 'Boiler[1].eta' with paramest.py. My theta_names looks like this: theta_names = ['Boiler[1].eta'].

Unfortunately i get this error message:
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/pyomo/pysp/phutils.py", line 482, in extractComponentIndices
    % (str(tuple(index_template)), component.name))
ValueError: The index ('1].eta',) is not valid for component named: Boiler

If i pause the program at the end of the execution of my model_function and execute the following line:
model.Boiler[1].eta 
I get this: 
<pyomo.core.base.var.SimpleVar at 0x12090e748>
So the variable actually exists in my model.

Is this a Bug or did i do something wrong? 
The index ('1].eta',) looks also a bit chopped up. Maybe there is a Problem?

Greetings
Jakob


Discussion:
It may take a few days to look into this.

On Thu, Mar 12, 2020 at 3:55 PM Jakob <notifications@github.com> wrote:

> Hi
>
> I'm trying to estimate the parameter 'Boiler[1].eta' with paramest.py. My
> theta_names looks like this: theta_names = ['Boiler[1].eta'].
>
> Unfortunately i get this error message:
> File
> "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/pyomo/pysp/phutils.py",
> line 482, in extractComponentIndices
> % (str(tuple(index_template)), component.name))
> ValueError: The index ('1].eta',) is not valid for component named: Boiler
>
> If i pause the program at the end of the execution of my model_function
> and execute the following line:
> model.Boiler[1].eta
> I get this:
> <pyomo.core.base.var.SimpleVar at 0x12090e748>
> So the variable actually exists in my model.
>
> Is this a Bug or did i do something wrong?
> The index ('1].eta',) looks also a bit chopped up. Maybe there is a
> Problem?
>
> Greetings
> Jakob
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/Pyomo/pyomo/issues/1340>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AD4VTBAVCM6PU6LNWCIH7LLRHFR5HANCNFSM4LGWZXCA>
> .
>

Okay! Thank you :)
May i ask how long this will approximately take to fix this bug? 
I had a similar problem. Maybe it has something to do with the block architecture of the model
I created a minimal example for this issue!
```
from matplotlib.pyplot import plot
import matplotlib.pyplot as plt
import pandas as pd

from pyomo.environ import *
import pyomo.contrib.parmest.parmest as parmest


def model_function(data):
    model = AbstractModel()

    model.T = Set(ordered=True)

    model.J = Set(initialize=[1])

    instance = model.create_instance(data={None: {
        'T': {None: [0, 1, 2, 3], 'J': [1]}
    }})

    def boiler_rule(A, u):
        A.eta = Var(bounds=(0, 1), initialize=0.5)
        A.eta.fix()
        A.cost_per_kWh = Param(initialize=1)

        A.P_in = Var(instance.T, within=NonNegativeReals)
        A.P_out = Var(instance.T, within=NonNegativeReals)

        # constraint efficiancy
        def eta_constraint_rule(A, t):
            return A.P_out[t] == A.P_in[t] * A.eta

        A.constraint_eta = Constraint(instance.T, rule=eta_constraint_rule)

    instance.Boiler = Block(instance.J, rule=boiler_rule)
    instance.Boiler.construct()

    instance.demand = Param(instance.T, initialize={0: 0, 1: 600, 2: 900, 3: 1200})
    instance.cost_error = Param(initialize=100)

    def Bilanz_Regel(instance, t):
        return instance.demand[t] == instance.Boiler[1].P_out[t]

    instance.balance = Constraint(instance.T, rule=Bilanz_Regel)

    instance.Zielfunktion = Objective(expr=sum(
        instance.Boiler[1].P_in[t] * instance.Boiler[1].cost_per_kWh * instance.cost_error for t in instance.T))
    return instance


def parameter_estimation():
    """Estimate Parameter Boiler[1].eta"""
    data = {'T': [0, 1, 2, 3],
            'P_in': {0: 0.0, 1: 1200.0, 2: 1800.0, 3: 2400.0},
            'P_out': {0: 0.0, 1: 600.0, 2: 900.0, 3: 1200.0}}
    data = [data]

    # theta_names
    # The third argument is a list of variable names that the user wants to estimate.
    # The list contains strings with Var names from the Pyomo model.
    theta_names = ['Boiler[1].eta']

    # objective_function
    def objective_function(model, data):
        expr = 0
        for t in data['T']:
            # (observed_value - value_calculated_form_model)^2*1/nr_time_steps
            # Achtung: Geht davon aus, dass alle Zeitschritte gleich lang sind.
            # Muss für variable Schrittweite ggf. angepasst werden.
            # P_in
            expr += ((data['P_in'][t] - model.Boiler[1].P_in[t]) ** 2) * (1 / len(data['T']))
            # P_out
            expr += ((data['P_in'][t] - model.Boiler[1].P_out[t]) ** 2) * (1 / len(data['T']))
        return expr

    pest = parmest.Estimator(model_function, data, theta_names, objective_function)
    obj, theta = pest.theta_est()


def solve_model():
    """this represents a normal solve of the model, without parameter estimation."""
    instance = model_function()
    # solver
    result = SolverFactory('glpk').solve(instance, tee=True)
    print(result)

    # plotte Verbrauch, den die Anlagen erfüllen müssen
    plt.figure(1)
    plot(pd.Series(instance.demand.extract_values()), drawstyle='steps-post')
    plt.show();
    print('Demand')

    # plotte die bereitgestellte Leistung P_out der Anlagen 1(blau) und 2(orange)
    # der Verbrauch ist noch mal gepunktet dargestellt
    plt.figure(2)
    plot(pd.Series(instance.Boiler[1].P_out.extract_values()), drawstyle='steps-post')

    plot(pd.Series(instance.demand.extract_values()), 'k:', drawstyle='steps-post')
    plt.legend(['Boiler 1', 'demand'])
    plt.show();

    print(instance.Boiler[1].P_in.extract_values())
    print(instance.Boiler[1].P_out.extract_values())


# solve_model([])
parameter_estimation()
```
This particular error message is created by Pyomo and has to do with how you are trying to create a ConcreteModel starting from an abstract model. It is not a bug in parmest. In this case, I recommend changing to a ConcreteModel from the start, e.g.,
def model_function():
    instance= ConcreteModel()
    instance.T = Set(ordered=True, initialize=[0,1,2,3])
    instance.J = Set(initialize=[1])

After you fix that, there are a lot of other troubles (e.g., parmest wants to pass in the data, but you have it all hard-wired). If you need help, you should write to the Pyomo forum or send me an email directly, rather than opening an issue, though.
OK. There is a bug in Parmest concerning Vars on blocks and I want to do a major revision to take
care of it. I will start on it today, but it will take a few days.

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
@@ -10,11 +10,13 @@
 
 import re
 import importlib as im
+import logging
 import types
 import json
 from itertools import combinations
 
 from pyomo.common.dependencies import (
+    attempt_import,
     numpy as np, numpy_available,
     pandas as pd, pandas_available,
     scipy, scipy_available,
@@ -23,9 +25,11 @@
 import pyomo.environ as pyo
 import pyomo.pysp.util.rapper as st
 from pyomo.pysp.scenariotree import tree_structure
-from pyomo.pysp.scenariotree.tree_structure_model import CreateAbstractScenarioTreeModel
+from pyomo.pysp.scenariotree.tree_structure_model import (
+    CreateAbstractScenarioTreeModel
+)
 from pyomo.opt import SolverFactory
-from pyomo.environ import Block
+from pyomo.environ import Block, ComponentUID
 
 import pyomo.contrib.parmest.mpi_utils as mpiu
 import pyomo.contrib.parmest.ipopt_solver_wrapper as ipopt_solver_wrapper
@@ -35,14 +39,10 @@
 
 parmest_available = numpy_available & pandas_available & scipy_available
 
-if numpy_available and scipy_available:
-    from pyomo.contrib.pynumero.asl import AmplInterface
-    asl_available = AmplInterface.available()
-else:
-    asl_available = False
+inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import(
+    'pyomo.contrib.interior_point.inverse_reduced_hessian')
 
-if asl_available:
-    from pyomo.contrib.interior_point.inverse_reduced_hessian import inv_reduced_hessian_barrier
+logger = logging.getLogger(__name__)
 
 __version__ = 0.1
 
@@ -375,12 +375,28 @@ def _create_parmest_model(self, data):
         if (len(self.theta_names) == 1) and (self.theta_names[0] == 'parmest_dummy_var'):
             model.parmest_dummy_var = pyo.Var(initialize = 1.0)
             
-        for theta in self.theta_names:
-            try:
-                var_validate = eval('model.'+theta)
-                var_validate.fixed = False
-            except:
-                print(theta +' is not a variable')
+        for i, theta in enumerate(self.theta_names):
+            # First, leverage the parser in ComponentUID to locate the
+            # component.  If that fails, fall back on the original
+            # (insecure) use of 'eval'
+            var_cuid = ComponentUID(theta)
+            var_validate = var_cuid.find_component_on(model)
+            if var_validate is None:
+                logger.warning(
+                    "theta_name[%s] (%s) was not found on the model",
+                    (i, theta))
+            else:
+                try:
+                    # If the component that was found is not a variable,
+                    # this will generate an exception (and the warning
+                    # in the 'except')
+                    var_validate.fixed = False
+                    # We want to standardize on the CUID string
+                    # representation (which is what PySP will use
+                    # internally)
+                    self.theta_names[i] = repr(var_cuid)
+                except:
+                    logger.warning(theta + ' is not a variable')
         
         if self.obj_function:
             for obj in model.component_objects(Objective):
@@ -510,8 +526,12 @@ def _Q_opt(self, ThetaVals=None, solver="ef_ipopt",
                 else:
                     solve_result = solver.solve(self.ef_instance, tee = self.tee)
 
-            elif not asl_available:
-                raise ImportError("parmest requires ASL to calculate the covariance matrix with solver 'ipopt'")
+            # The import error will be raised when we attempt to use
+            # inv_reduced_hessian_barrier below.
+            #
+            #elif not asl_available:
+            #    raise ImportError("parmest requires ASL to calculate the "
+            #                      "covariance matrix with solver 'ipopt'")
             else:
                 # parmest makes the fitted parameters stage 1 variables
                 # thus we need to convert from var names (string) to 
@@ -523,10 +543,12 @@ def _Q_opt(self, ThetaVals=None, solver="ef_ipopt",
                     ind_vars.append(self.ef_instance.MASTER_BLEND_VAR_RootNode[v])
         
                 # calculate the reduced hessian
-                solve_result, inv_red_hes = inv_reduced_hessian_barrier(self.ef_instance, 
-                    independent_variables= ind_vars,
-                    solver_options=self.solver_options,
-                    tee=self.tee)
+                solve_result, inv_red_hes = \
+                    inverse_reduced_hessian.inv_reduced_hessian_barrier(
+                        self.ef_instance,
+                        independent_variables= ind_vars,
+                        solver_options=self.solver_options,
+                        tee=self.tee)
             
             # Extract solution from pysp
             stsolver.scenario_tree.pullScenarioSolutionsFromInstances()
@@ -573,7 +595,7 @@ def _Q_opt(self, ThetaVals=None, solver="ef_ipopt",
                 for exp_i in self.ef_instance.component_objects(Block, descend_into=False):
                     vals = {}
                     for var in return_values:
-                        exp_i_var = eval('exp_i.'+ str(var))
+                        exp_i_var = exp_i.find_component(str(var))
                         temp = [pyo.value(_) for _ in exp_i_var.itervalues()]
                         if len(temp) == 1:
                             vals[var] = temp[0]
diff --git a/pyomo/pysp/util/rapper.py b/pyomo/pysp/util/rapper.py
--- a/pyomo/pysp/util/rapper.py
+++ b/pyomo/pysp/util/rapper.py
@@ -6,6 +6,7 @@
 """
 
 import inspect
+from pyomo.core.base.component import _name_index_generator
 from pyomo.environ import SolverFactory
 from pyomo.pysp.scenariotree.instance_factory \
     import ScenarioTreeInstanceFactory
@@ -52,7 +53,7 @@ def dointpair(pho, fo):
                     float(phopts["--scenario-tree-downsample-fraction"])
         else:
             kwargs['downsample_fraction'] = None
-            
+
         if "--scenario-bundle-specification" in phopts:
             kwargs['bundles'] = phopts["--scenario-tree-bundle-specification"]
         else:
@@ -67,19 +68,19 @@ class StochSolver:
     abstract models.
     Inspired by the IDAES use case and by daps ability to create tree models.
     Author: David L. Woodruff, February 2017
-    
-    Args: 
-      fsfile (str): is a path to the file that contains the scenario 
+
+    Args:
+      fsfile (str): is a path to the file that contains the scenario
                     callback for concrete or the reference model for abstract.
-      fsfct (str, or fct, or None): 
+      fsfct (str, or fct, or None):
          |  str:   callback function name in the file
          |  fct:   callback function (fsfile is ignored)
          |  None:  it is a AbstractModel
-      tree_model (concrete model, or networkx tree, or path): 
+      tree_model (concrete model, or networkx tree, or path):
         gives the tree as a concrete model (which could be a fct)
         or a valid networkx scenario tree
         or path to AMPL data file.
-      phopts: dictionary of ph options; needed during construction 
+      phopts: dictionary of ph options; needed during construction
               if there is bundling.
 
     Attributes:
@@ -109,7 +110,7 @@ def __init__(self, fsfile,
                 print ("ERROR in StochSolver called from",inspect.stack()[1][3])
                 raise RuntimeError("fsfct is None, so assuming",
                       "AbstractModel but could not find all ingredients.")
-                
+
         else:  # concrete model
             if  callable(fsfct):
                 scen_function = fsfct
@@ -130,7 +131,7 @@ def __init__(self, fsfile,
 
                 scenario_instance_factory = ScenarioTreeInstanceFactory(scen_function, tree_model)
 
-            else: 
+            else:
                 # DLW March 21: still not correct
                 scenario_instance_factory = \
                     ScenarioTreeInstanceFactory(scen_function, tree_model)
@@ -141,7 +142,7 @@ def __init__(self, fsfile,
                 scenario_instance_factory.generate_scenario_tree(**kwargs) #verbose = True)
             instances = scenario_instance_factory. \
                         construct_instances_for_scenario_tree(self.scenario_tree)
-            self.scenario_tree.linkInInstances(instances)        
+            self.scenario_tree.linkInInstances(instances)
 
     #=========================
     def make_ef(self,
@@ -152,7 +153,7 @@ def make_ef(self,
                 cc_indicator_var_name=None,
                 cc_alpha=0.0):
         """ Make an ef object (used by solve_ef); all Args are optional.
-        
+
         Args:
             verbose (boolean): indicates verbosity to PySP for construction
             generate_weighted_cvar (boolean): indicates we want weighted CVar
@@ -171,7 +172,7 @@ def make_ef(self,
                                          risk_alpha = risk_alpha,
                                          cc_indicator_var_name = cc_indicator_var_name,
                                          cc_alpha = cc_alpha)
-    
+
     def solve_ef(self, subsolver, sopts = None, tee = False, need_gap = False,
                  verbose=False,
                  generate_weighted_cvar = False,
@@ -182,7 +183,7 @@ def solve_ef(self, subsolver, sopts = None, tee = False, need_gap = False,
 
         """Solve the stochastic program directly using the extensive form.
         All Args other than subsolver are optional.
-       
+
         Args:
             subsolver (str): the solver to call (e.g., 'ipopt')
             sopts (dict):  solver options
@@ -208,7 +209,7 @@ def solve_ef(self, subsolver, sopts = None, tee = False, need_gap = False,
            This needs more work to deal with solver failure (dlw, March, 2018)
 
         """
-        
+
         self.ef_instance = self.make_ef(verbose=verbose,
                                         generate_weighted_cvar = generate_weighted_cvar,
                                         cvar_weight = cvar_weight,
@@ -253,7 +254,7 @@ def solve_ph(self, subsolver, default_rho, phopts = None, sopts = None):
         Returns: the ph object
 
         Note:
-            Updates the scenario tree, populated with the xbar values; 
+            Updates the scenario tree, populated with the xbar values;
             however, you probably want to do
             obj, xhat = ph.compute_and_report_inner_bound_using_xhat()
             where ph is the return value.
@@ -268,13 +269,13 @@ def solve_ph(self, subsolver, default_rho, phopts = None, sopts = None):
         phargslist.append('--solver')
         phargslist.append(str(subsolver))
         phargslist = _optiondict_2_list(phopts, args_list = phargslist)
-                    
+
         # Subproblem options go to PH as space-delimited, equals-separated pairs.
         if sopts is not None:
             soptstring = ""
             for key in sopts:
                 soptstring += key + '=' + str(sopts[key]) + ' '
-            phargslist.append('--scenario-solver-options')    
+            phargslist.append('--scenario-solver-options')
             phargslist.append(soptstring)
         phoptions = parser.parse_args(phargslist)
 
@@ -307,7 +308,7 @@ def root_Var_solution(self):
             var_name, index = root_node._variable_ids[variable_id]
             name = var_name
             if index is not None:
-                name += "["+str(index)+"]"
+                name += _name_index_generator(index)
             yield name, root_node._solution[variable_id]
 
     #=========================


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
@@ -18,7 +18,6 @@
     pandas as pd, pandas_available,
     scipy, scipy_available,
 )
-imports_present = numpy_available & pandas_available & scipy_available
 
 import platform
 is_osx = platform.mac_ver()[0] != ''
@@ -37,12 +36,6 @@
 from pyomo.opt import SolverFactory
 ipopt_available = SolverFactory('ipopt').available()
 
-if numpy_available:
-    from pyomo.contrib.pynumero.asl import AmplInterface
-    asl_available = AmplInterface.available()
-else:
-    asl_available=False
-
 testdir = os.path.dirname(os.path.abspath(__file__))
 
 class Object_from_string_Tester(unittest.TestCase):
@@ -51,46 +44,46 @@ def setUp(self):
         self.instance.IDX = pyo.Set(initialize=['a', 'b', 'c'])
         self.instance.x = pyo.Var(self.instance.IDX, initialize=1134)
         # TBD add a block
-        if imports_present:
+        if parmest.parmest_available:
             np.random.seed(1134)
-        
+
     def tearDown(self):
         pass
-    
+
     def test_Var(self):
         # just making sure it executes
         pyo_Var_obj = parmest._object_from_string(self.instance, "x[b]")
         fixstatus = pyo_Var_obj.fixed
         self.assertEqual(fixstatus, False)
 
-        
+
 @unittest.skipIf(not parmest.parmest_available,
                  "Cannot test parmest: required dependencies are missing")
 @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")
 class parmest_object_Tester_RB(unittest.TestCase):
-    
+
     def setUp(self):
         from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model
-           
+
         data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0],
                                   [4,16.0],[5,15.6],[6,19.8]], columns=['hour', 'y'])
-        
+
         theta_names = ['asymptote', 'rate_constant']
-        
-        def SSE(model, data):  
+
+        def SSE(model, data):
             expr = sum((data.y[i] - model.response_function[data.hour[i]])**2 for i in data.index)
             return expr
 
         solver_options = {
                 'tol': 1e-8,
                 }
-        
+
         self.pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE,
                 solver_options=solver_options)
 
     def test_theta_est(self):
         objval, thetavals = self.pest.theta_est()
-        
+
         self.assertAlmostEqual(objval, 4.4675, places=2)
         self.assertAlmostEqual(thetavals['asymptote'], 19.2189, places=2) # 19.1426 from the paper
         self.assertAlmostEqual(thetavals['rate_constant'], 0.5312, places=2) # 0.5311 from the paper
@@ -99,65 +92,65 @@ def test_theta_est(self):
                      "parmest.graphics imports are unavailable")
     def test_bootstrap(self):
         objval, thetavals = self.pest.theta_est()
-        
+
         num_bootstraps=10
         theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True)
-        
+
         num_samples = theta_est['samples'].apply(len)
         self.assertTrue(len(theta_est.index), 10)
         self.assertTrue(num_samples.equals(pd.Series([6]*10)))
 
         del theta_est['samples']
-        
+
         # apply cofidence region test
         CR = self.pest.confidence_region_test(theta_est, 'MVN', [0.5, 0.75, 1.0])
-        
+
         self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0]))
-        self.assertTrue(CR[0.5].sum() == 5) 
+        self.assertTrue(CR[0.5].sum() == 5)
         self.assertTrue(CR[0.75].sum() == 7)
         self.assertTrue(CR[1.0].sum() == 10) # all true
-        
+
         graphics.pairwise_plot(theta_est)
         graphics.pairwise_plot(theta_est, thetavals)
         graphics.pairwise_plot(theta_est, thetavals, 0.8, ['MVN', 'KDE', 'Rect'])
-        
+
     @unittest.skipIf(not graphics.imports_available,
                      "parmest.graphics imports are unavailable")
     def test_likelihood_ratio(self):
         objval, thetavals = self.pest.theta_est()
-        
+
         asym = np.arange(10, 30, 2)
         rate = np.arange(0, 1.5, 0.25)
         theta_vals = pd.DataFrame(list(product(asym, rate)), columns=self.pest.theta_names)
-        
+
         obj_at_theta = self.pest.objective_at_theta(theta_vals)
-        
+
         LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0])
-        
+
         self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0]))
-        self.assertTrue(LR[0.8].sum() == 7) 
+        self.assertTrue(LR[0.8].sum() == 7)
         self.assertTrue(LR[0.9].sum() == 11)
         self.assertTrue(LR[1.0].sum() == 60) # all true
-        
+
         graphics.pairwise_plot(LR, thetavals, 0.8)
 
     def test_leaveNout(self):
-        lNo_theta = self.pest.theta_est_leaveNout(1) 
+        lNo_theta = self.pest.theta_est_leaveNout(1)
         self.assertTrue(lNo_theta.shape == (6,2))
-        
+
         results = self.pest.leaveNout_bootstrap_test(1, None, 3, 'Rect', [0.5, 1.0])
         self.assertTrue(len(results) == 6) # 6 lNo samples
         i = 1
         samples = results[i][0] # list of N samples that are left out
-        lno_theta = results[i][1] 
-        bootstrap_theta = results[i][2] 
+        lno_theta = results[i][1]
+        bootstrap_theta = results[i][2]
         self.assertTrue(samples == [1]) # sample 1 was left out
         self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1
         self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0]))
         self.assertTrue(lno_theta[1.0].sum() == 1) # all true
         self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1
         self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true
-        
+
     def test_diagnostic_mode(self):
         self.pest.diagnostic_mode = True
 
@@ -166,7 +159,7 @@ def test_diagnostic_mode(self):
         asym = np.arange(10, 30, 2)
         rate = np.arange(0, 1.5, 0.25)
         theta_vals = pd.DataFrame(list(product(asym, rate)), columns=self.pest.theta_names)
-        
+
         obj_at_theta = self.pest.objective_at_theta(theta_vals)
 
         self.pest.diagnostic_mode = False
@@ -186,7 +179,7 @@ def test_rb_main(self):
         else:
             retcode = subprocess.call([sys.executable, rbpath])
         assert(retcode == 0)
-        
+
     @unittest.skip("Presently having trouble with mpiexec on appveyor")
     def test_parallel_parmest(self):
         """ use mpiexec and mpi4py """
@@ -210,7 +203,7 @@ def test_theta_k_aug_for_Hessian(self):
         # this will fail if k_aug is not installed
         objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug")
         self.assertAlmostEqual(objval, 4.4675, places=2)
-        
+
 
 '''
 The test cases above were developed with a transcription mistake in the dataset.
@@ -220,42 +213,43 @@ def test_theta_k_aug_for_Hessian(self):
                  "Cannot test parmest: required dependencies are missing")
 @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")
 class parmest_object_Tester_RB_match_paper(unittest.TestCase):
-    
+
     def setUp(self):
         from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model
-           
+
         data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0],
                                   [4,16.0],[5,15.6],[7,19.8]], columns=['hour', 'y'])
-        
+
         theta_names = ['asymptote', 'rate_constant']
-        
-        def SSE(model, data):  
+
+        def SSE(model, data):
             expr = sum((data.y[i] - model.response_function[data.hour[i]])**2 for i in data.index)
             return expr
-        
+
         self.pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE)
-    
+
     def test_theta_est(self):
         objval, thetavals = self.pest.theta_est(calc_cov=False)
-        
+
         self.assertAlmostEqual(objval, 4.3317112, places=2)
         self.assertAlmostEqual(thetavals['asymptote'], 19.1426, places=2) # 19.1426 from the paper
         self.assertAlmostEqual(thetavals['rate_constant'], 0.5311, places=2) # 0.5311 from the paper
-    
-    @unittest.skipIf(not asl_available, "Cannot test covariance matrix: required ASL dependency is missing")
+
+    @unittest.skipIf(not parmest.inverse_reduced_hessian_available,
+                     "Cannot test covariance matrix: required ASL dependency is missing")
     def test_theta_est_cov(self):
         objval, thetavals, cov = self.pest.theta_est(calc_cov=True)
-        
+
         self.assertAlmostEqual(objval, 4.3317112, places=2)
         self.assertAlmostEqual(thetavals['asymptote'], 19.1426, places=2) # 19.1426 from the paper
         self.assertAlmostEqual(thetavals['rate_constant'], 0.5311, places=2) # 0.5311 from the paper
-        
+
         # Covariance matrix
         self.assertAlmostEqual(cov[0,0], 6.30579403, places=2) # 6.22864 from paper
         self.assertAlmostEqual(cov[0,1], -0.4395341, places=2) # -0.4322 from paper
         self.assertAlmostEqual(cov[1,0], -0.4395341, places=2) # -0.4322 from paper
         self.assertAlmostEqual(cov[1,1], 0.04193591, places=2) # 0.04124 from paper
-        
+
         ''' Why does the covariance matrix from parmest not match the paper? Parmest is
         calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely
         employed the first order approximation common for nonlinear regression. The paper
@@ -263,18 +257,101 @@ def test_theta_est_cov(self):
         The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in
         "Nonlinear Parameter Estimation", Y. Bard, 1974.
         '''
-        
-    
-        
 
-@unittest.skipIf(not imports_present, "Cannot test parmest: required dependencies are missing")
-@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available")
+
+@unittest.skipIf(not parmest.parmest_available,
+                 "Cannot test parmest: required dependencies are missing")
+@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available")
+class Test_parmest_indexed_variables(unittest.TestCase):
+
+    def make_model(self, theta_names):
+
+        data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0],
+                                  [4,16.0],[5,15.6],[7,19.8]], columns=['hour', 'y'])
+
+        def rooney_biegler_model_alternate(data):
+            ''' Alternate model definition used in a unit test
+            Here, the fitted parameters are defined as a single variable over a set
+            A bit silly for this specific example
+            '''
+
+            model = pyo.ConcreteModel()
+
+            model.var_names = pyo.Set(initialize=['asymptote','rate_constant'])
+
+            model.theta = pyo.Var(model.var_names, initialize={'asymptote':15, 'rate_constant':0.5})
+
+            def response_rule(m, h):
+                expr = m.theta['asymptote'] * (1 - pyo.exp(-m.theta['rate_constant'] * h))
+                return expr
+            model.response_function = pyo.Expression(data.hour, rule = response_rule)
+
+            def SSE_rule(m):
+                return sum((data.y[i] - m.response_function[data.hour[i]])**2 for i in data.index)
+            model.SSE = pyo.Objective(rule = SSE_rule, sense=pyo.minimize)
+
+            return model
+
+        def SSE(model, data):
+            expr = sum((data.y[i] - model.response_function[data.hour[i]])**2 for i in data.index)
+            return expr
+
+        return parmest.Estimator(rooney_biegler_model_alternate, data, theta_names, SSE)
+
+    def test_theta_est_quotedIndex(self):
+
+        theta_names = ["theta['asymptote']", "theta['rate_constant']"]
+
+        pest = self.make_model(theta_names)
+        objval, thetavals = pest.theta_est(calc_cov=False)
+
+        self.assertAlmostEqual(objval, 4.3317112, places=2)
+        self.assertAlmostEqual(thetavals["theta[asymptote]"], 19.1426, places=2) # 19.1426 from the paper
+        self.assertAlmostEqual(thetavals['theta[rate_constant]'], 0.5311, places=2) # 0.5311 from the paper
+
+    def test_theta_est_impliedStrIndex(self):
+
+        theta_names = ["theta[asymptote]", "theta[rate_constant]"]
+
+        pest = self.make_model(theta_names)
+        objval, thetavals = pest.theta_est(calc_cov=False)
+
+        self.assertAlmostEqual(objval, 4.3317112, places=2)
+        self.assertAlmostEqual(thetavals["theta[asymptote]"], 19.1426, places=2) # 19.1426 from the paper
+        self.assertAlmostEqual(thetavals['theta[rate_constant]'], 0.5311, places=2) # 0.5311 from the paper
+
+
+    @unittest.skipIf(
+        not parmest.inverse_reduced_hessian_available,
+        "Cannot test covariance matrix: required ASL dependency is missing")
+    def test_theta_est_cov(self):
+        theta_names = ["theta[asymptote]", "theta[rate_constant]"]
+
+        pest = self.make_model(theta_names)
+        objval, thetavals, cov = pest.theta_est(calc_cov=True)
+
+        self.assertAlmostEqual(objval, 4.3317112, places=2)
+        self.assertAlmostEqual(thetavals["theta[asymptote]"], 19.1426, places=2) # 19.1426 from the paper
+        self.assertAlmostEqual(thetavals['theta[rate_constant]'], 0.5311, places=2) # 0.5311 from the paper
+
+        # Covariance matrix
+        self.assertAlmostEqual(cov[0,0], 6.30579403, places=2) # 6.22864 from paper
+        self.assertAlmostEqual(cov[0,1], -0.4395341, places=2) # -0.4322 from paper
+        self.assertAlmostEqual(cov[1,0], -0.4395341, places=2) # -0.4322 from paper
+        self.assertAlmostEqual(cov[1,1], 0.04193591, places=2) # 0.04124 from paper
+
+
+
+@unittest.skipIf(not parmest.parmest_available,
+                 "Cannot test parmest: required dependencies are missing")
+@unittest.skipIf(not ipopt_available,
+                 "The 'ipopt' solver is not available")
 class parmest_object_Tester_reactor_design(unittest.TestCase):
-    
+
     def setUp(self):
         from pyomo.contrib.parmest.examples.reactor_design.reactor_design import reactor_design_model
-          
-        # Data from the design 
+
+        # Data from the design
         data = pd.DataFrame(data=[[1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5],
                                   [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4],
                                   [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8],
@@ -293,12 +370,12 @@ def setUp(self):
                                   [1.80, 10000, 4392.8, 1056.0,  977.7, 1786.7],
                                   [1.85, 10000, 4442.6, 1052.8,  948.4, 1778.1],
                                   [1.90, 10000, 4491.3, 1049.4,  920.5, 1769.4],
-                                  [1.95, 10000, 4538.8, 1045.8,  893.9, 1760.8]], 
+                                  [1.95, 10000, 4538.8, 1045.8,  893.9, 1760.8]],
                           columns=['sv', 'caf', 'ca', 'cb', 'cc', 'cd'])
 
         theta_names = ['k1', 'k2', 'k3']
-        
-        def SSE(model, data): 
+
+        def SSE(model, data):
             expr = (float(data['ca']) - model.ca)**2 + \
                    (float(data['cb']) - model.cb)**2 + \
                    (float(data['cc']) - model.cc)**2 + \
@@ -306,44 +383,44 @@ def SSE(model, data):
             return expr
 
         solver_options = {"max_iter": 6000}
-        
+
         self.pest = parmest.Estimator(reactor_design_model, data,
                                       theta_names, SSE, solver_options)
 
     def test_theta_est(self):
         objval, thetavals = self.pest.theta_est()
-        
-        self.assertAlmostEqual(thetavals['k1'], 5.0/6.0, places=4) 
-        self.assertAlmostEqual(thetavals['k2'], 5.0/3.0, places=4) 
-        self.assertAlmostEqual(thetavals['k3'], 1.0/6000.0, places=7) 
+
+        self.assertAlmostEqual(thetavals['k1'], 5.0/6.0, places=4)
+        self.assertAlmostEqual(thetavals['k2'], 5.0/3.0, places=4)
+        self.assertAlmostEqual(thetavals['k3'], 1.0/6000.0, places=7)
 
     def test_return_values(self):
         objval, thetavals, data_rec =\
             self.pest.theta_est(return_values=['ca', 'cb', 'cc', 'cd', 'caf'])
         self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3)
 
-        
-        
+
+
 @unittest.skipIf(not parmest.parmest_available,
                  "Cannot test parmest: required dependencies are missing")
 @unittest.skipIf(not graphics.imports_available,
                  "parmest.graphics imports are unavailable")
 @unittest.skipIf(is_osx, "Disabling graphics tests on OSX due to issue in Matplotlib, see Pyomo PR #1337")
 class parmest_graphics(unittest.TestCase):
-    
+
     def setUp(self):
         self.A = pd.DataFrame(np.random.randint(0,100,size=(100,4)), columns=list('ABCD'))
         self.B = pd.DataFrame(np.random.randint(0,100,size=(100,4)), columns=list('ABCD'))
-        
+
     def test_pairwise_plot(self):
         graphics.pairwise_plot(self.A, alpha=0.8, distributions=['Rect', 'MVN', 'KDE'])
-        
+
     def test_grouped_boxplot(self):
-        graphics.grouped_boxplot(self.A, self.B, normalize=True, 
+        graphics.grouped_boxplot(self.A, self.B, normalize=True,
                                 group_names=['A', 'B'])
-        
+
     def test_grouped_violinplot(self):
         graphics.grouped_violinplot(self.A, self.B)
-        
+
 if __name__ == '__main__':
     unittest.main()