Skip to content

Extended Parmest Capability for weighted SSE objective #3535

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 85 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
d332230
Updated parmest file
slilonfe5 Mar 23, 2025
e2c5eab
Updated parmest.py file
slilonfe5 Mar 27, 2025
6f832ca
Updated parmest.py
slilonfe5 Apr 17, 2025
14c0f6d
Updated parmest.py
slilonfe5 Apr 18, 2025
d0c857f
Merge branch 'main' into main
slilonfe5 Apr 19, 2025
0c210de
Updated parmest.py file
slilonfe5 Apr 23, 2025
877d58b
Merge branch 'main' of https://github.com/slilonfe5/pyomo
slilonfe5 Apr 23, 2025
f98590f
Created test for the new capabilities
slilonfe5 May 1, 2025
8fb0de2
Updated parmest.py file
slilonfe5 May 4, 2025
0aaf29b
Updated parmest.py file
slilonfe5 May 7, 2025
b3aa7b1
Updated parmest.py file
slilonfe5 May 12, 2025
8a190eb
Updated parmest.py file
slilonfe5 May 14, 2025
8837f6d
Implemented Alex's comments on the parmest.py file
slilonfe5 May 19, 2025
8a68329
Updated parmest.py file
slilonfe5 May 20, 2025
acd444c
Ran black on parmest.py file
slilonfe5 May 20, 2025
c3d37a5
Updated the test file for the new covariance methods
slilonfe5 May 20, 2025
f106b5c
Ran black on test_new_parmest_capabilities.py
slilonfe5 May 20, 2025
615db14
Merge branch 'main' into main
mrmundt May 20, 2025
5dc48f2
Update pyomo/contrib/parmest/parmest.py
slilonfe5 May 22, 2025
4450a01
Update pyomo/contrib/parmest/parmest.py
slilonfe5 May 22, 2025
55dd836
Update pyomo/contrib/parmest/parmest.py
slilonfe5 May 22, 2025
1b362cf
Updated parmest.py and test_new_parmest_capabilities.py files
slilonfe5 May 22, 2025
ab18ad0
Merge branch 'main' of https://github.com/slilonfe5/pyomo
slilonfe5 May 22, 2025
49c8abe
Updated parmest.py file
slilonfe5 May 23, 2025
46b5356
Updated pyomo/contrib/parmest/parmest.py
slilonfe5 May 23, 2025
a9c4148
Updated pyomo/contrib/parmest/parmest.py
slilonfe5 May 23, 2025
927e36e
Updated parmest.py and the test file
slilonfe5 May 28, 2025
78cc3d8
Updated parmest.py and test_parmest.py files
slilonfe5 Jun 3, 2025
39824b1
Removed test_new_parmest_capabilities.py file
slilonfe5 Jun 3, 2025
7591dc9
Ran black on parmest.py and test_parmest.py files
slilonfe5 Jun 3, 2025
f54dbc6
Added back the test for the deprecated interface
slilonfe5 Jun 4, 2025
bbcdb2d
Small updates to the parmest.py file
slilonfe5 Jun 7, 2025
bc5da0f
Updated the test_parmest.py file
slilonfe5 Jun 7, 2025
3130ad7
Updated parmest.py and test_parmest.py files
slilonfe5 Jun 13, 2025
2346e1b
Ran black and reduced the code line length of parmest.py and test_par…
slilonfe5 Jun 14, 2025
7ba1815
Some string formatting to parmest.py
slilonfe5 Jun 16, 2025
06e4e17
A small bug fix in test_parmest.py file
slilonfe5 Jun 19, 2025
e1e0392
Ran black on test_parmest.py file
slilonfe5 Jun 19, 2025
67a0cbd
Small formatting changes to parmest.py and test_parmest.py
slilonfe5 Jun 24, 2025
ed4abb1
Merge branch 'main' into main
mrmundt Jul 1, 2025
8f64290
Merge branch 'main' into main
slilonfe5 Jul 2, 2025
3756fdb
Implemented Miranda's feedback on parmest.py and updated test_parmest.py
slilonfe5 Jul 5, 2025
23bf10a
Merge branch 'main' into main
slilonfe5 Jul 5, 2025
c78ff0b
Updated parmest.py file
slilonfe5 Jul 8, 2025
cfb8d60
Minor string formatting in test_parmest.py
slilonfe5 Jul 8, 2025
2c2f202
Updated documentation files driver.rst and datarec.rst
slilonfe5 Jul 13, 2025
5e1d04a
Undo the trial fix in the datarec.rst and driver.rst files
slilonfe5 Jul 13, 2025
b472357
Updated driver.rst file and minor string formats in parmest.py
slilonfe5 Jul 16, 2025
b9962dd
Minor string format in test_parmest.py
slilonfe5 Jul 16, 2025
b8df9e1
Updated driver.rst and datarec.rst files
slilonfe5 Jul 17, 2025
93f28b3
Updated covariance.rst and Xinhong comment on parmest.py
slilonfe5 Jul 17, 2025
a2ba7f6
Merge branch 'main' into main
slilonfe5 Jul 17, 2025
0f88a71
Fixed typo in covariance.rst
slilonfe5 Jul 17, 2025
d06338f
Merge branch 'main' of https://github.com/slilonfe5/pyomo
slilonfe5 Jul 17, 2025
726561f
Updated documentation in covariance.rst file
slilonfe5 Jul 17, 2025
aa5a92a
Updated driver.rst and covariance.rst files
slilonfe5 Jul 17, 2025
0a5e74b
Updated covariance.rst, driver.rst, and parmest.py files
slilonfe5 Jul 17, 2025
5f915fb
Updated driver.rst
slilonfe5 Jul 18, 2025
42bda97
Fixed indentation in driver.rst
slilonfe5 Jul 18, 2025
e715e9f
Merge branch 'main' into main
slilonfe5 Jul 21, 2025
1b64e41
Fixed LaTeX error in covariance.rst file
slilonfe5 Jul 21, 2025
bdd93a5
Merge branch 'main' of https://github.com/slilonfe5/pyomo
slilonfe5 Jul 21, 2025
590c5c9
Implemented Miranda's final review on parmest.py, test_parmest.py, an…
slilonfe5 Jul 22, 2025
dd4308e
Update doc/OnlineDocs/explanation/analysis/parmest/covariance.rst
slilonfe5 Aug 1, 2025
01505e9
Merge branch 'main' into main
slilonfe5 Aug 1, 2025
ee4686f
Update pyomo/contrib/parmest/parmest.py
slilonfe5 Aug 1, 2025
66fa00f
Update pyomo/contrib/parmest/parmest.py
slilonfe5 Aug 1, 2025
c1ee5c4
Update pyomo/contrib/parmest/parmest.py
slilonfe5 Aug 1, 2025
d74d133
Update doc/OnlineDocs/explanation/analysis/parmest/covariance.rst
slilonfe5 Aug 1, 2025
d11fdc2
Updated Enum names in parmest.py
slilonfe5 Aug 1, 2025
f509c3b
Updated parmest.py file and ran black
slilonfe5 Aug 1, 2025
9db147b
Updated the covariance.rst file
slilonfe5 Aug 3, 2025
f9a5f69
Merge branch 'main' into main
slilonfe5 Aug 4, 2025
b4a2a5b
Minor string formatting in covariance.rst
slilonfe5 Aug 4, 2025
c92b405
Updated the rooney_biegler.py and all the examples
slilonfe5 Aug 5, 2025
b125043
Ran black on all changed files
slilonfe5 Aug 5, 2025
5c826b8
Merge branch 'main' into main
slilonfe5 Aug 5, 2025
9480646
Update datarec.rst file
slilonfe5 Aug 5, 2025
2149189
Merge branch 'main' of https://github.com/slilonfe5/pyomo
slilonfe5 Aug 5, 2025
72e44d4
Updated rooney_biegler.py and test_parmest.py
slilonfe5 Aug 6, 2025
6925c97
Updated rooney_biegler.py
slilonfe5 Aug 6, 2025
0007ce0
Merge branch 'main' into main
slilonfe5 Aug 6, 2025
71c07ea
Ran black
slilonfe5 Aug 6, 2025
9c8fe6e
Implemented Bethany's comments
slilonfe5 Aug 12, 2025
57e8011
Merge branch 'main' into main
slilonfe5 Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 111 additions & 12 deletions doc/OnlineDocs/explanation/analysis/parmest/covariance.rst
Original file line number Diff line number Diff line change
@@ -1,16 +1,115 @@
Covariance Matrix Estimation
=================================
============================

If the optional argument ``calc_cov=True`` is specified for :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est`,
parmest will calculate the covariance matrix :math:`V_{\theta}` as follows:
The uncertainty in the estimated parameters is quantified using the covariance matrix.
The diagonal of the covariance matrix contains the variance of the estimated parameters.
Assuming Gaussian independent and identically distributed measurement errors, the
covariance matrix of the estimated parameters can be computed using the following
methods which have been implemented in parmest.

.. math::
V_{\theta} = 2 \sigma^2 H^{-1}
1. Reduced Hessian Method

This formula assumes all measurement errors are independent and identically distributed with
variance :math:`\sigma^2`. :math:`H^{-1}` is the inverse of the Hessian matrix for an unweighted
sum of least squares problem. Currently, the covariance approximation is only valid if the
objective given to parmest is the sum of squared error. Moreover, parmest approximates the
variance of the measurement errors as :math:`\sigma^2 = \frac{1}{n-l} \sum e_i^2` where :math:`n` is
the number of data points, :math:`l` is the number of fitted parameters, and :math:`e_i` is the
residual for experiment :math:`i`.
When the objective function is the sum of squared errors (SSE) between the
observed and predicted values of the measured variables, the covariance matrix is:

.. math::
V_{\boldsymbol{\theta}} = 2 \sigma^2 \left(\frac{\partial^2 \text{SSE}}
{\partial \boldsymbol{\theta} \partial \boldsymbol{\theta}}\right)^{-1}_{\boldsymbol{\theta}
= \boldsymbol{\theta}^*}

When the objective function is the weighted SSE (WSSE), the covariance matrix is:

.. math::
V_{\boldsymbol{\theta}} = \left(\frac{\partial^2 \text{WSSE}}
{\partial \boldsymbol{\theta} \partial \boldsymbol{\theta}}\right)^{-1}_{\boldsymbol{\theta}
= \boldsymbol{\theta}^*}

Where :math:`V_{\boldsymbol{\theta}}` is the covariance matrix of the estimated
parameters, :math:`\boldsymbol{\theta}` are the unknown parameters,
:math:`\boldsymbol{\theta^*}` are the estimates of the unknown parameters, and
:math:`\sigma^2` is the variance of the measurement error. When the standard
deviation of the measurement error is not supplied by the user, parmest
approximates the variance of the measurement error as
:math:`\sigma^2 = \frac{1}{n-l} \sum e_i^2` where :math:`n` is the number of data
points, :math:`l` is the number of fitted parameters, and :math:`e_i` is the
residual for experiment :math:`i`.

In parmest, this method computes the inverse of the Hessian by scaling the
objective function (SSE or WSSE) with a constant probability factor.

2. Finite Difference Method

In this method, the covariance matrix, :math:`V_{\boldsymbol{\theta}}`, is
calculated by applying the Gauss-Newton approximation to the Hessian,
:math:`\frac{\partial^2 \text{SSE}}{\partial \boldsymbol{\theta} \partial \boldsymbol{\theta}}`
or
:math:`\frac{\partial^2 \text{WSSE}}{\partial \boldsymbol{\theta} \partial \boldsymbol{\theta}}`,
leading to:

.. math::
V_{\boldsymbol{\theta}} = \left(\sum_{i = 1}^n \mathbf{G}_{i}^{\mathrm{T}} \mathbf{W}
\mathbf{G}_{i} \right)^{-1}

This method uses central finite difference to compute the Jacobian matrix,
:math:`\mathbf{G}_{i}`, for experiment :math:`i`, which is the sensitivity of
the measured variables with respect to the parameters, :math:`\boldsymbol{\theta}`.
:math:`\mathbf{W}` is a diagonal matrix containing the inverse of the variance
of the measurement errors, :math:`\sigma^2`.

3. Automatic Differentiation Method

Similar to the finite difference method, the covariance matrix is calculated as:

.. math::
V_{\boldsymbol{\theta}} = \left( \sum_{i = 1}^n \mathbf{G}_{\text{kaug},\, i}^{\mathrm{T}}
\mathbf{W} \mathbf{G}_{\text{kaug},\, i} \right)^{-1}

However, this method uses the model optimality (KKT) condition to compute the
Jacobian matrix, :math:`\mathbf{G}_{\text{kaug},\, i}`, for experiment :math:`i`.

The covariance matrix calculation is only supported with the built-in objective
functions "SSE" or "SSE_weighted".

In parmest, the covariance matrix can be calculated after defining the
:class:`~pyomo.contrib.parmest.parmest.Estimator` object and estimating the unknown
parameters using :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est`. To
estimate the covariance matrix, with the default method being "finite_difference", call
the :class:`~pyomo.contrib.parmest.parmest.Estimator.cov_est` function, e.g.,

.. testsetup:: *
:skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available

# Data
import pandas as pd
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'],
)

# Create the Experiment class
from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment

exp_list = []
for i in range(data.shape[0]):
exp_list.append(RooneyBieglerExperiment(data.loc[i, :]))

.. doctest::
:skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available

>>> import pyomo.contrib.parmest.parmest as parmest
>>> pest = parmest.Estimator(exp_list, obj_function="SSE")
>>> obj_val, theta_val = pest.theta_est()
>>> cov = pest.cov_est()

Optionally, one of the three methods; "reduced_hessian", "finite_difference",
and "automatic_differentiation_kaug" can be supplied for the covariance calculation,
e.g.,

.. doctest::
:skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available

>>> pest = parmest.Estimator(exp_list, obj_function="SSE")
>>> obj_val, theta_val = pest.theta_est()
>>> cov_method = "reduced_hessian"
>>> cov = pest.cov_est(method=cov_method)
4 changes: 2 additions & 2 deletions doc/OnlineDocs/explanation/analysis/parmest/datarec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ The following example returns model values from a Pyomo Expression.

>>> # Define objective
>>> def SSE(model):
... expr = (model.experiment_outputs[model.y]
... - model.response_function[model.experiment_outputs[model.hour]]
... expr = (model.experiment_outputs[model.y[model.hour]]
... - model.y[model.hour]
... ) ** 2
... return expr

Expand Down
51 changes: 28 additions & 23 deletions doc/OnlineDocs/explanation/analysis/parmest/driver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@ the model and the observations (typically defined as the sum of squared
deviation between model values and observed values).

If the Pyomo model is not formatted as a two-stage stochastic
programming problem in this format, the user can supply a custom
function to use as the second stage cost and the Pyomo model will be
programming problem in this format, the user can choose either the
built-in "SSE" or "SSE_weighted" objective functions, or supply a custom
objective function to use as the second stage cost. The Pyomo model will then be
modified within parmest to match the required specifications.
The stochastic programming callback function is also defined within parmest. The callback
function returns a populated and initialized model for each scenario.
The stochastic programming callback function is also defined within parmest.
The callback function returns a populated and initialized model for each scenario.

To use parmest, the user creates a :class:`~pyomo.contrib.parmest.parmest.Estimator` object
which includes the following methods:
To use parmest, the user creates a :class:`~pyomo.contrib.parmest.parmest.Estimator`
object which includes the following methods:

.. autosummary::
:nosignatures:

~pyomo.contrib.parmest.parmest.Estimator.theta_est
~pyomo.contrib.parmest.parmest.Estimator.cov_est
~pyomo.contrib.parmest.parmest.Estimator.theta_est_bootstrap
~pyomo.contrib.parmest.parmest.Estimator.theta_est_leaveNout
~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta
Expand Down Expand Up @@ -65,16 +67,9 @@ Section.
columns=['hour', 'y'],
)

# Sum of squared error function
def SSE(model):
expr = (
model.experiment_outputs[model.y]
- model.response_function[model.experiment_outputs[model.hour]]
) ** 2
return expr

# Create an experiment list
from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment

exp_list = []
for i in range(data.shape[0]):
exp_list.append(RooneyBieglerExperiment(data.loc[i, :]))
Expand All @@ -83,15 +78,15 @@ Section.
:skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available

>>> import pyomo.contrib.parmest.parmest as parmest
>>> pest = parmest.Estimator(exp_list, obj_function=SSE)
>>> pest = parmest.Estimator(exp_list, obj_function="SSE")

Optionally, solver options can be supplied, e.g.,

.. doctest::
:skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available

>>> solver_options = {"max_iter": 6000}
>>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options)
>>> pest = parmest.Estimator(exp_list, obj_function="SSE", solver_options=solver_options)


List of experiment objects
Expand Down Expand Up @@ -137,17 +132,20 @@ expressions that are used to build an objective for the two-stage
stochastic programming problem.

If the Pyomo model is not written as a two-stage stochastic programming problem in
this format, and/or if the user wants to use an objective that is
different than the original model, a custom objective function can be
defined for parameter estimation. The objective function has a single argument,
which is the model from a single experiment.
this format, the user can select the "SSE" or "SSE_weighted" built-in objective
functions. If the user wants to use an objective that is different from the built-in
options, a custom objective function can be defined for parameter estimation. However,
covariance matrix estimation will not support this custom objective function. The objective
function (built-in or custom) has a single argument, which is the model from a single
experiment.
The objective function returns a Pyomo
expression which is used to define "SecondStageCost". The objective
function can be used to customize data points and weights that are used
in parameter estimation.

Parmest includes one built in objective function to compute the sum of squared errors ("SSE") between the
``m.experiment_outputs`` model values and data values.
Parmest includes two built-in objective functions ("SSE" and "SSE_weighted") to compute
the sum of squared errors between the ``m.experiment_outputs`` model values and
data values.

Suggested initialization procedure for parameter estimation problems
--------------------------------------------------------------------
Expand All @@ -162,4 +160,11 @@ estimation solve from the square problem solution, set optional argument ``solve
argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted
parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**)

3. Solve parameter estimation problem by calling :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est`
3. Solve parameter estimation problem by calling
:class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est`, e.g.,

.. doctest::
:skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available

>>> pest = parmest.Estimator(exp_list, obj_function="SSE")
>>> obj_val, theta_val = pest.theta_est()
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ def main():
# Sum of squared error function
def SSE(model):
expr = (
model.experiment_outputs[model.y]
- model.response_function[model.experiment_outputs[model.hour]]
model.experiment_outputs[model.y[model.hour]] - model.y[model.hour]
) ** 2
return expr

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ def main():
# Sum of squared error function
def SSE(model):
expr = (
model.experiment_outputs[model.y]
- model.response_function[model.experiment_outputs[model.hour]]
model.experiment_outputs[model.y[model.hour]] - model.y[model.hour]
) ** 2
return expr

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ def main():
# Sum of squared error function
def SSE(model):
expr = (
model.experiment_outputs[model.y]
- model.response_function[model.experiment_outputs[model.hour]]
model.experiment_outputs[model.y[model.hour]] - model.y[model.hour]
) ** 2
return expr

Expand Down
25 changes: 11 additions & 14 deletions pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,15 @@ def rooney_biegler_model(data):
model.asymptote = pyo.Var(initialize=15)
model.rate_constant = pyo.Var(initialize=0.5)

model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True)
model.y = pyo.Param(within=pyo.PositiveReals, mutable=True)
model.y = pyo.Var(data.hour, within=pyo.PositiveReals, initialize=5)

def response_rule(m, h):
expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h))
return expr
return m.y[h] == m.asymptote * (1 - pyo.exp(-m.rate_constant * h))

model.response_function = pyo.Expression(data.hour, rule=response_rule)
model.response_function = pyo.Constraint(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
)
return sum((data.y[i] - m.y[data.hour[i]]) ** 2 for i in data.index)

model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize)

Expand All @@ -47,9 +43,10 @@ def SSE_rule(m):

class RooneyBieglerExperiment(Experiment):

def __init__(self, data):
def __init__(self, data, measure_error=None):
self.data = data
self.model = None
self.measure_error = measure_error

def create_model(self):
# rooney_biegler_model expects a dataframe
Expand All @@ -61,22 +58,22 @@ def label_model(self):
m = self.model

m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.experiment_outputs.update(
[(m.hour, self.data['hour']), (m.y, self.data['y'])]
)
m.experiment_outputs.update([(m.y[self.data['hour']], self.data['y'])])

m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.unknown_parameters.update(
(k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant]
)

m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.measurement_error.update([(m.y[self.data['hour']], self.measure_error)])

def finalize_model(self):

m = self.model

# Experiment output values
# Experiment input values
m.hour = self.data['hour']
m.y = self.data['y']

def get_labeled_model(self):
self.create_model()
Expand Down
Loading
Loading