Skip to content
Draft
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
248 commits
Select commit Hold shift + click to select a range
cdd7d52
Work on multistart implement 4/23 morning
sscini Apr 23, 2025
eca0ba8
Finished first draft of pseudocode for multistart
sscini Apr 23, 2025
2160aec
Fixed logical errors in pseudocode
sscini Apr 23, 2025
266beea
Started implementing review comments 4/30
sscini Apr 30, 2025
b877ada
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Apr 30, 2025
9f1ffe5
Work on edits, 5/1/25
sscini May 1, 2025
43f1ab3
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini May 1, 2025
ea067c8
Made edits, still debugging
sscini May 2, 2025
3b839ef
Addressed some comments in code. Still working through example to debug
sscini May 14, 2025
3a7aa1d
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini May 14, 2025
c688f2d
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini May 21, 2025
50c36bc
Got dataframe formatted, still working on executing Q_opt
sscini May 21, 2025
8e5f078
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jun 2, 2025
f4c7018
Working code, adding features 6/2/25
sscini Jun 2, 2025
4444e6d
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jun 3, 2025
e788000
Added questions for next round of reviews
sscini Jun 3, 2025
4429caf
Merge branch 'multistart-in-parmest' of https://github.com/sscini/pyo…
sscini Jun 3, 2025
f071718
Removed diagnostic tables to simplify output
sscini Jun 3, 2025
9b1545d
Work from Wednesday of Sprint week
sscini Jun 4, 2025
80079cb
Create Simple_Multimodal_Multistart.ipynb
sscini Jun 4, 2025
1695519
New features Thursday morning
sscini Jun 5, 2025
0634014
First successful running multistart feature, before Alex recommended …
sscini Jun 5, 2025
a959346
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jun 5, 2025
04a9096
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jun 23, 2025
06e0a72
Ran black, removed temp example
sscini Jun 24, 2025
6b3ee40
Added utility to update model using suffix values
sscini Jun 27, 2025
5cadfac
Work on Friday 6/27 applying PR comments
sscini Jun 27, 2025
922fd57
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jun 30, 2025
1be2d9e
Addressed some reviewer comments and ran black.
sscini Jun 30, 2025
56800f5
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jul 1, 2025
05381c5
Updated argument for theta_est_multistart
sscini Jul 6, 2025
5b4f9c1
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jul 7, 2025
07ae1e8
Addressed majority of review comments. State before 7/8 dev meeting
sscini Jul 8, 2025
33d838f
Fixing conflict
sscini Jul 8, 2025
65a9cff
Merge branch 'main' into multistart-in-parmest
sscini Jul 15, 2025
90093df
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Jul 17, 2025
e7b2df1
Added in TODO items based on Dan morning meeting
sscini Jul 17, 2025
10f6b37
First pass at code redesign, still need to figure out more
sscini Nov 18, 2025
b0b7e2c
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Nov 20, 2025
3e95e91
Added comments where I have question
sscini Nov 20, 2025
8d3a4d5
Merge branch 'Q_opt-redesign' of https://github.com/sscini/pyomo into…
sscini Nov 20, 2025
3982e1b
Got preliminary _Q_opt simple working with example!
sscini Nov 21, 2025
e829344
Ran black
sscini Nov 21, 2025
e464097
Changed name to _Q_opt_blocks
sscini Nov 21, 2025
dc5ee76
Update parmest.py
sscini Nov 21, 2025
6355818
Ran black
sscini Nov 21, 2025
099f541
Added in case for bootlist, works with example
sscini Nov 25, 2025
76ee05e
Ran black
sscini Nov 25, 2025
d91ce3f
Simplified structure, ran black
sscini Nov 29, 2025
1aea99f
Removed _Q_opt, and replicate functions, only using _Q_opt_blocks
sscini Dec 13, 2025
7d93cc0
Ran black on mac
sscini Dec 15, 2025
d7d2214
Revert "Removed _Q_opt, and replicate functions, only using _Q_opt_bl…
sscini Dec 15, 2025
7f21344
Added testing statement
sscini Dec 17, 2025
32d8d41
Ran black
sscini Dec 17, 2025
4e8c3ee
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Jan 6, 2026
1e802ba
Made small design changes, in progress, ran black.
sscini Jan 6, 2026
4b89bb3
Merge branch 'Q_opt-redesign' of https://github.com/sscini/pyomo into…
sscini Jan 6, 2026
4777253
Progress made on objective_at_theta_blocks, unfinished.
sscini Jan 8, 2026
c58d5f3
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Jan 11, 2026
ea734b7
Merge branch 'Q_opt-redesign' of https://github.com/sscini/pyomo into…
sscini Jan 11, 2026
490abea
Added notes for design meeting 01/12/26
sscini Jan 12, 2026
1074fb0
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Jan 14, 2026
9543456
Removed answered reviewer question, attempted adding covariance
sscini Jan 14, 2026
af4df1a
Added assertions for cov_n
sscini Jan 14, 2026
d4c4125
Finished implementing covariance
sscini Jan 14, 2026
9d396fa
Added functional return values argument
sscini Jan 14, 2026
a97b21e
Ran black
sscini Jan 14, 2026
0afb5ba
Corrected extraction for unknown parameters
sscini Jan 14, 2026
191b131
Initial attempt at objective_at_theta_blocks
sscini Jan 14, 2026
2c0760e
Working out bugs in _Q_at_theta implement. In progress.
sscini Jan 14, 2026
bbe994b
Corrected obj_at_theta_blocks
sscini Jan 15, 2026
2c2e024
Removed _Q_opt, commented out _Q_at_theta, ran black
sscini Jan 15, 2026
2333a4b
Fixed for loop issue
sscini Jan 15, 2026
b9af9f1
Removed fix_theta from create_parm_model
sscini Jan 15, 2026
6cd3b4b
Renamed objective_at_theta_blocks, ran black.
sscini Jan 15, 2026
b46e1a7
Removed mentions of _Q_opt_blocks
sscini Jan 15, 2026
3261798
Changed back to get_labeled_model in _cov_at_theta()
sscini Jan 15, 2026
fc478be
Added notes in unused files.
sscini Jan 15, 2026
acba985
Removed _Q_at_theta and objective_at_theta
sscini Jan 15, 2026
145c2d8
Added comments for reviewers, ran black.
sscini Jan 15, 2026
337095d
Corrected count_total_experiments to divide by # outputs.
sscini Jan 15, 2026
837192c
Ran black.
sscini Jan 15, 2026
4b46c30
Undo change to count_total_experiments.
sscini Jan 15, 2026
b9cf010
Update mpi_utils.py
sscini Jan 19, 2026
062a9ee
Switched unknown_params to Vars
sscini Jan 19, 2026
5baaa2f
Fixed number for cov_n, still need to adjust counting function
sscini Jan 19, 2026
c8194ac
Added question for reviewers
sscini Jan 19, 2026
26d70e3
Ran black
sscini Jan 19, 2026
4aa027d
Changed retrieval of variables for ind_red_hes.
sscini Jan 19, 2026
26ba2ea
Added note related to count_total_experiments, commented out assertion.
sscini Jan 19, 2026
dd926f8
Ran black
sscini Jan 19, 2026
7b70d1d
Removed dependence on cov_est for theta_est(), added bool for len(exp…
sscini Jan 19, 2026
43cbaa4
Merge branch 'main' into Q_opt-redesign
sscini Jan 19, 2026
07798c9
Ran black
sscini Jan 19, 2026
b325f0d
Attempted adding support for indexed vars
sscini Jan 19, 2026
8b47430
Ran black
sscini Jan 20, 2026
aac1476
Addressed some review comments.
sscini Jan 20, 2026
66a1396
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Jan 21, 2026
3957dc9
Added Shammah fix for exp count
sscini Jan 21, 2026
382ea20
Updates to address comments.
sscini Jan 21, 2026
0da606f
Addressed some comments, simplified scenarios
sscini Jan 22, 2026
935b700
Replaced getattr with suffix calls.
sscini Jan 22, 2026
1fc71ee
Updated, ran black.
sscini Jan 22, 2026
56ac15d
Noted failing tests, currently 15
sscini Jan 22, 2026
60dc796
Removed old comment during dev
sscini Jan 22, 2026
6bf439e
Fixed scenario count issue, ran black.
sscini Jan 22, 2026
a68906b
Added else statement in cov calc
sscini Jan 23, 2026
f31a35f
Update test_parmest.py
sscini Jan 23, 2026
b124def
Update parmest.py
sscini Jan 23, 2026
2908c78
Update simple_reaction_parmest_example.py
sscini Jan 23, 2026
58435d3
Added measurement error to reactor_design
sscini Jan 23, 2026
db646ba
Changed to built-in SSE
sscini Jan 23, 2026
d222c4f
Commented out model_initialized
sscini Jan 23, 2026
9d829c4
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Jan 23, 2026
7ffab76
Merge remote-tracking branch 'refs/remotes/origin/Q_opt-redesign' int…
sscini Jan 23, 2026
e267983
Remove solver import
sscini Jan 23, 2026
e3ae6e6
Ran black
sscini Jan 23, 2026
345c3f2
Update test_parmest.py
sscini Jan 23, 2026
6c3d5a0
Added back option, ran black
sscini Jan 23, 2026
49b787a
Rearranged _Q_opt for fix_theta
sscini Jan 23, 2026
8cf624e
Ran black
sscini Jan 26, 2026
c248c74
Adjusting parmest models, in progress
sscini Jan 27, 2026
b975089
Added more description, simplified comparison
sscini Jan 27, 2026
a63e4fc
Update parameter_estimation_example.py
sscini Jan 27, 2026
b92aa7d
Ran black
sscini Jan 27, 2026
471dbe7
Adjusted if statement
sscini Jan 27, 2026
98d91fc
Removed answered questions
sscini Jan 27, 2026
a6821ae
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Jan 27, 2026
f0ef6d6
Update parmest.py
sscini Feb 2, 2026
1f86b03
Fixed models in variants test
sscini Feb 5, 2026
72b4345
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Feb 5, 2026
c495e0f
Merge remote-tracking branch 'refs/remotes/origin/Q_opt-redesign' int…
sscini Feb 5, 2026
82ee4ce
Update test_parmest.py
sscini Feb 5, 2026
559a900
Update parmest.py
sscini Feb 5, 2026
65067d5
Update parmest.py
sscini Feb 5, 2026
db26533
Temporary remove failing test.
sscini Feb 5, 2026
c9b19d7
Fixed experiment counter
sscini Feb 5, 2026
7bba006
Modified testing
sscini Feb 5, 2026
2cd2614
Removed extra print statements
sscini Feb 5, 2026
5ba4fab
Updated error message.
sscini Feb 5, 2026
ee57ec9
Adjusted experimental counter
sscini Feb 5, 2026
7cffb34
Update test_examples.py
sscini Feb 6, 2026
ebbd279
Addressed comments
sscini Feb 6, 2026
0c1e605
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Feb 11, 2026
92a2dd6
Update simple_reaction_parmest_example.py
sscini Feb 15, 2026
854366b
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Feb 19, 2026
c55143d
Added in files from main now, removed example
sscini Feb 19, 2026
8f3a902
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Feb 19, 2026
3c87d7a
Added old files in temporarily for reference
sscini Feb 19, 2026
4e09d33
Ran black
sscini Feb 19, 2026
4f94329
Merge branch 'Q_opt-redesign' into multistart-in-parmest
sscini Feb 19, 2026
12d0af1
Made quick working example, and small modification for multistart
sscini Feb 19, 2026
72204d9
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Feb 23, 2026
0a1167d
Merge branch 'main' into multistart-in-parmest
sscini Feb 24, 2026
9ccfba4
Added theta generation and old multistart over
sscini Mar 3, 2026
78f65f3
Updated multistart example and code
sscini Mar 3, 2026
43a67ab
Added bounds to models, ran black
sscini Mar 3, 2026
ed2cf54
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Mar 6, 2026
2b41ba5
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Mar 6, 2026
cc0513a
Updated interface, added block scenario tests.
sscini Mar 6, 2026
2e5ac42
Merge branch 'Q_opt-redesign' into multistart-in-parmest
sscini Mar 6, 2026
813a981
Adjusted implementation, added new tests in separate file
sscini Mar 6, 2026
44a0b83
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Mar 6, 2026
9c0d0a3
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Mar 6, 2026
f9c8e3a
Fixed issue with repeat thetas, ran black
sscini Mar 9, 2026
4df5b48
Merge branch 'main' into Q_opt-redesign
sscini Mar 17, 2026
12a3c1b
Merge branch 'main' into multistart-in-parmest
sscini Mar 24, 2026
4fc0336
Merge branch 'main' into Q_opt-redesign
sscini Mar 24, 2026
b372edd
Moved new tests into the main parmest testing file.
sscini Mar 24, 2026
41e8e98
Ran black
sscini Mar 24, 2026
9fe600f
Update test_parmest.py
sscini Mar 25, 2026
48ba6a6
Merge branch 'main' into Q_opt-redesign
sscini Mar 25, 2026
ba4091c
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Mar 27, 2026
eba10b3
Removed covariance functionality from theta_est, in progress
sscini Mar 27, 2026
c997944
Update simple_reaction_parmest_example.py
sscini Mar 27, 2026
d2b5d74
Update simple_reaction_parmest_example.py
sscini Mar 27, 2026
ae9808f
Update parameter_estimation_example.py
sscini Mar 27, 2026
9fa6528
Adjusted tests, ran black
sscini Mar 27, 2026
cb1ecea
Update test_parmest.py
sscini Mar 27, 2026
261a78f
Delete scenarios.csv
sscini Mar 27, 2026
664d687
Merge branch 'Q_opt-redesign' into multistart-in-parmest
sscini Mar 27, 2026
3dca7d7
Merge branch 'main' into Q_opt-redesign
blnicho Apr 2, 2026
959d58e
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Apr 7, 2026
5530e9d
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini Apr 7, 2026
b21c34d
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Apr 8, 2026
1b6c63a
Merge branch 'main' into Q_opt-redesign
sscini Apr 9, 2026
f768ea3
Merge branch 'main' into Q_opt-redesign
sscini Apr 13, 2026
867e642
Merge branch 'main' into Q_opt-redesign
sscini Apr 15, 2026
6d7eb7a
Merge branch 'main' into Q_opt-redesign
sscini Apr 21, 2026
a39d367
Merge branch 'main' into Q_opt-redesign
sscini Apr 22, 2026
deb3c5a
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini Apr 24, 2026
9fcf4f1
Merge branch 'main' into Q_opt-redesign
sscini Apr 28, 2026
a188dd7
Merge branch 'main' into Q_opt-redesign
blnicho Apr 29, 2026
6552bd3
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini May 5, 2026
338fbfb
Update pyomo/contrib/parmest/parmest.py
sscini May 5, 2026
e4c841e
Addressing PR comments, in progress
sscini May 5, 2026
e889dd7
Ran black, tests passing, in progress
sscini May 5, 2026
a13244a
Addressed more comments, ran black, testing in progress
sscini May 5, 2026
4a60be9
Update parmest.py
sscini May 5, 2026
26bfd77
Merge branch 'main' into Q_opt-redesign
sscini May 6, 2026
477429f
Updated logger issue, ran black
sscini May 6, 2026
7a81ffb
Merge branch 'Q_opt-redesign' of https://github.com/sscini/pyomo into…
sscini May 6, 2026
ca54944
Addressed list comment, refactor for indexing by scenarios in progress
sscini May 6, 2026
3389627
Attempting scenario storage and indexing.
sscini May 6, 2026
34ccc7e
Update test_parmest.py
sscini May 6, 2026
fe317fa
Small comment change
sscini May 6, 2026
dd5cbb4
Attempt at making exp count more robust
sscini May 6, 2026
9e3380b
Revert "Attempt at making exp count more robust"
sscini May 6, 2026
0a8c216
Attempt at adding termination condition support.
sscini May 6, 2026
5118d77
Test failures address, ran black.
sscini May 6, 2026
fd723ff
Resolved issues with experiment count, tests passing
sscini May 6, 2026
2edc2c2
Merge branch 'main' into Q_opt-redesign
sscini May 6, 2026
c8c458d
Updated the datapoint counting function in parmest.py
slilonfe5 May 7, 2026
1d5bfd6
Fixed string formatting issues in parmest.py file
slilonfe5 May 7, 2026
f48b2e9
Added assertions to check the experiment outputs
slilonfe5 May 8, 2026
c4b7070
Ran black
slilonfe5 May 8, 2026
3a10f4f
Final polishing for data point counting
slilonfe5 May 8, 2026
387d3e8
Merge branch 'main' into Q_opt-redesign
slilonfe5 May 8, 2026
23e5579
Minor string formatting
slilonfe5 May 8, 2026
118008a
Removed parentheses in assertion strings
slilonfe5 May 8, 2026
e07bc95
Merge branch 'main' into Q_opt-redesign
slilonfe5 May 8, 2026
03db22d
Addressed inconsistent returns in _Q_opt
sscini May 11, 2026
3bd26e3
Ran black
sscini May 11, 2026
19c4edf
Merge branch 'Pyomo:main' into Q_opt-redesign
sscini May 11, 2026
724e5c7
Merge branch 'Q_opt-redesign' of https://github.com/sscini/pyomo into…
sscini May 11, 2026
2dcf3e3
Modified to use CUIDs
sscini May 11, 2026
c3607a3
Ran black
sscini May 11, 2026
8e82f9d
Removed print statements
slilonfe5 May 11, 2026
72e2806
Remove redundant tests
sscini May 11, 2026
75dd55a
Ran black
sscini May 11, 2026
fe0627a
Moved deprecated functions down
sscini May 11, 2026
8e29566
Ran black
sscini May 11, 2026
c67ecce
Removed print statement
sscini May 11, 2026
6626ee6
Addressed mock tests and new failure
sscini May 11, 2026
d919c17
ran black
sscini May 11, 2026
07e1f24
Remove mock import
sscini May 11, 2026
c966bbc
Update test_parmest.py
sscini May 11, 2026
12fc720
Added a few tests to improve coverage and removed duplicated exceptio…
slilonfe5 May 12, 2026
57a14a2
Merge branch 'main' into Q_opt-redesign
slilonfe5 May 12, 2026
e9f64a9
Ran black
slilonfe5 May 12, 2026
eecccba
Final polishing of comments in data counting
slilonfe5 May 12, 2026
6e9455e
Merge branch 'main' into Q_opt-redesign
blnicho May 14, 2026
7bb9c1a
Removed the local case from the datapoint counting
slilonfe5 May 14, 2026
769ac2f
Merge branch 'Q_opt-redesign' of https://github.com/sscini/pyomo into…
slilonfe5 May 14, 2026
db1483a
Improved code flow in data point counting
slilonfe5 May 14, 2026
de855fa
Addressed comments, ran black,
sscini May 14, 2026
aa30f0c
Merge branch 'Pyomo:main' into multistart-in-parmest
sscini May 14, 2026
1a1a7c0
Merge branch 'Q_opt-redesign' into multistart-in-parmest
sscini May 14, 2026
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
8,231 changes: 8,231 additions & 0 deletions pyomo/contrib/parmest/examples/Simple_Multimodal_Multistart.ipynb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# ___________________________________________________________________________
#
# 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 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,
)


def main():

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

# Create an experiment list
exp_list = []
for i in range(data.shape[0]):
exp_list.append(ReactorDesignExperiment(data, i))

# View one model
# exp0_model = exp_list[0].get_labeled_model()
# exp0_model.pprint()

pest = parmest.Estimator(exp_list, obj_function='SSE')

# Parameter estimation
obj, theta = pest.theta_est()

# Parameter estimation with multistart to avoid local minima
obj, theta = pest.theta_est_multistart(
num_starts=10,
start_method='random',
random_seed=42,
max_iter=1000,
tol=1e-6,
)



if __name__ == "__main__":
main()
312 changes: 312 additions & 0 deletions pyomo/contrib/parmest/parmest.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ def SSE(model):
return expr


'''Adding pseudocode for draft implementation of the estimator class,
incorporating multistart.
'''
class Estimator(object):
"""
Parameter estimation class
Expand Down Expand Up @@ -275,6 +278,11 @@ def __init__(
solver_options=None,
):

'''first theta would be provided by the user in the initialization of
the Estimator class through the unknown parameter variables. Additional
would need to be generated using the sampling method provided by the user.
'''

# check that we have a (non-empty) list of experiments
assert isinstance(experiment_list, list)
self.exp_list = experiment_list
Expand Down Expand Up @@ -447,6 +455,145 @@ def TotalCost_rule(model):
parmest_model = utils.convert_params_to_vars(model, theta_names, fix_vars=False)

return parmest_model

# Make new private method, _generate_initial_theta:
# This method will be used to generate the initial theta values for multistart
# optimization. It will take the theta names and the initial theta values
# and return a dictionary of theta names and their corresponding values.
def _generate_initial_theta(self, parmest_model=None, seed=None, n_restarts=None, multistart_sampling_method=None, user_provided=None):
"""
Generate initial theta values for multistart optimization using selected sampling method.
"""
if n_restarts == 1:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like just sending a warning, and not returning. For example, n_restarts might be 1 by default. You should check if n_restarts is an int as well. Then, if n_restarts is 1, you should send a warning that the tool is intended for this number to be greater than one and solve as normal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alex had initially said just return message, but I will ask him again

# If only one restart, return an empty list
return print("No multistart optimization needed. Please use normal theta_est()")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should raise a warning/log something here instead of using a print statement. That way you can use a debugger to control whether the message is displayed.


# Get the theta names and initial theta values
theta_names = self._return_theta_names()
Comment thread
sscini marked this conversation as resolved.
Outdated
initial_theta = [parmest_model.find_component(name)() for name in theta_names]
Comment thread
sscini marked this conversation as resolved.
Outdated
Comment thread
sscini marked this conversation as resolved.
Outdated

# Get the lower and upper bounds for the theta values
lower_bound = np.array([parmest_model.find_component(name).lb for name in theta_names])
upper_bound = np.array([parmest_model.find_component(name).ub for name in theta_names])
# Check if the lower and upper bounds are defined
if any(bound is None for bound in lower_bound) and any(bound is None for bound in upper_bound):
raise ValueError(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably already know this, but you will need to check all the errors are raised when expected.

"The lower and upper bounds for the theta values must be defined."
)

# Check the length of theta_names and initial_theta, and make sure bounds are defined
if len(theta_names) != len(initial_theta):
Comment thread
sscini marked this conversation as resolved.
Outdated
raise ValueError(
"The length of theta_names and initial_theta must be the same."
)

if multistart_sampling_method == "uniform":
Comment thread
sscini marked this conversation as resolved.
Outdated
# Generate random theta values using uniform distribution, with set seed for reproducibility
np.random.seed(seed)
Comment thread
sscini marked this conversation as resolved.
Outdated
# Generate random theta values for each restart (n_restarts x len(theta_names))
theta_vals_multistart = np.random.uniform(
low=lower_bound, high=upper_bound, size=(n_restarts, len(theta_names))
)

elif multistart_sampling_method == "latin_hypercube":
# Generate theta values using Latin hypercube sampling or Sobol sampling
# Generate theta values using Latin hypercube sampling
# Create a Latin Hypercube sampler that uses the dimensions of the theta names
sampler = scipy.stats.qmc.LatinHypercube(d=len(theta_names), seed=seed)
# Generate random samples in the range of [0, 1] for number of restarts
samples = sampler.random(n=n_restarts)
# Resulting samples should be size (n_restarts, len(theta_names))

elif multistart_sampling_method == "sobol":
sampler = scipy.stats.qmc.Sobol(d=len(theta_names), seed=seed)
# Generate theta values using Sobol sampling
# The first value of the Sobol sequence is 0, so we skip it
samples = sampler.random(n=n_restarts+1)[1:]

elif multistart_sampling_method == "user_provided":
Comment thread
sscini marked this conversation as resolved.
Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe user_provided does not imply values. For instance, what if a user wants to provide their own random sample generator?

Probably should use user_provided_values to be more explicit.

Also, should have some comments at the beginning of this describing what the method does, just like the other options.

# Add user provided dataframe option
if user_provided is not None:

if isinstance(user_provided, np.ndarray):
# Check if the user provided numpy array has the same number of rows as the number of restarts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure comments are not too long. Break up over multiple lines like you did above...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running black may help?

if user_provided.shape[0] != n_restarts:
Comment thread
sscini marked this conversation as resolved.
Outdated
raise ValueError(
"The user provided numpy array must have the same number of rows as the number of restarts."
)
# Check if the user provided numpy array has the same number of columns as the number of theta names
if user_provided.shape[1] != len(theta_names):
raise ValueError(
"The user provided numpy array must have the same number of columns as the number of theta names."
)
# Check if the user provided numpy array has the same theta names as the model
# if not, raise an error
# if not all(theta in theta_names for theta in user_provided.columns):
raise ValueError(
Comment thread
sscini marked this conversation as resolved.
Outdated
"The user provided numpy array must have the same theta names as the model."
)
# If all checks pass, return the user provided numpy array
theta_vals_multistart = user_provided
elif isinstance(user_provided, pd.DataFrame):
# Check if the user provided dataframe has the same number of rows as the number of restarts
if user_provided.shape[0] != n_restarts:
raise ValueError(
"The user provided dataframe must have the same number of rows as the number of restarts."
)
# Check if the user provided dataframe has the same number of columns as the number of theta names
if user_provided.shape[1] != len(theta_names):
raise ValueError(
"The user provided dataframe must have the same number of columns as the number of theta names."
)
# Check if the user provided dataframe has the same theta names as the model
# if not, raise an error
# if not all(theta in theta_names for theta in user_provided.columns):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be uncommented...

raise ValueError(
"The user provided dataframe must have the same theta names as the model."
)
# If all checks pass, return the user provided dataframe
theta_vals_multistart = user_provided.iloc[0: len(initial_theta)].values
else:
raise ValueError(
"The user must provide a numpy array or pandas dataframe from a previous attempt to use the 'user_provided' method."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing with all these long output messages, make sure they don't exceed one line (break them up).

)

else:
raise ValueError(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would probably be more consistent with other code (and other suggestions) if the options were using an Enum object. You can check the DoE code, or Shammah's PR. It just makes it so that the strings are attached to an object instead (safer).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Enum class above, working to implement here. Making note to talk to shammah about this

"Invalid sampling method. Choose 'uniform', 'latin_hypercube', 'sobol' or 'user_provided'."
)

if multistart_sampling_method == "sobol" or multistart_sampling_method == "latin_hypercube":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the flow of the code being:

  1. Generate samples
  2. Load into dataframe
  3. Set up dataframe for success

However, I wonder if keeping each step inside it's own setup above makes the code more interpretable.

# Scale the samples to the range of the lower and upper bounds for each theta in theta_names
Comment thread
sscini marked this conversation as resolved.
theta_vals_multistart = np.array([lower_bound + (upper_bound - lower_bound) * theta for theta in samples])

# Create a DataFrame where each row is an initial theta vector for a restart,
# columns are theta_names, and values are the initial theta values for each restart
if multistart_sampling_method == "user_provided":
# If user_provided is a DataFrame, use its columns and values directly
if isinstance(user_provided, pd.DataFrame):
df_multistart = user_provided.copy()
df_multistart.columns = theta_names
else:
df_multistart = pd.DataFrame(theta_vals_multistart, columns=theta_names)
else:
# Ensure theta_vals_multistart is 2D (n_restarts, len(theta_names))
arr = np.atleast_2d(theta_vals_multistart)
if arr.shape[0] == 1 and n_restarts > 1:
arr = np.tile(arr, (n_restarts, 1))
df_multistart = pd.DataFrame(arr, columns=theta_names)

# Add columns for output info, initialized as nan
for name in theta_names:
df_multistart[f'converged_{name}'] = np.nan
df_multistart["initial objective"] = np.nan
df_multistart["final objective"] = np.nan
df_multistart["solver termination"] = np.nan
df_multistart["solve_time"] = np.nan

# Debugging output
# print(df_multistart)

return df_multistart

def _instance_creation_callback(self, experiment_number=None, cb_data=None):
model = self._create_parmest_model(experiment_number)
Expand Down Expand Up @@ -921,6 +1068,171 @@ def theta_est(
cov_n=cov_n,
)

def theta_est_multistart(
Comment thread
sscini marked this conversation as resolved.
Outdated
self,
n_restarts=20,
buffer=10,
Comment thread
sscini marked this conversation as resolved.
Outdated
multistart_sampling_method="uniform",
user_provided=None,
seed=None,
save_results=False,
theta_vals=None,
solver="ef_ipopt",
file_name = "multistart_results.csv",
return_values=[],
):
"""
Parameter estimation using multistart optimization

Parameters
----------
n_restarts: int, optional
Number of restarts for multistart. Default is 1.
Comment thread
sscini marked this conversation as resolved.
Outdated
th_sampling_method: string, optional
Method used to sample theta values. Options are "uniform", "latin_hypercube", or "sobol".
Default is "uniform".
buffer: int, optional
Number of iterations to save results dynamically. Default is 10.
user_provided: pd.DataFrame, optional
User provided dataframe of theta values for multistart optimization.
solver: string, optional
Comment thread
sscini marked this conversation as resolved.
Outdated
Currently only "ef_ipopt" is supported. Default is "ef_ipopt".
return_values: list, optional
List of Variable names, used to return values from the model for data reconciliation


Returns
-------
objectiveval: float
Comment thread
sscini marked this conversation as resolved.
Outdated
The objective function value
thetavals: pd.Series
Estimated values for theta
variable values: pd.DataFrame
Variable values for each variable name in return_values (only for solver='ef_ipopt')

"""

# check if we are using deprecated parmest
if self.pest_deprecated is not None:
return print(
"Multistart is not supported in the deprecated parmest interface"
)

assert isinstance(n_restarts, int)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check that this is > 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look at other Pyomo code fgor exampels of throwing exceptions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @adowling2 here, you need to throw an exception so you can test the exception is caught.

assert isinstance(multistart_sampling_method, str)
Comment thread
sscini marked this conversation as resolved.
Outdated
assert isinstance(solver, str)
assert isinstance(return_values, list)

if n_restarts > 1 and multistart_sampling_method is not None:

# Find the initialized values of theta from the labeled parmest model
# and the theta names from the estimator object

# print statement to indicate multistart optimization is starting
print(f"Starting multistart optimization with {n_restarts} restarts using {multistart_sampling_method} sampling method.")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this should be a logging output, not a print statement.


# @Reviewers, pyomo team: Use this or use instance creation callback?
theta_names = self._return_theta_names()
# Generate theta values using the sampling method
parmest_model_for_bounds = self._create_parmest_model(experiment_number=0)
results_df = self._generate_initial_theta(
parmest_model_for_bounds, seed=seed, n_restarts=n_restarts,
multistart_sampling_method=multistart_sampling_method, user_provided=user_provided
)
results_df = pd.DataFrame(results_df)
# Extract theta_vals from the dataframe
theta_vals = results_df.iloc[:, :len(theta_names)]
converged_theta_vals = np.zeros((n_restarts, len(theta_names)))

# Each restart uses a fresh model instance
for i in range(n_restarts):
# Create a fresh model for each restart
parmest_model = self._create_parmest_model(experiment_number=0)
theta_vals_current = theta_vals.iloc[i, :].to_dict()

# Set current theta values in the model
for name, value in theta_vals_current.items():
parmest_model.find_component(name).set_value(value)

# Optional: Print the current theta values being set
print(f"Setting {name} to {value}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about logging.

for name in theta_names:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Why is this not just included in the previous for loop?

current_value = parmest_model.find_component(name)()
print(f"Current value of {name} is {current_value}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about logging


# Call the _Q_opt method with the generated theta values
qopt_result = self._Q_opt(
bootlist=None,
solver=solver,
return_values=return_values,
)

# Unpack results
objectiveval, converged_theta = qopt_result


# Since _Q_opt does not return the solver result object, we cannot check
# solver termination condition directly here. Instead, we can assume
# that if converged_theta contains NaN, the solve failed.
if converged_theta.isnull().any():
Comment thread
sscini marked this conversation as resolved.
Outdated
solver_termination = "not successful"
solve_time = np.nan
thetavals = np.nan
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is never used?

final_objectiveval = np.nan
init_objectiveval = np.nan
else:
converged_theta_vals[i, :] = converged_theta.values
# Calculate the initial objective value using the current theta values
# Use the _Q_at_theta method to evaluate the objective at these theta values
init_objectiveval, _, _ = self._Q_at_theta(theta_vals_current)
final_objectiveval = objectiveval
solver_termination = "successful"

# plan to add solve time if available, @Reviewers, recommendations on how from current pyomo examples would
# be appreciated
solve_time = converged_theta.solve_time if hasattr(converged_theta, 'solve_time') else np.nan

# # Check if the objective value is better than the best objective value
# # Set a very high initial best objective value
# best_objectiveval = np.inf
# best_theta = np.inf
# if final_objectiveval < best_objectiveval:
# best_objectiveval = objectiveval
# best_theta = thetavals

print(f"Restart {i+1}/{n_restarts}: Objective Value = {final_objectiveval}, Theta = {converged_theta}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as other logging comments.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, using "out of" instead of "/" is probably a better choice.


# Store the results in the DataFrame for this restart
# Fill converged theta values
for j, name in enumerate(theta_names):
results_df.at[i, f'converged_{name}'] = converged_theta[j] if not np.isnan(converged_theta_vals[i, j]) else np.nan
# Fill initial and final objective values, solver termination, and solve time
results_df.at[i, "initial objective"] = init_objectiveval if 'init_objectiveval' in locals() else np.nan
results_df.at[i, "final objective"] = objectiveval if 'objectiveval' in locals() else np.nan
results_df.at[i, "solver termination"] = solver_termination if 'solver_termination' in locals() else np.nan
results_df.at[i, "solve_time"] = solve_time if 'solve_time' in locals() else np.nan

# Diagnostic: print the table after each restart
# print(results_df)

# Add buffer to save the dataframe dynamically, if save_results is True
if save_results and (i + 1) % buffer == 0:
mode = 'w' if i + 1 == buffer else 'a'
Comment thread
sscini marked this conversation as resolved.
Outdated
header = i + 1 == buffer
results_df.to_csv(
file_name, mode=mode, header=header, index=False
)
print(f"Intermediate results saved after {i + 1} iterations.")
Comment thread
sscini marked this conversation as resolved.
Outdated

Comment thread
sscini marked this conversation as resolved.
# Final save after all iterations
if save_results:
Comment thread
sscini marked this conversation as resolved.
Outdated
results_df.to_csv(file_name, mode='a', header=False, index=False)
print("Final results saved.")
Comment thread
sscini marked this conversation as resolved.
Outdated

return results_df # just this for now, then best_theta, best_objectiveval



def theta_est_bootstrap(
self,
bootstrap_samples,
Expand Down