From eb63f0149045d29717be55422176f9892a9b95ee Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Tue, 16 Jun 2026 16:54:43 -0400 Subject: [PATCH 1/2] Fix: constraint Jacobian scaling with pyOptSparse Summary of changes; - Fixed an issue related to the scaling of the constraint Jacobian when the `rhs` value for a Flume constraint was not equal to 1.0 or 0.0. pyOptSparse automatically applies the value for `scale` to the constraint value and the Jacobian entries, so manually scaling the Jacobian was incorrect - Removed the `it_counter` parameter from the System class within the `reset_analysis_flags` method. Now, the analysis flags are reset every time this method is called, regardless of the iteration number. This would have skipped the reset previously if `it_counter = 0`, which caused numerical issues for SNOPT's derivative check - Modified the tests for the ParOpt and pyOptSparse interfaces to reflect these changes --- flume/base_classes/system.py | 26 ++++++++++------------- flume/interfaces/paropt_interface.py | 8 +++---- flume/interfaces/pyoptsparse_interface.py | 15 +++++-------- flume/interfaces/scipy_interface.py | 6 +++--- pyproject.toml | 2 +- tests/test_paropt_interface.py | 2 +- tests/test_pyoptsparse_interface.py | 4 ++-- 7 files changed, 26 insertions(+), 37 deletions(-) diff --git a/flume/base_classes/system.py b/flume/base_classes/system.py index 7da15c9..dc7149d 100644 --- a/flume/base_classes/system.py +++ b/flume/base_classes/system.py @@ -59,26 +59,22 @@ def __init__( return - def reset_analysis_flags(self, it_counter): + def reset_analysis_flags(self): """ - Resets all of the analysis flags for all analyses within the system to be False. This is done when variable values are updated, as all systems must be analyzed again to propagate chanes in design variable values. + Resets all of the analysis flags for all analyses within the system to be False. This is done when variable values are updated, as all systems must be analyzed again to propagate changes in design variable values. """ - # If it_counter is zero, skip this setp - if it_counter == 0: - pass - else: - # Loop through each top-level analysis, resetting each analysis in the stack - for top_level in self.top_level_analysis_list: + # Loop through each top-level analysis, resetting each analysis in the stack + for top_level in self.top_level_analysis_list: - # If the stack does not exist, make it - if not hasattr(top_level, "stack"): - # Assemble the stack - top_level.stack = top_level._make_stack() + # If the stack does not exist, make it + if not hasattr(top_level, "stack"): + # Assemble the stack + top_level.stack = top_level._make_stack() - # Loop through each object within the current top-level's analysis stack and set the analyzed attribute to be False - for analysis in top_level.stack: - analysis.analyzed = False + # Loop through each object within the current top-level's analysis stack and set the analyzed attribute to be False + for analysis in top_level.stack: + analysis.analyzed = False return diff --git a/flume/interfaces/paropt_interface.py b/flume/interfaces/paropt_interface.py index c671dfd..0770e8e 100644 --- a/flume/interfaces/paropt_interface.py +++ b/flume/interfaces/paropt_interface.py @@ -201,7 +201,7 @@ def getVarsAndBounds(self, x, lb, ub): return - def set_system_variables(self, x, it_counter): + def set_system_variables(self, x): """ Sets the variable values for the analysis objects contained within the System. @@ -209,12 +209,10 @@ def set_system_variables(self, x, it_counter): ---------- x : ParOptVec Design variable vector at the current iteration - it_counter : int - Current iteration for the optimization """ # Since system variables are being set, all analysis objects must be recomputed - self.flume_sys.reset_analysis_flags(it_counter) + self.flume_sys.reset_analysis_flags() # Loop through the design variables for the system and set for their components for var in self.flume_sys.design_vars_info: @@ -279,7 +277,7 @@ def evalObjCon(self, x): ) # Set the variable values for the various analyses - self.set_system_variables(x, self.it_counter) + self.set_system_variables(x) # Perform the analysis for the objective function self.flume_sys.obj_analysis.analyze(debug_print=False) diff --git a/flume/interfaces/pyoptsparse_interface.py b/flume/interfaces/pyoptsparse_interface.py index 8a9ab5d..a9834ca 100644 --- a/flume/interfaces/pyoptsparse_interface.py +++ b/flume/interfaces/pyoptsparse_interface.py @@ -51,7 +51,7 @@ def user_update(it_number): # Initialize the iteration counter self.it_counter = 0 - def _set_system_variables(self, xdict: dict, it_counter: int): + def _set_system_variables(self, xdict: dict): """ Private method that is used to set the design variables values provided in the input dictionary into the various Flume Analysis objects. @@ -59,12 +59,10 @@ def _set_system_variables(self, xdict: dict, it_counter: int): ---------- xdict : dict Dictionary that specifies the current design point for the optimization problem. The keys are the global variable names, and the values are the numeric values for the design variables in the System - it_counter : int - Iteration number for the optimization """ # Since system variables are being set, all analysis objects must be recomputed - self.flume_sys.reset_analysis_flags(it_counter) + self.flume_sys.reset_analysis_flags() # Loop through the design variables for the system and set the values for their components for var in self.flume_sys.design_vars_info: @@ -122,7 +120,7 @@ def _objconfun(self, xdict: dict): ) # Set the variable values for the various analyses - self._set_system_variables(xdict, self.it_counter) + self._set_system_variables(xdict) # Perform the analysis for the objective function self.flume_sys.obj_analysis.analyze(debug_print=False) @@ -250,10 +248,6 @@ def _objconGradFun(self, xdict: dict, funcs: dict): .deriv ) - # Apply the scaling, if necessary - if rhs != 0.0: - gradc_i /= abs(rhs) - # Assign the gradient value into the grad_vals dictionary if i > 0: current = grad_vals[con_name][var] @@ -359,7 +353,7 @@ def _addConGroups(self, optProb: Optimization): scale = 1.0 else: scale = 1 / abs(rhs_val) - bound_val = np.sign(rhs_val) + bound_val = rhs_val # Greater than inequality constraints if direction == "geq": @@ -577,6 +571,7 @@ def _update_options_output_directory(self, optimizer: str, opt: Optimizer): options["Summary file"] = os.path.join( self.flume_sys.log_prefix, "SNOPT_summary.out" ) + options["Verify level"] = 3 elif optimizer.lower() == "ipopt": options = opt.getOptions() options["output_file"] = os.path.join( diff --git a/flume/interfaces/scipy_interface.py b/flume/interfaces/scipy_interface.py index fb71eee..6f48963 100644 --- a/flume/interfaces/scipy_interface.py +++ b/flume/interfaces/scipy_interface.py @@ -142,13 +142,13 @@ def _get_dv_bounds(self, x): return bounds - def _set_system_variables(self, x, it_counter): + def _set_system_variables(self, x): """ Sets the variable values for the analysis objects contained within the system. """ # Since system variables are being set, all analysis objects must be recomputed - self.flume_sys.reset_analysis_flags(it_counter - 1) + self.flume_sys.reset_analysis_flags() # Loop through the design variables for the system and set for their components for var in self.flume_sys.design_vars_info: @@ -229,7 +229,7 @@ def _objective_func(self, x, method): ) # Set the variable values for the various analyses - self._set_system_variables(x, self.it_counter) + self._set_system_variables(x) # Perform the analysis for the objective function self.flume_sys.obj_analysis.analyze(debug_print=False) diff --git a/pyproject.toml b/pyproject.toml index 264ecfe..9cb5c5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "flume-smdo" -version = "1.0.5" +version = "1.0.6" description = "A framework for performing forward and derivative analyses for systems following directed acylic graph structure." readme = "README.md" authors = [{ name = "Cameron Smith", email = "csmith763@gatech.edu" }] diff --git a/tests/test_paropt_interface.py b/tests/test_paropt_interface.py index 50f9d75..bb0641c 100644 --- a/tests/test_paropt_interface.py +++ b/tests/test_paropt_interface.py @@ -247,7 +247,7 @@ def optimize_system(self, n_p: int, maxit: int = 100): self.positions.set_var_values(variables={"theta": theta0, "phi": phi0}) # Optimize the problem with ParOpt's TR method - x, fstar, con_star = interface.optimize_system(algorithm="tr") + x, fstar, con_star, opt = interface.optimize_system(algorithm="tr") # Check that the potential energy at the final point matches the expected value obj_val = fstar diff --git a/tests/test_pyoptsparse_interface.py b/tests/test_pyoptsparse_interface.py index 9f7c5d6..8199d3d 100644 --- a/tests/test_pyoptsparse_interface.py +++ b/tests/test_pyoptsparse_interface.py @@ -134,7 +134,7 @@ def setUp(self): # Declare the constraint sys.declare_constraints( - global_con_name={"con.g": {"direction": "leq", "rhs": 1.0}} + global_con_name={"con.g": {"direction": "geq", "rhs": -2.0}} ) # Store the system as an attribute @@ -165,7 +165,7 @@ def test_optimization_pyoptsparse_slsqp(self): x = np.concatenate((sol.xStar["dvs.x_dv"], sol.xStar["dvs.y_dv"])) # Set the expected optimal values - xstar = np.array([0.786, 0.618]) + xstar = np.array([1.0, 1.0]) # Check that the values match np.testing.assert_allclose( From 95e133bffebb5da5fea49204992150711a8da222 Mon Sep 17 00:00:00 2001 From: Cameron Smith Date: Tue, 16 Jun 2026 17:02:54 -0400 Subject: [PATCH 2/2] Test: removed ParOpt gradient check --- tests/test_paropt_interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_paropt_interface.py b/tests/test_paropt_interface.py index bb0641c..98a91e4 100644 --- a/tests/test_paropt_interface.py +++ b/tests/test_paropt_interface.py @@ -247,7 +247,9 @@ def optimize_system(self, n_p: int, maxit: int = 100): self.positions.set_var_values(variables={"theta": theta0, "phi": phi0}) # Optimize the problem with ParOpt's TR method - x, fstar, con_star, opt = interface.optimize_system(algorithm="tr") + x, fstar, con_star, opt = interface.optimize_system( + algorithm="tr", check_gradients=False + ) # Check that the potential energy at the final point matches the expected value obj_val = fstar