Skip to content

Pyomo. DoE: Sensitivity initialization#3639

Open
smondal13 wants to merge 73 commits into
Pyomo:mainfrom
smondal13:sensitivity-initialization
Open

Pyomo. DoE: Sensitivity initialization#3639
smondal13 wants to merge 73 commits into
Pyomo:mainfrom
smondal13:sensitivity-initialization

Conversation

@smondal13
Copy link
Copy Markdown
Contributor

@smondal13 smondal13 commented Jun 23, 2025

Fixes # .

Summary/Motivation:

  • Initializing the model to a previously solved point in the design space can help the model converge at another point in the design space for sensitivity analysis
  • Better error messages in the method can help the user in debugging
  • Saving the file can be helpful if the model takes a long time to solve or if the user wants it.
  • The pattern generator in the utils.py file can be used in other cases where it can help with the initialization
  • Correlation matrix computation functionality will be helpful in computing correlation among the parameters

Changes proposed in this PR:

  • Added a new method compute_FIM_factorial() in Pyomo.DoE for sensitivity initialization
  • Added snake_traversal_grid_sampling() and compute_correlation_matrix() in utils.py

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

…pected. Now, I need to add cases when some of the design variables are not changed.
… use compute_FIM_metrics() method. Added both the nested for loop and change_one_design_at_a_time argument to compute_FIM_factorial() in doe.py. Everything is working fine.
…gn_values` does not need to be in the same order as the `experiment_inputs` as long as the key matches, we are good. Also, if we don't want to change a design variable, just not passing it as a key in the `design_values` dictionary will do the trick. Tested with the `reactor_example.py`
@smondal13
Copy link
Copy Markdown
Contributor Author

@adowling2 @djlaky, this draft PR is ready for initial review.

@adowling2
Copy link
Copy Markdown
Member

Does this PR depend on changes in #3575?

Copy link
Copy Markdown
Member

@adowling2 adowling2 left a comment

Choose a reason for hiding this comment

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

@smondal13 I left some initial comments.

Comment thread pyomo/contrib/doe/utils_updated.py Outdated
@@ -0,0 +1,267 @@
# ___________________________________________________________________________
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.

@smondal13 Why are your creating a new utils file instead of updating the current one? There might be a point of confusion regarding branches in git.

Copy link
Copy Markdown
Contributor Author

@smondal13 smondal13 Jul 1, 2025

Choose a reason for hiding this comment

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

This issue is resolved now. merged the utils file from my previous PR # 3525.

Comment thread pyomo/contrib/doe/doe.py Outdated

return self.fim_factorial_results

def compute_FIM_factorial(
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.

Was this function already in Pyomo.DoE somewhere else?

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.

No, I created this method. I do not see the same name anywhere else.

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.

@adowling2, we have compute_FIM_full_factorial(). I have not changed that method, rather I have added a new method. Maybe we can show a deprecation warning for compute_FIM_full_factorial()

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.

Yes, a depreciation warning sounds reasonable.

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.

Actually, I am really confused: why did you create a new method in the same file that did effectively the same thing? Does this replace the other method? Is it a superset of the other method? Do we need both?

Also, you might consider renaming this function: I don't know what a FIM factorial is (nor how to compute one). Maybe something like compute_FIM_from_full_factorial_sampling?

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.

@jsiirola This function is a superset of the already existing compute_FIM_full_factorial() method. So, I am not sure if I should keep it under the same name since the "full" in the method name can be misleading. It can use a full factorial (changing all the decision variables) and a fractional factorial (changing some of the decision variables). This method basically analyzes the sensitivity.

@adowling2, @slilonfe5, @snarasi2, @sscini Can we name it something like compute_sensitivity_at_design_points()? Or similar to the one @jsiirola suggested, compute_FIM_from_factorial_sampling?

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.

@smondal13 If sensitivity is what you are doing, I think compute_sensitivity_at_design_points is better.

Comment thread pyomo/contrib/doe/doe.py Outdated
# design_map_keys so that design_point can be constructed correctly in the loop.
des_ranges = [design_values[k.name] for k in design_map_keys]
if change_one_design_at_a_time:
factorial_points = generate_snake_zigzag_pattern(*des_ranges)
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.

Let's brainstorm different sensitivity analysis sequences we might use. We can then define an enum.

Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py
@smondal13 smondal13 changed the title Sensitivity initialization in Pyomo. DoE Pyomo. DoE: Sensitivity initialization Jun 25, 2025
@smondal13
Copy link
Copy Markdown
Contributor Author

smondal13 commented Jul 1, 2025

@adowling2 , @djlaky, @blnicho This PR is ready for design review.

Copy link
Copy Markdown
Member

@adowling2 adowling2 left a comment

Choose a reason for hiding this comment

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

Please see my requested changes.

Comment thread pyomo/contrib/doe/doe.py Outdated
traversal_scheme="snake_traversal",
file_name: str = None,
):
"""Will run a simulation-based factorial exploration of the experimental input
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.

Change "will run" to "performs"

Comment thread pyomo/contrib/doe/doe.py Outdated
----------
model : DoE model, optional
The model to perform the full factorial exploration on. Default: None
design_vals : dict,
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.

@mrmundt @blnicho Do you have any suggestions for avoiding string-based names? I know that we want to avoid string-based names, but I do not have any good ideas of how to do that in a way that is convenient for the user.

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'll let @blnicho also chime in but my initial thoughts are:

The best general practice is to accept component objects / references, not names. It is convenient (for users) to optionally accept names and resolve them internally. So if it's possible, support both, with a clear canonical form.

Comment thread pyomo/contrib/doe/doe.py Outdated
Comment thread pyomo/contrib/doe/doe.py

return self.factorial_results

# TODO: Overhaul plotting functions to not use strings
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.

Do we still want to overhaul the plotting functions to not use strings? Or do we just want this function to be consistent with our user interface above? In other words, if we use string-based names above, I think it it okay to use string-based names here.

Comment thread pyomo/contrib/doe/doe.py
def draw_factorial_figure(
self,
results=None,
sensitivity_design_variables=None,
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.

Let's update this user interface to match the function above. It appears that design variables as specified differently in these two functions. Likewise, this function should do design variable name error checking similar to the function above.

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.

Made the FIM metrics consistent in both compute_FIM_full_factorial() and compute_FIM_factorial(). Added design variable name error checking.

Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py
Comment thread pyomo/contrib/doe/utils.py Outdated

Parameters
----------
covariance_matrix : numpy.ndarray
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.

Can this be a square DataFrame? If yes, what type of error checking should we do on the column and row names?

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.

Good point. We now support square DataFrame input and have added explicit label checks: row labels must be unique, column labels must be unique, and row/column labels must match in the same order. If any of those fail, we raise a ValueError with a specific message.

Comment thread pyomo/contrib/doe/utils.py Outdated
# Set the index to the variable names if provided,
corr_df = (
pd.DataFrame(correlation_matrix, index=var_name, columns=var_name)
if var_name
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.

What is this logic doing here?

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.

This ternary block returns a pd DataFrame if var_name is passed; otherwise, it returns a numpy array.

@adowling2
Copy link
Copy Markdown
Member

@smondal13 Were you able to address the comments from my review in early February? If yes, I can re-review. Reply here.

@smondal13
Copy link
Copy Markdown
Contributor Author

@smondal13 Were you able to address the comments from my review in early February? If yes, I can re-review. Reply here.

Excluding the string related comments, the comments are addressed. You can probably wait until @blnicho or @mrmundt replies to that.

Comment thread pyomo/contrib/doe/tests/test_doe_errors.py
Comment thread pyomo/contrib/doe/doe.py Outdated

return self.fim_factorial_results

def compute_FIM_factorial(
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.

Actually, I am really confused: why did you create a new method in the same file that did effectively the same thing? Does this replace the other method? Is it a superset of the other method? Do we need both?

Also, you might consider renaming this function: I don't know what a FIM factorial is (nor how to compute one). Maybe something like compute_FIM_from_full_factorial_sampling?

Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/doe.py Outdated
Comment thread pyomo/contrib/doe/doe.py Outdated
2,
),
)
except:
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.

Bare except: is potentially problematic, as it catching everything including both solver errors and generic programming errors. At a minimum, you should include the exception in the log so that a user can get an idea as to why things failed.

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 the error message inside the logger warning.

Comment thread pyomo/contrib/doe/doe.py Outdated
Comment thread pyomo/contrib/doe/doe.py Outdated
Comment on lines +2378 to +2383
print("\n\n=========Factorial results===========")
print("Total points:", total_points)
print("Success count:", success_count)
print("Failure count:", failure_count)
print("\n")
print(res_df)
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.

We generally strongly discourage print() within Pyomo. Instead, output should be channeled via loggers or global streams (like sys.stdout) that are easily replaceable.

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.

I have repaced the print with sys.stdout

Comment thread pyomo/contrib/doe/doe.py Outdated
Comment on lines +2496 to +2499
if not isinstance(fixed_design_variables, dict):
raise ValueError("``fixed_design_variables`` must be a dictionary.")
if any(not isinstance(name, str) for name in fixed_design_variables.keys()):
raise ValueError("Fixed design variable names must be strings.")
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.

This kind of type checking is very "non-Pythonic" and something I would generally discourage. Python encourages duck-typing: as long as the variable behaves like the desired type, then it should be OK.

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.

I have removed the strict type checks,

smondal13 and others added 15 commits May 14, 2026 15:37
Co-authored-by: John Siirola <jsiirola@users.noreply.github.com>
…gn_vals input; add tests for component key normalization and error handling
… to abs_step/rel_step and update related logic; add test for factorial step formula using design span
…change to abs_step/rel_step in docstring and test case
…p_change and update related comments for clarity
Co-authored-by: John Siirola <jsiirola@users.noreply.github.com>
…ccess and update related functions. Run black
…ance error handling. Update related tests for consistency.
…prove output formatting and add validation for design mode arguments in compute_FIM_factorial.
…e unique row and column labels, and match between them. Add corresponding unit tests for validation.
Comment thread pyomo/contrib/doe/doe.py Outdated
rel_change: list = None,
n_designs: int = 5,
method="sequential",
df_settings=(True, None, None, 500),
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.

Should I use this tuple? I was reluctant to use 4 different arguments for the pd df setting / configuration

Comment thread pyomo/contrib/doe/doe.py Outdated
rel_change: list = None,
n_design_points: int = None,
method="sequential",
return_df=True,
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.

Since the method is printing the df depending on this flag, not returning it, does it make more sense to use show_df instead of return_df?

Comment thread pyomo/contrib/doe/utils.py
Comment thread pyomo/contrib/doe/utils.py Outdated
# Set the index to the variable names if provided,
corr_df = (
pd.DataFrame(correlation_matrix, index=var_name, columns=var_name)
if var_name
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.

This ternary block returns a pd DataFrame if var_name is passed; otherwise, it returns a numpy array.

Comment thread pyomo/contrib/doe/utils.py
Comment thread pyomo/contrib/doe/doe.py
factorial_points = snake_traversal_grid_sampling(*design_values)
elif scheme_enum == SensitivityInitialization.nested_for_loop:
factorial_points = product(*design_values)

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.

Great suggestion, and I implemented that split: traversal generation now lives in a dedicated helper (_materialize_factorial_points).
Current tests already cover traversal behavior through the public API (including invalid scheme errors and explicit nested-for-loop ordering on a 2x2 grid).

Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py Outdated
Comment thread pyomo/contrib/doe/utils.py Outdated

Parameters
----------
covariance_matrix : numpy.ndarray
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.

Good point. We now support square DataFrame input and have added explicit label checks: row labels must be unique, column labels must be unique, and row/column labels must match in the same order. If any of those fail, we raise a ValueError with a specific message.

Comment thread pyomo/contrib/doe/doe.py Outdated

return self.fim_factorial_results

def compute_FIM_factorial(
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.

@jsiirola Thanks for the suggestion. I agree and have refactored this in the latest update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready for design review

Development

Successfully merging this pull request may close these issues.

7 participants