Skip to content

Pyomo. DoE: Sensitivity initialization#3639

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

Pyomo. DoE: Sensitivity initialization#3639
smondal13 wants to merge 65 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

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?

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.

@mrmundt mrmundt requested review from adowling2 and mrmundt January 27, 2026 19:49
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.

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


def compute_correlation_matrix(
covariance_matrix, var_name: list = None, significant_digits=3
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 make the default for significant digits None.

Comment thread pyomo/contrib/doe/utils.py Outdated
var_name : list, optional
List of variable names corresponding to the rows/columns of the covariance matrix.
significant_digits : int, optional
Number of significant digits to round the correlation matrix to. Default: 3.
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.

Number of significant digits for rounding entries of the correlation matrix. Default: None (no rounding)

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?

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?

@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

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 on lines +280 to +282
Generates a multi-dimensional pattern for an arbitrary number of 1D array-like arguments.
This pattern is useful for generating patterns for sensitivity analysis when we want
to change one variable at a time. This function uses recursion and acts as a generator.
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 make sure these are wrapped within 88 columns.



def check_FIM(FIM):
def check_matrix(mat):
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 should be renamed to indicate what it is checking. Further, it is not checking and returning True/False, it is asserting (see, e.g. the difference between check_optimal_termination and assert_optimal_termination).

Maybe rename to something like assert_matrix_symmetric_positive_definite or assert_symmetric_positive_definite?


# Check if the FIM is positive definite
# Check if the matrix is positive definite
if np.min(evals) < -_SMALL_TOLERANCE_DEFINITENESS:
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.

Should these tolerances be passed as optional arguments that default to the global value if None?

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.

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.

Comment thread pyomo/contrib/doe/doe.py
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.

smondal13 and others added 9 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
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.

6 participants