Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
222b396
Add utility to utils.py
sscini Jun 27, 2025
81a595f
Ran black to update formatting
sscini Jun 27, 2025
a3504f8
Merge branch 'Pyomo:main' into add-update-model-utility
sscini Jul 1, 2025
748c732
Added example in doe to show use of util
sscini Jul 1, 2025
3e3a5b8
Modified utils based on 7/1 meeting
sscini Jul 1, 2025
403eae7
Updated example and added tests, still debugging
sscini Jul 1, 2025
1304112
Merge branch 'Pyomo:main' into add-update-model-utility
sscini Jul 1, 2025
5ea8ba5
Merge branch 'Pyomo:main' into add-update-model-utility
sscini Jul 3, 2025
df4de56
Moved utility from doe to parmest
sscini Jul 9, 2025
2b6e304
Updated test_utils, confirming they work tomorrow.
sscini Jul 14, 2025
02460d5
Ran black
sscini Jul 14, 2025
67dccc5
Updated doe utils to match new in main
sscini Jul 14, 2025
63e3a7d
Merge branch 'main' into add-update-model-utility
sscini Jul 14, 2025
692fa9e
Ran black on doe
sscini Jul 14, 2025
cb394e7
Merge branch 'add-update-model-utility' of https://github.com/sscini/…
sscini Jul 14, 2025
a4f3d1c
Ran black again after merging changes
sscini Jul 14, 2025
d2786d7
Added parmest example, fixed and added tests, ran black.
sscini Jul 14, 2025
aa3b6f1
Remove changes from doe tests
sscini Jul 14, 2025
7b390b1
Addressed comments from Dan that do not need clarification
sscini Jul 15, 2025
21db44b
Merge branch 'Pyomo:main' into add-update-model-utility
sscini Jul 28, 2025
d43721a
Updated files and added test to address comments
sscini Aug 1, 2025
dee6c38
Ran black on doe and parmest
sscini Aug 1, 2025
c013ac4
Merge branch 'Pyomo:main' into add-update-model-utility
sscini Aug 1, 2025
a25875c
Made edits to address comments, ran black
sscini Aug 1, 2025
cf37a8b
Merge branch 'Pyomo:main' into add-update-model-utility
sscini Aug 4, 2025
92f772e
Merge branch 'main' into add-update-model-utility
mrmundt Aug 4, 2025
4d1dfec
Addressed comments and ran black
sscini Aug 4, 2025
793f34a
Update updatesuffix_example.py
sscini Aug 4, 2025
a7babec
Delete scenarios.csv
sscini Aug 4, 2025
9ea7e31
Update test_utils.py
sscini Aug 4, 2025
14f7eb6
Addressed file name changes
sscini Aug 5, 2025
a47c54f
Addressed consistency changes
sscini Aug 5, 2025
0a4c9a2
Merge branch 'main' into add-update-model-utility
sscini Aug 5, 2025
6efaa0e
Merge branch 'main' into add-update-model-utility
mrmundt Aug 6, 2025
2767263
Apply suggestions from code review
blnicho Aug 6, 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
74 changes: 74 additions & 0 deletions pyomo/contrib/doe/examples/update_suffix_doe_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
from pyomo.common.dependencies import numpy as np

from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment
from pyomo.contrib.doe import DesignOfExperiments
from pyomo.contrib.doe import utils

from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix
from os.path import join, abspath, dirname

import pyomo.environ as pyo

import json


# Example to run a DoE on the reactor
def main():
# Read in file
file_dirname = dirname(abspath(str(__file__)))
file_path = abspath(join(file_dirname, "result.json"))

# Read in data
with open(file_path) as f:
data_ex = json.load(f)

# Put temperature control time points into correct format for reactor experiment
data_ex["control_points"] = {
float(k): v for k, v in data_ex["control_points"].items()
}

# Create a ReactorExperiment object; data and discretization information are part
# of the constructor of this object
experiment = ReactorExperiment(data=data_ex, nfe=10, ncp=3)

# Call the experiment's model using get_labeled_model
reactor_model = experiment.get_labeled_model()

# Show the model
reactor_model.pprint()
# The suffix object 'measurement_error' stores measurement error values for each component.
# Here, we retrieve the original values from the suffix for inspection.
suffix_obj = reactor_model.measurement_error
me_vars = list(suffix_obj.keys()) # components
orig_vals = np.array(list(suffix_obj.values()))

# Original values
print("Original sigma values")
print("-----------------------")
suffix_obj.display()

# Update the suffix with new values
new_vals = orig_vals + 1
# Here we are updating the values of the measurement error
# You must know the length of the list and order of the suffix items to update them correctly
update_model_from_suffix(suffix_obj, new_vals)

# Updated values
print("Updated sigma values :")
print("-----------------------")
suffix_obj.display()
return suffix_obj, orig_vals, new_vals


if __name__ == "__main__":
main()
11 changes: 11 additions & 0 deletions pyomo/contrib/doe/tests/test_doe_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,17 @@ def test_generate_blocks_without_model(self):
doe_obj.model.find_component("scenario_blocks[" + str(i) + "]")
)

def test_reactor_update_suffix_items(self):
"""Test the reactor example with updating suffix items."""
from pyomo.contrib.doe.examples.update_suffix_doe_example import main

# Run the reactor update suffix items example
suffix_obj, _, new_vals = main()

# Check that the suffix object has been updated correctly
for i, v in enumerate(suffix_obj.values()):
self.assertAlmostEqual(v, new_vals[i], places=6)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2025
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from pyomo.common.dependencies import numpy as np, pandas as pd
from os.path import join, abspath, dirname
import pyomo.contrib.parmest.parmest as parmest
from pyomo.contrib.parmest.examples.reactor_design.reactor_design import (
ReactorDesignExperiment,
)

import pyomo.environ as pyo
from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix


def main():
# Read in file
# Read in data
file_dirname = dirname(abspath(str(__file__)))
file_name = abspath(join(file_dirname, "reactor_data.csv"))
data = pd.read_csv(file_name)

experiment = ReactorDesignExperiment(data, 0)

# Call the experiment's model using get_labeled_model
reactor_model = experiment.get_labeled_model()

example_suffix = "unknown_parameters"
suffix_obj = reactor_model.unknown_parameters
var_list = list(suffix_obj.keys()) # components
orig_var_vals = np.array([pyo.value(v) for v in var_list]) # numeric var values

# Print original values
print("Original sigma values")
print("----------------------")
print(orig_var_vals)

# Update the suffix with new values
new_vals = orig_var_vals + 0.5

print("New sigma values")
print("----------------")
print(new_vals)

# Here we are updating the values of the unknown parameters
# You must know the length of the list and order of the suffix items to update them correctly
update_model_from_suffix(suffix_obj, new_vals)

# Updated values
print("Updated sigma values :")
print("-----------------------")
new_var_vals = np.array([pyo.value(v) for v in var_list])
print(new_var_vals)

# Return the suffix obj, original and new values for further use if needed
return suffix_obj, new_vals, new_var_vals


if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions pyomo/contrib/parmest/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ def test_datarec_example(self):

datarec_example.main()

def test_update_suffix_example(self):
from pyomo.contrib.parmest.examples.reactor_design import update_suffix_example

suffix_obj, new_vals, new_var_vals = update_suffix_example.main()

# Check that the suffix object has been updated correctly
for i, v in enumerate(new_var_vals):
self.assertAlmostEqual(new_var_vals[i], new_vals[i], places=6)


if __name__ == "__main__":
unittest.main()
147 changes: 145 additions & 2 deletions pyomo/contrib/parmest/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,37 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from pyomo.common.dependencies import pandas as pd, pandas_available
from pyomo.common.dependencies import (
pandas as pd,
pandas_available,
numpy as np,
numpy_available,
)

import os.path
import json

import pyomo.environ as pyo

from pyomo.common.fileutils import this_file_dir
import pyomo.common.unittest as unittest

import pyomo.contrib.parmest.parmest as parmest
from pyomo.opt import SolverFactory

ipopt_available = SolverFactory("ipopt").available()
from pyomo.contrib.parmest.utils.model_utils import update_model_from_suffix
from pyomo.contrib.doe.examples.reactor_example import (
ReactorExperiment as FullReactorExperiment,
)

currdir = this_file_dir()
file_path = os.path.join(currdir, "..", "..", "doe", "examples", "result.json")

with open(file_path) as f:
data_ex = json.load(f)
data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()}

ipopt_available = pyo.SolverFactory("ipopt").available()


@unittest.skipIf(
Expand Down Expand Up @@ -60,6 +83,126 @@ def test_convert_param_to_var(self):
self.assertEqual(pyo.value(c), pyo.value(c_old))
self.assertTrue(c in m_vars.unknown_parameters)

def test_update_model_from_suffix_experiment_outputs(self):
from pyomo.contrib.parmest.examples.reactor_design.reactor_design import (
ReactorDesignExperiment,
)

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],
],
columns=["sv", "caf", "ca", "cb", "cc", "cd"],
)
experiment = ReactorDesignExperiment(data, 0)
test_model = experiment.get_labeled_model()

suffix_obj = test_model.experiment_outputs # a Suffix
var_list = list(suffix_obj.keys()) # components
orig_var_vals = np.array([pyo.value(v) for v in var_list])
orig_suffix_val = np.array([tag for _, tag in suffix_obj.items()])
new_vals = orig_var_vals + 0.5
# Update the model from the suffix
update_model_from_suffix(suffix_obj, new_vals)
# ── Check results ────────────────────────────────────────────────────
new_var_vals = np.array([pyo.value(v) for v in var_list])
new_suffix_val = np.array(list(suffix_obj.values()))
# (1) Variables have been overwritten with `new_vals`
self.assertTrue(np.allclose(new_var_vals, new_vals))
# (2) Suffix tags are unchanged
self.assertTrue(np.array_equal(new_suffix_val, orig_suffix_val))

def test_update_model_from_suffix_measurement_error(self):
experiment = FullReactorExperiment(data_ex, 10, 3)
test_model = experiment.get_labeled_model()

suffix_obj = test_model.measurement_error # a Suffix
var_list = list(suffix_obj.keys()) # components
orig_var_vals = np.array([suffix_obj[v] for v in var_list])
new_vals = orig_var_vals + 0.5
# Update the model from the suffix
update_model_from_suffix(suffix_obj, new_vals)
# ── Check results ────────────────────────────────────────────────────
new_var_vals = np.array([suffix_obj[v] for v in var_list])
# (1) Variables have been overwritten with `new_vals`
self.assertTrue(np.allclose(new_var_vals, new_vals))

def test_update_model_from_suffix_length_mismatch(self):
m = pyo.ConcreteModel()

# Create a suffix with a Var component
m.x = pyo.Var(initialize=0.0)
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.unknown_parameters[m.x] = 0.0 # tag a Var
with self.assertRaisesRegex(
ValueError, "values length does not match suffix length"
):
# Attempt to update with a list of different length
update_model_from_suffix(m.unknown_parameters, [42, 43, 44])

def test_update_model_from_suffix_not_numeric(self):
m = pyo.ConcreteModel()

# Create a suffix with a Var component
m.x = pyo.Var(initialize=0.0)
m.y = pyo.Var(initialize=1.0)
bad_value = "not_a_number"
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
# Make multiple values
m.unknown_parameters[m.x] = 0.0 # tag a Var
m.unknown_parameters[m.y] = bad_value # tag a Var with a bad value
# Attempt to update with a list of mixed types
# This should raise an error because this utility only allows numeric values
# in the model to be updated.

with self.assertRaisesRegex(
ValueError, f"could not convert string to float: '{bad_value}'"
):
# Attempt to update with a non-numeric value
update_model_from_suffix(m.unknown_parameters, [42, bad_value])

def test_update_model_from_suffix_wrong_component_type(self):
m = pyo.ConcreteModel()

# Create a suffix with a Var component
m.x = pyo.Var(initialize=0.0)
m.e = pyo.Expression(expr=m.x + 1) # not Var/Param
m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.unknown_parameters[m.x] = 0.0
m.unknown_parameters[m.e] = 1.0 # tag an Expression
# Attempt to update with a list of wrong component type
with self.assertRaisesRegex(
TypeError,
f"Unsupported component type {type(m.e)}; expected VarData or ParamData.",
):
update_model_from_suffix(m.unknown_parameters, [42, 43])

def test_update_model_from_suffix_unsupported_component(self):
m = pyo.ConcreteModel()

# Create a suffix with a ConstraintData component
m.x = pyo.Var(initialize=0.0)
m.c = pyo.Constraint(expr=m.x == 0) # not Var/Param!

m.bad_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL)
m.bad_suffix[m.c] = 0 # tag a Constraint

with self.assertRaisesRegex(
TypeError, r"Unsupported component type .*Constraint.*"
):
update_model_from_suffix(m.bad_suffix, [1.0])

def test_update_model_from_suffix_empty(self):
m = pyo.ConcreteModel()

# Create an empty suffix
m.empty_suffix = pyo.Suffix(direction=pyo.Suffix.LOCAL)

# This should not raise an error
update_model_from_suffix(m.empty_suffix, [])


if __name__ == "__main__":
unittest.main()
Loading
Loading