From da60b5a515c67712a58cafbe72e87df75b63ab68 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 18 Apr 2024 13:05:36 -0600 Subject: [PATCH 01/36] add callback that computes condition number of the jacobian --- svi/cyipopt.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/svi/cyipopt.py b/svi/cyipopt.py index be5f3a3..98f76bd 100644 --- a/svi/cyipopt.py +++ b/svi/cyipopt.py @@ -14,6 +14,8 @@ from pyomo.core.base import Block, Objective, minimize from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense from pyomo.opt.results.solution import Solution +import numpy as np +from scipy import sparse pyomo_nlp = attempt_import("pyomo.contrib.pynumero.interfaces.pyomo_nlp")[0] pyomo_grey_box = attempt_import("pyomo.contrib.pynumero.interfaces.pyomo_grey_box_nlp")[ @@ -327,3 +329,46 @@ def __call__( ls_trials, ) ) + +class ConditioningCallback: + + def __init__(self): + self.iterate_data = [] + self.condition_numbers = [] + + def __call__( + self, + nlp, + # Don't include this argument, for compatibility with current CyIpopt + # interface + #ipopt_problem, + alg_mod, + iter_count, + obj_value, + inf_pr, + inf_du, + mu, + d_norm, + regularization_size, + alpha_du, + alpha_pr, + ls_trials, + ): + self.iterate_data.append( + ( + alg_mod, + iter_count, + obj_value, + inf_pr, + inf_du, + mu, + d_norm, + regularization_size, + alpha_du, + alpha_pr, + ls_trials, + ) + ) + jac = nlp.evaluate_jacobian() + cond = np.linalg.cond(jac.toarray()) + self.condition_numbers.append(cond) From 2ac6f80e2bd7a1c642aa39ae63c0a3049238d00c Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 21 Apr 2024 21:28:18 -0600 Subject: [PATCH 02/36] add option to compute surrogate errors --- .../compute_surrogate_error.py | 112 ++++++++++++++++++ svi/auto_thermal_reformer/config.py | 10 ++ svi/auto_thermal_reformer/run_alamo_sweep.py | 54 +++++++++ 3 files changed, 176 insertions(+) create mode 100644 svi/auto_thermal_reformer/compute_surrogate_error.py diff --git a/svi/auto_thermal_reformer/compute_surrogate_error.py b/svi/auto_thermal_reformer/compute_surrogate_error.py new file mode 100644 index 0000000..8eb7cbd --- /dev/null +++ b/svi/auto_thermal_reformer/compute_surrogate_error.py @@ -0,0 +1,112 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import pyomo.environ as pyo +from pyomo.common.timing import TicTocTimer +from pyomo.util.subsystems import TemporarySubsystemManager +from pyomo.contrib.incidence_analysis import solve_strongly_connected_components +from svi.auto_thermal_reformer.reactor_model import create_instance + + +def compute_surrogate_error(model): + model_surrogate = model.fs.reformer_surrogate + + model_surrogate_outputs = { + key: var.value for key, var in model_surrogate.output_vars_as_dict().items() + } + + inputs = [ + model.fs.reformer_bypass.reformer_outlet.flow_mol[0], + model.fs.reformer_bypass.reformer_outlet.temperature[0], + model.fs.reformer_mix.steam_inlet.flow_mol[0], + model.fs.reformer.conversion, + ] + + scc_solver = pyo.SolverFactory("ipopt") + + timer = TicTocTimer() + timer.tic() + + with TemporarySubsystemManager(to_fix=inputs): + surrogate_copy = model_surrogate.clone() + timer.toc("clone-surrogate") + solve_strongly_connected_components(surrogate_copy, solver=scc_solver) + timer.toc("solve-scc-surrogate") + surrogate_res = scc_solver.solve(surrogate_copy) + if not pyo.check_optimal_termination(surrogate_res): + raise ValueError("Surrogate reactor model failed to simulate") + + # If we have converged, these newly computed surrogate outputs should be + # the same as our "model surrogate outputs" above. If we did not converge, + # they may be different. + surrogate_outputs = { + key: var.value for key, var in surrogate_copy.output_vars_as_dict().items() + } + + fullspace_model = create_instance( + model.fs.reformer.conversion.value, + model.fs.reformer_mix.steam_inlet.flow_mol[0].value, + # Note that "reformer_outlet" here means "the outlet of the bypass splitter + # that goes to the reformer". The other outlet is called "bypass_outlet". + # The inlet is simply called "inlet". + model.fs.reformer_bypass.reformer_outlet.flow_mol[0].value, + model.fs.reformer_bypass.reformer_outlet.temperature[0].value, + ) + timer.toc("create-instance-fullspace") + solve_strongly_connected_components( + fullspace_model, + solver=scc_solver, + use_calc_var=False, + ) + timer.toc("solve-scc-fullspace") + fullspace_res = scc_solver.solve(fullspace_model, tee=True) + timer.toc("solve-fullspace") + + if not pyo.check_optimal_termination(fullspace_res): + # In parameter sweep scripts, this ValueError is caught and we + # write an empty row in the surrogate-error file. + # But we don't want to count a failure for the surrogate if this + # fails... this can be handled by the caller. + raise ValueError("Fullspace reactor model failed to simulate") + + fullspace_output = { + "Fout": fullspace_model.fs.reformer.outlet.flow_mol[0].value, + "Tout": fullspace_model.fs.reformer.outlet.temperature[0].value, + "HeatDuty": fullspace_model.fs.reformer.heat_duty[0].value, + } + for key in surrogate_outputs: + # The keys we have not added so far are component names, which may + # be used as indices to mole_frac_comp + if key not in fullspace_output: + fullspace_output[key] = fullspace_model.fs.reformer.outlet.mole_frac_comp[0, key].value + + relative_errors = { + key: ( + abs(fullspace_output[key] - surrogate_outputs[key]) + / max(1, fullspace_output[key], surrogate_outputs[key]) + ) + for key in surrogate_outputs + } + #max_relative_error = max(relative_errors.values()) + #ave_relative_error = sum(relative_errors.values()) / len(relative_errors) + timer.toc("done") + + return relative_errors diff --git a/svi/auto_thermal_reformer/config.py b/svi/auto_thermal_reformer/config.py index f573806..563e744 100644 --- a/svi/auto_thermal_reformer/config.py +++ b/svi/auto_thermal_reformer/config.py @@ -130,3 +130,13 @@ def get_parameter_samples(args): subset = [int(i) for i in subset] xp_samples = [xp_samples[i] for i in subset] return xp_samples + +def get_plot_argparser(): + argparser = get_argparser() + argparser.add_argument("--show", action="store_true", help="Flag to show the plot") + argparser.add_argument("--no-save", action="store_true", help="Flag to not save the plot") + argparser.add_argument("--plot-fname", default=None, help="Basename for plot file") + argparser.add_argument("--no-legend", action="store_true", help="Flag to exclude a legend") + argparser.add_argument("--title", default=None, help="Plot title") + argparser.add_argument("--show-training-bounds", action="store_true") + return argparser diff --git a/svi/auto_thermal_reformer/run_alamo_sweep.py b/svi/auto_thermal_reformer/run_alamo_sweep.py index b83dca9..e4ab8f8 100644 --- a/svi/auto_thermal_reformer/run_alamo_sweep.py +++ b/svi/auto_thermal_reformer/run_alamo_sweep.py @@ -34,6 +34,7 @@ DEFAULT_SURROGATE_FNAME, ) import svi.auto_thermal_reformer.config as config +from svi.auto_thermal_reformer.compute_surrogate_error import compute_surrogate_error INVALID = None @@ -47,6 +48,11 @@ def main(): default="alamo-sweep.csv", help="Base file name for parameter sweep results", ) + argparser.add_argument( + "--compute-surrogate-error", + action="store_true", + help="Compute surrogate error (at termination point) and write to a separate results file", + ) args = argparser.parse_args() # TODO: This should be configurable by CLI @@ -55,6 +61,9 @@ def main(): df = {key: [] for key in config.PARAM_SWEEP_KEYS} + if args.compute_surrogate_error: + surrogate_error = {"X": [], "P": [], "surrogate-error": []} + """ The optimization problem to solve is the following: Maximize H2 composition in the product stream such that its minimum flow is 3500 mol/s, @@ -104,6 +113,28 @@ def main(): for key in config.PARAM_SWEEP_KEYS: if key not in ("X", "P", "Termination"): df[key].append(INVALID) + + # Regardless of whether the solve was successful, if we didn't encounter + # an error, attempt to compute the surrogate error at the point of + # termination. + if args.compute_surrogate_error: + # Computing this surrogate error involves two simulations. What + # if either fails? + surrogate_error["X"].append(X) + surrogate_error["P"].append(P) + try: + surr_err = compute_surrogate_error(m) + # Maximum relative error, defined as: + # |a - b| / max(|a|, |b|, 1) + max_surr_err = max(surr_err.values()) + surrogate_error["surrogate-error"].append(max_surr_err) + except ValueError: + # We failed to simulate either the full-space or surrogate + # reactor model. + surrogate_error["surrogate-error"].append(None) + + #finally: + # pass except ValueError: df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) @@ -112,11 +143,34 @@ def main(): if key not in ("X", "P", "Termination"): df[key].append(INVALID) + if args.compute_surrogate_error: + # If we encountered an error, do not attempt to compute surrogate + # error. A solution has not been loaded, so the value we would + # get has nothing to do with the formulation or parameter values. + surrogate_error["X"].append(X) + surrogate_error["P"].append(P) + surrogate_error["surrogate-error"].append(INVALID) + df = pd.DataFrame(df) print(df) if not args.no_save: df.to_csv(output_fpath) + if args.compute_surrogate_error: + surrogate_error_df = pd.DataFrame(surrogate_error) + + # TODO: Allow specification of a file for surrogate error + sweep_basename = args.fname + if "." in sweep_basename: + experiment_extension = "." + sweep_basename.split(".")[-1] + name = sweep_basename[:-len(experiment_extension)] + error_fname = name + "-surrogate-error" + experiment_extension + else: + error_fname = sweep_basename + "-surrogate-error" + error_fpath = os.path.join(args.data_dir, error_fname) + + surrogate_error_df.to_csv(error_fpath) + if __name__ == "__main__": main() From fe74f6b695a38659feb07211fe407eef7468fb22 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 21 Apr 2024 21:29:30 -0600 Subject: [PATCH 03/36] function to get gradient of lagrangian in cyipopt --- svi/cyipopt.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/svi/cyipopt.py b/svi/cyipopt.py index 98f76bd..8d16e2a 100644 --- a/svi/cyipopt.py +++ b/svi/cyipopt.py @@ -372,3 +372,29 @@ def __call__( jac = nlp.evaluate_jacobian() cond = np.linalg.cond(jac.toarray()) self.condition_numbers.append(cond) + +def get_gradient_of_lagrangian( + nlp, + primal_lb_multipliers, + primal_ub_multipliers, +): + # PyNumero NLPs contain constraint multipliers, but does not define a convention. + # We still need: + # - primal LB/UB multipliers + # We should not need slack multipliers (Ipopt should take care of this...) + grad_obj = nlp.evaluate_grad_objective() + + # There is no way this works. We will probably need to separate equality and + # inequality multipliers. + jac = nlp.evaluate_jacobian() + duals = nlp.get_duals() + # Each constraint gradient times its multiplier + conjac_term = jac.transpose().dot(duals) + + grad_lag = ( + - grad_obj + - conjac_term + + primal_lb_multipliers + - primal_ub_multipliers + ) + return grad_lag From ff3e5a3589b41a2eac49248585be2535b8541940 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 21 Apr 2024 21:33:44 -0600 Subject: [PATCH 04/36] initial draft of surrogate error plotting function --- .../plot_surrogate_error.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 svi/auto_thermal_reformer/plot_surrogate_error.py diff --git a/svi/auto_thermal_reformer/plot_surrogate_error.py b/svi/auto_thermal_reformer/plot_surrogate_error.py new file mode 100644 index 0000000..a3c1815 --- /dev/null +++ b/svi/auto_thermal_reformer/plot_surrogate_error.py @@ -0,0 +1,89 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# __________________________________________________________________________ + +import os +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import svi.auto_thermal_reformer.config as config + + +def plot_surrogate_error(df, legend=True, title=None, show_training_bounds=False): + fig = plt.figure() + + conversion_list = list(sorted(set(df["X"]))) + pressure_list = list(sorted(set(df["P"]))) + n_conversion = len(conversion_list) + n_pressure = len(pressure_list) + error_lookup = { + (df["X"][i], df["P"][i]): df["surrogate-error"][i] + for i in range(len(df)) + } + error_array = [ + [error_lookup[conversion_list[j], pressure_list[i]] for i in range(n_pressure)] + for j in range(n_conversion) + ] + + ax = sns.heatmap( + error_array, + ) + + if title is not None: + ax.set_title(title) + + plt.gca().invert_yaxis() + + return fig, ax + + +def main(args): + df = pd.read_csv(args.error_fpath) + + fig, ax = plot_surrogate_error( + df, + legend=not args.no_legend, + title=args.title, + show_training_bounds=args.show_training_bounds, + ) + + if not args.no_save: + if args.plot_fname is None: + plot_fname = os.path.basename(args.error_fpath) + data_ext = "." + plot_fname.split(".")[-1] + ext_len = len(data_ext) + plot_fname = plot_fname[:-ext_len] + "-surrogate-error" + ".pdf" + else: + plot_fname = args.plot_fname + + plot_fpath = os.path.join(args.results_dir, plot_fname) + fig.savefig(plot_fpath, transparent=True) + + if args.show: + plt.show() + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + argparser.add_argument( + "error_fpath", help="Path to CSV file containing surrogate errors to plot" + ) + args = argparser.parse_args() + main(args) From eb6b192e337981d23a1d71cb97568a0429b5c4b1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 21 Apr 2024 22:05:05 -0600 Subject: [PATCH 05/36] add initialize option to reactor model --- svi/auto_thermal_reformer/reactor_model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/svi/auto_thermal_reformer/reactor_model.py b/svi/auto_thermal_reformer/reactor_model.py index 3b63d5f..6d07242 100644 --- a/svi/auto_thermal_reformer/reactor_model.py +++ b/svi/auto_thermal_reformer/reactor_model.py @@ -45,6 +45,7 @@ def add_reactor_model( flow_mol_h2o=300, flow_mol_gas=750, temp_gas=750, + initialize=True, ): """Add reactor unit model to provided Pyomo/IDAES model @@ -134,7 +135,8 @@ def add_reactor_model( m.fs.reformer_mix.initialize() propagate_state(arc=m.fs.connect) - m.fs.reformer.initialize() + if initialize: + m.fs.reformer.initialize() ######### SET REFORMER OUTLET PRESSURE ######### m.fs.reformer.outlet.pressure[0].fix(137895) @@ -158,6 +160,7 @@ def create_instance( flow_mol_h2o=300, flow_mol_gas=750, temp_gas=750, + initialize=True, ): # Set up global information m = ConcreteModel() @@ -172,6 +175,7 @@ def create_instance( flow_mol_h2o=flow_mol_h2o, flow_mol_gas=flow_mol_gas, temp_gas=temp_gas, + initialize=initialize, ) return m From 59a6af9d6193e52ff80510e72910252cc34518ca Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 21 Apr 2024 22:05:41 -0600 Subject: [PATCH 06/36] option to compute surrogate error in NN sweep --- .../compute_surrogate_error.py | 37 +++++++++---- svi/auto_thermal_reformer/run_nn_sweep.py | 53 +++++++++++++++++++ 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/svi/auto_thermal_reformer/compute_surrogate_error.py b/svi/auto_thermal_reformer/compute_surrogate_error.py index 8eb7cbd..630258f 100644 --- a/svi/auto_thermal_reformer/compute_surrogate_error.py +++ b/svi/auto_thermal_reformer/compute_surrogate_error.py @@ -23,6 +23,7 @@ from pyomo.common.timing import TicTocTimer from pyomo.util.subsystems import TemporarySubsystemManager from pyomo.contrib.incidence_analysis import solve_strongly_connected_components +from idaes.core.util.exceptions import InitializationError from svi.auto_thermal_reformer.reactor_model import create_instance @@ -52,6 +53,7 @@ def compute_surrogate_error(model): timer.toc("solve-scc-surrogate") surrogate_res = scc_solver.solve(surrogate_copy) if not pyo.check_optimal_termination(surrogate_res): + print("WARNING: Surrogate model failed to simulate") raise ValueError("Surrogate reactor model failed to simulate") # If we have converged, these newly computed surrogate outputs should be @@ -61,16 +63,28 @@ def compute_surrogate_error(model): key: var.value for key, var in surrogate_copy.output_vars_as_dict().items() } - fullspace_model = create_instance( - model.fs.reformer.conversion.value, - model.fs.reformer_mix.steam_inlet.flow_mol[0].value, - # Note that "reformer_outlet" here means "the outlet of the bypass splitter - # that goes to the reformer". The other outlet is called "bypass_outlet". - # The inlet is simply called "inlet". - model.fs.reformer_bypass.reformer_outlet.flow_mol[0].value, - model.fs.reformer_bypass.reformer_outlet.temperature[0].value, - ) - timer.toc("create-instance-fullspace") + try: + fullspace_model = create_instance( + model.fs.reformer.conversion.value, + model.fs.reformer_mix.steam_inlet.flow_mol[0].value, + # Note that "reformer_outlet" here means "the outlet of the bypass splitter + # that goes to the reformer". The other outlet is called "bypass_outlet". + # The inlet is simply called "inlet". + model.fs.reformer_bypass.reformer_outlet.flow_mol[0].value, + model.fs.reformer_bypass.reformer_outlet.temperature[0].value, + initialize=True, + ) + timer.toc("create-instance-fullspace") + except InitializationError: + print("WARNING: Full-space model failed to initialize. Trying to continue.") + fullspace_model = create_instance( + model.fs.reformer.conversion.value, + model.fs.reformer_mix.steam_inlet.flow_mol[0].value, + model.fs.reformer_bypass.reformer_outlet.flow_mol[0].value, + model.fs.reformer_bypass.reformer_outlet.temperature[0].value, + initialize=False, + ) + solve_strongly_connected_components( fullspace_model, solver=scc_solver, @@ -85,7 +99,8 @@ def compute_surrogate_error(model): # write an empty row in the surrogate-error file. # But we don't want to count a failure for the surrogate if this # fails... this can be handled by the caller. - raise ValueError("Fullspace reactor model failed to simulate") + print("WARNING: Full-space model failed to simulate") + raise ValueError("Full-space reactor model failed to simulate") fullspace_output = { "Fout": fullspace_model.fs.reformer.outlet.flow_mol[0].value, diff --git a/svi/auto_thermal_reformer/run_nn_sweep.py b/svi/auto_thermal_reformer/run_nn_sweep.py index 3a006c1..723fb63 100644 --- a/svi/auto_thermal_reformer/run_nn_sweep.py +++ b/svi/auto_thermal_reformer/run_nn_sweep.py @@ -35,6 +35,7 @@ ) from idaes.core.surrogate.keras_surrogate import KerasSurrogate import svi.auto_thermal_reformer.config as config +from svi.auto_thermal_reformer.compute_surrogate_error import compute_surrogate_error INVALID = None @@ -70,6 +71,11 @@ def main(): " Must be 'full' or 'reduced'." ), ) + argparser.add_argument( + "--compute-surrogate-error", + action="store_true", + help="Compute surrogate error (at termination point) and write to a separate results file", + ) args = argparser.parse_args() @@ -77,12 +83,17 @@ def main(): if args.fname is None: sweep_fname = f"nn-sweep-{args.formulation}.csv" + else: + sweep_fname = args.fname surrogate_fname = os.path.join(args.data_dir, args.surrogate_fname) output_fpath = os.path.join(args.data_dir, sweep_fname) df = {key: [] for key in config.PARAM_SWEEP_KEYS} + if args.compute_surrogate_error: + surrogate_error = {"X": [], "P": [], "surrogate-error": []} + """ The optimization problem to solve is the following: Maximize H2 composition in the product stream such that its minimum flow is 3500 mol/s, @@ -134,6 +145,25 @@ def main(): for key in config.PARAM_SWEEP_KEYS: if key not in ("X", "P", "Termination"): df[key].append(INVALID) + + # Regardless of whether the solve was successful, if we didn't encounter + # an error, attempt to compute the surrogate error at the point of + # termination. + if args.compute_surrogate_error: + # Computing this surrogate error involves two simulations. What + # if either fails? + surrogate_error["X"].append(X) + surrogate_error["P"].append(P) + try: + surr_err = compute_surrogate_error(m) + # Maximum relative error, defined as: + # |a - b| / max(|a|, |b|, 1) + max_surr_err = max(surr_err.values()) + surrogate_error["surrogate-error"].append(max_surr_err) + except ValueError: + # We failed to simulate either the full-space or surrogate + # reactor model. + surrogate_error["surrogate-error"].append(None) except ValueError: df[list(df.keys())[0]].append(X) @@ -145,8 +175,31 @@ def main(): if key not in ("X", "P", "Termination"): df[key].append(INVALID) + if args.compute_surrogate_error: + # If we encountered an error, do not attempt to compute surrogate + # error. A solution has not been loaded, so the value we would + # get has nothing to do with the formulation or parameter values. + surrogate_error["X"].append(X) + surrogate_error["P"].append(P) + surrogate_error["surrogate-error"].append(INVALID) + df = pd.DataFrame(df) df.to_csv(output_fpath) + if args.compute_surrogate_error: + surrogate_error_df = pd.DataFrame(surrogate_error) + + # TODO: Allow specification of a file for surrogate error + sweep_basename = sweep_fname + if "." in sweep_basename: + experiment_extension = "." + sweep_basename.split(".")[-1] + name = sweep_basename[:-len(experiment_extension)] + error_fname = name + "-surrogate-error" + experiment_extension + else: + error_fname = sweep_basename + "-surrogate-error" + error_fpath = os.path.join(args.data_dir, error_fname) + + surrogate_error_df.to_csv(error_fpath) + if __name__ == "__main__": main() From d5b9445ab26fb86be5fb420c748d14a4945e6a82 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 21 Apr 2024 22:19:40 -0600 Subject: [PATCH 07/36] add --opaque option --- svi/auto_thermal_reformer/config.py | 1 + svi/auto_thermal_reformer/plot_surrogate_error.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/svi/auto_thermal_reformer/config.py b/svi/auto_thermal_reformer/config.py index 563e744..af64904 100644 --- a/svi/auto_thermal_reformer/config.py +++ b/svi/auto_thermal_reformer/config.py @@ -139,4 +139,5 @@ def get_plot_argparser(): argparser.add_argument("--no-legend", action="store_true", help="Flag to exclude a legend") argparser.add_argument("--title", default=None, help="Plot title") argparser.add_argument("--show-training-bounds", action="store_true") + argparser.add_argument("--opaque", action="store_true", help="Not transparent") return argparser diff --git a/svi/auto_thermal_reformer/plot_surrogate_error.py b/svi/auto_thermal_reformer/plot_surrogate_error.py index a3c1815..dd4301d 100644 --- a/svi/auto_thermal_reformer/plot_surrogate_error.py +++ b/svi/auto_thermal_reformer/plot_surrogate_error.py @@ -26,7 +26,12 @@ import svi.auto_thermal_reformer.config as config -def plot_surrogate_error(df, legend=True, title=None, show_training_bounds=False): +def plot_surrogate_error( + df, + legend=True, + title=None, + show_training_bounds=False, +): fig = plt.figure() conversion_list = list(sorted(set(df["X"]))) @@ -69,12 +74,12 @@ def main(args): plot_fname = os.path.basename(args.error_fpath) data_ext = "." + plot_fname.split(".")[-1] ext_len = len(data_ext) - plot_fname = plot_fname[:-ext_len] + "-surrogate-error" + ".pdf" + plot_fname = plot_fname[:-ext_len] + ".pdf" else: plot_fname = args.plot_fname plot_fpath = os.path.join(args.results_dir, plot_fname) - fig.savefig(plot_fpath, transparent=True) + fig.savefig(plot_fpath, transparent=not args.opaque) if args.show: plt.show() From a936847e9b15ef976da4af064bccac6750c48da4 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Sat, 27 Apr 2024 13:50:44 -0600 Subject: [PATCH 08/36] plot condition numbers for each formulation --- .../condition_num_plots.py | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 svi/auto_thermal_reformer/condition_num_plots.py diff --git a/svi/auto_thermal_reformer/condition_num_plots.py b/svi/auto_thermal_reformer/condition_num_plots.py new file mode 100644 index 0000000..21276d6 --- /dev/null +++ b/svi/auto_thermal_reformer/condition_num_plots.py @@ -0,0 +1,168 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import pyomo.environ as pyo +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP +from pyomo.contrib.incidence_analysis import solve_strongly_connected_components +from svi.auto_thermal_reformer.fullspace_flowsheet import ( + make_optimization_model, + make_simulation_model, +) +from svi.auto_thermal_reformer.implicit_flowsheet import make_implicit +from svi.external import add_external_function_libraries_to_environment +from pyomo.contrib.pynumero.interfaces.external_pyomo_model import ExternalPyomoModel +from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock +from pyomo.contrib.pynumero.algorithms.solvers.implicit_functions import ( + CyIpoptSolverWrapper +) + +from svi.auto_thermal_reformer.alamo_flowsheet import ( + create_instance as create_alamo_instance, + initialize_alamo_atr_flowsheet, + DEFAULT_SURROGATE_FNAME as DEFAULT_ALAMO_SURROGATE_FNAME +) + +from svi.auto_thermal_reformer.nn_flowsheet import ( + create_instance as create_nn_instance, + initialize_nn_atr_flowsheet, + DEFAULT_SURROGATE_FNAME as DEFAULT_NN_SURROGATE_FNAME, +) +import svi.auto_thermal_reformer.config as config +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + +# This script will plot the condition number of the Jacobian vs Ipopt iteration +# for 4 successful instances of the full space and implicit formulations. + +# SUBSETS represent a tuple containing (X,P,iters), where iters = number of iterations +# for the {full-space, implicit function, ALAMO, NN} formulations to successfully converge. + +data_dir = config.get_data_dir() + +SUBSETS_FULLSPACE = [ + (0.94, 1947379.0, 89), + (0.90, 1727379.0, 123), + (0.92, 1587379.0, 58), + (0.93, 1657379.0, 52) +] + +SUBSETS_IMPLICIT = [ + (0.94, 1947379.0, 45), + (0.90, 1727379.0, 35), + (0.92, 1587379.0, 41), + (0.93, 1657379.0, 37) +] + +SUBSETS_ALAMO = [ + (0.94, 1947379.0, 29), + (0.90, 1727379.0, 26), + (0.92, 1587379.0, 32), + (0.93, 1657379.0, 27) +] + +SUBSETS_NN = [ + (0.94, 1947379.0, 53), + (0.90, 1727379.0, 48), + (0.92, 1587379.0, 47), + (0.93, 1657379.0, 44) +] + +def calculate_condition_number(m): + nlp = PyomoNLP(m) + jac = nlp.evaluate_jacobian() + cond_num = np.linalg.cond(jac.toarray()) + return cond_num + +def full(X=0.94, P=1947379.0, iters=300): + m = make_optimization_model(X, P) + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True) + cond_num = calculate_condition_number(m) + return cond_num + +def implicit(X=0.94, P=1947379.0, iters=300): + m = make_optimization_model(X, P) + add_external_function_libraries_to_environment(m) + m_implicit = make_implicit(m) + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m_implicit, tee=True) + cond_num = calculate_condition_number(m) + return cond_num + +def alamo(X=0.94, P=1947379.0, iters=300): + m = create_alamo_instance(X, P, surrogate_fname=os.path.join(data_dir, DEFAULT_ALAMO_SURROGATE_FNAME)) + initialize_alamo_atr_flowsheet(m) + m.fs.reformer_bypass.inlet.temperature.unfix() + m.fs.reformer_bypass.inlet.flow_mol.unfix() + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True) + cond_num = calculate_condition_number(m) + return cond_num + +def nn(X=0.94, P=1947379.0, iters=300): + m = create_nn_instance(X, P, surrogate_fname=os.path.join(data_dir, DEFAULT_NN_SURROGATE_FNAME)) + initialize_nn_atr_flowsheet(m) + m.fs.reformer_bypass.inlet.temperature.unfix() + m.fs.reformer_bypass.inlet.flow_mol.unfix() + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True) + cond_num = calculate_condition_number(m) + return cond_num + +plt.figure(figsize=(12, 8)) + +for i, (full_subset, implicit_subset, alamo_subset, nn_subset) in enumerate(zip(SUBSETS_FULLSPACE, + SUBSETS_IMPLICIT, + SUBSETS_ALAMO, + SUBSETS_NN), 1): + plt.subplot(2, 2, i) + full_list = [] + implicit_list = [] + alamo_list = [] + nn_list = [] + for iters in range(1, full_subset[2] + 1): + full_list.append(full(X=full_subset[0], P=full_subset[1], iters=iters)) + for iters in range(1, implicit_subset[2] + 1): + implicit_list.append(implicit(X=implicit_subset[0], P=implicit_subset[1], iters=iters)) + for iters in range(1, alamo_subset[2] + 1): + alamo_list.append(alamo(X=alamo_subset[0], P=alamo_subset[1], iters=iters)) + for iters in range(1, nn_subset[2] + 1): + nn_list.append(nn(X=nn_subset[0], P=nn_subset[1], iters=iters)) + plt.plot(full_list, label='Full-space') + plt.plot(implicit_list, label='Implicit function') + plt.plot(alamo_list, label='ALAMO surrogate') + plt.plot(nn_list, label='Neural Network surrogate') + plt.xlabel('Iteration') + plt.ylabel('Condition number of constraint Jacobian') + plt.title(f'Subset {i}: X={full_subset[0]}, P={full_subset[1]} Pa') + plt.legend() + plt.yscale('log') + plt.grid(True) + +plt.tight_layout() +plt.show() + From 802eae8efe92fc19b16bd1a2984b785160d11025 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Sat, 27 Apr 2024 13:51:33 -0600 Subject: [PATCH 09/36] added iterations arg for get_optimization_solver --- svi/auto_thermal_reformer/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/svi/auto_thermal_reformer/config.py b/svi/auto_thermal_reformer/config.py index af64904..8242050 100644 --- a/svi/auto_thermal_reformer/config.py +++ b/svi/auto_thermal_reformer/config.py @@ -43,8 +43,7 @@ 'CH4 Feed', ] - -def get_optimization_solver(options=None): +def get_optimization_solver(options=None, iters = 300): # Use cyipopt for everything for Ipopt version consistency among all # formulations #solver = pyo.SolverFactory("cyipopt") @@ -53,7 +52,7 @@ def get_optimization_solver(options=None): solver = TimedPyomoCyIpoptSolver(intermediate_callback=cb) if options is None: options = {} - solver.config.options["max_iter"] = 300 + solver.config.options["max_iter"] = iters solver.config.options["linear_solver"] = "ma27" solver.config.options["tol"] = 1e-7 solver.config.options["print_user_options"] = "yes" From cf6009c3d3377000ba613224f129fb3423500285 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 28 Apr 2024 16:58:58 -0600 Subject: [PATCH 10/36] improve surrogate error plots --- .../plot_surrogate_error.py | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/svi/auto_thermal_reformer/plot_surrogate_error.py b/svi/auto_thermal_reformer/plot_surrogate_error.py index dd4301d..2712030 100644 --- a/svi/auto_thermal_reformer/plot_surrogate_error.py +++ b/svi/auto_thermal_reformer/plot_surrogate_error.py @@ -22,6 +22,7 @@ import os import pandas as pd import matplotlib.pyplot as plt +from matplotlib.patches import Patch import seaborn as sns import svi.auto_thermal_reformer.config as config @@ -29,9 +30,13 @@ def plot_surrogate_error( df, legend=True, - title=None, show_training_bounds=False, ): + # TODO: Make this (as well as font.family) a global option set + # when we get the argparser? Or maybe just make this configurable from the + # command line with a global default? + # But the same font size might not be appropriate for all plots. + plt.rcParams["font.size"] = 20 fig = plt.figure() conversion_list = list(sorted(set(df["X"]))) @@ -47,12 +52,43 @@ def plot_surrogate_error( for j in range(n_conversion) ] + cbar = legend + # "Maximum relative error" (Between reactor model and surrogate model, over + # all output values, for the intput values at which the optimization stopped) + cbar_kws = dict(label="Maximum relative error") ax = sns.heatmap( error_array, + cbar=cbar, + cbar_kws=cbar_kws, + square=True, + cmap="Reds", ) - - if title is not None: - ax.set_title(title) + # Change "background color" used for missing data (where either full-space + # or surrogate model failed) + ax.set_facecolor("black") + + if legend: + w, h = fig.get_size_inches() + # Getting the legend to appear in the right location is really annoying... + fig.set_size_inches(1.5*w, h) + legend_handles = [Patch(color="black", label="Failed simulation")] + ax.legend( + handles=legend_handles, + #ncol=1, + handlelength=0.75, + bbox_to_anchor=(2.4, 1.0), + #loc="upper right", + #borderaxespad=0, + ) + + xtick_positions = [i+0.5 for i in range(n_pressure)] + ytick_positions = [i+0.5 for i in range(n_conversion)] + xtick_labels = ["%1.2f" % (p / 1e6) if i%2 else "" for i, p in enumerate(pressure_list)] + ytick_labels = ["%1.2f" % x if i%2 else "" for i, x in enumerate(conversion_list)] + ax.set_xticks(xtick_positions, labels=xtick_labels) + ax.set_yticks(ytick_positions, labels=ytick_labels, rotation=0) + ax.set_xlabel("Pressure (MPa)") + ax.set_ylabel("Conversion") plt.gca().invert_yaxis() @@ -65,10 +101,14 @@ def main(args): fig, ax = plot_surrogate_error( df, legend=not args.no_legend, - title=args.title, show_training_bounds=args.show_training_bounds, ) + if args.title is not None: + ax.set_title(args.title) + + fig.tight_layout() + if not args.no_save: if args.plot_fname is None: plot_fname = os.path.basename(args.error_fpath) From 030d4068381f927ec73c6e69ba94e5278743b4ef Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 29 Apr 2024 10:49:54 -0600 Subject: [PATCH 11/36] plot tweaks --- svi/auto_thermal_reformer/makefile | 24 +++++++++++++++++++ .../plot_surrogate_error.py | 6 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/svi/auto_thermal_reformer/makefile b/svi/auto_thermal_reformer/makefile index 8a38ef3..d7e381f 100644 --- a/svi/auto_thermal_reformer/makefile +++ b/svi/auto_thermal_reformer/makefile @@ -62,3 +62,27 @@ plot: --validation-fpath=data/implicit-sweep-validation.csv \ --feastol=1e-5 \ --title="Implicit function" + +plot-error: + python plot_surrogate_error.py \ + data/alamo-sweep-surrogate-error.csv \ + --no-legend \ + --opaque \ + --title="ALAMO" + python plot_surrogate_error.py \ + data/nn-sweep-full-surrogate-error.csv \ + --opaque \ + --title="Neural network" + +plot-error-png: + python plot_surrogate_error.py \ + data/alamo-sweep-surrogate-error.csv \ + --no-legend \ + --opaque \ + --title="ALAMO" \ + --plot-fname=alamo-sweep-surrogate-error.png + python plot_surrogate_error.py \ + data/nn-sweep-full-surrogate-error.csv \ + --opaque \ + --title="Neural network" \ + --plot-fname=nn-sweep-full-surrogate-error.png diff --git a/svi/auto_thermal_reformer/plot_surrogate_error.py b/svi/auto_thermal_reformer/plot_surrogate_error.py index 2712030..f3e5ea8 100644 --- a/svi/auto_thermal_reformer/plot_surrogate_error.py +++ b/svi/auto_thermal_reformer/plot_surrogate_error.py @@ -59,13 +59,14 @@ def plot_surrogate_error( ax = sns.heatmap( error_array, cbar=cbar, + vmin=0.0, + vmax=0.05, cbar_kws=cbar_kws, square=True, cmap="Reds", ) # Change "background color" used for missing data (where either full-space # or surrogate model failed) - ax.set_facecolor("black") if legend: w, h = fig.get_size_inches() @@ -76,7 +77,7 @@ def plot_surrogate_error( handles=legend_handles, #ncol=1, handlelength=0.75, - bbox_to_anchor=(2.4, 1.0), + bbox_to_anchor=(2.5, 1.0), #loc="upper right", #borderaxespad=0, ) @@ -91,6 +92,7 @@ def plot_surrogate_error( ax.set_ylabel("Conversion") plt.gca().invert_yaxis() + ax.set_facecolor("black") return fig, ax From c0ac62ddaf69cbbf124febd7fedf558ec3014b48 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Mon, 29 Apr 2024 13:59:30 -0600 Subject: [PATCH 12/36] code to get a condition number grid --- .../cond_run_fullspace_sweep.py | 133 ++++++++++++++++++ .../plot_condition_num_grid.py | 117 +++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 svi/auto_thermal_reformer/cond_run_fullspace_sweep.py create mode 100644 svi/auto_thermal_reformer/plot_condition_num_grid.py diff --git a/svi/auto_thermal_reformer/cond_run_fullspace_sweep.py b/svi/auto_thermal_reformer/cond_run_fullspace_sweep.py new file mode 100644 index 0000000..ed6b089 --- /dev/null +++ b/svi/auto_thermal_reformer/cond_run_fullspace_sweep.py @@ -0,0 +1,133 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import pyomo.environ as pyo +from pyomo.common.timing import TicTocTimer, HierarchicalTimer +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP +from pyomo.contrib.incidence_analysis import solve_strongly_connected_components +from svi.auto_thermal_reformer.fullspace_flowsheet import ( + make_optimization_model, +) +import svi.auto_thermal_reformer.config as config +import pandas as pd +import numpy as np +from idaes.core.solvers import get_solver + +df = {key: [] for key in config.PARAM_SWEEP_KEYS} +index_to_insert = 3 +new_key = "Condition Number" +df_keys = list(df.keys()) +df_keys.insert(index_to_insert, new_key) +df = {key: [] for key in df_keys} + +INVALID = None + +def calculate_condition_number(m): + nlp = PyomoNLP(m) + jac = nlp.evaluate_jacobian() + cond_num = np.linalg.cond(jac.toarray()) + return cond_num + +def main(X,P): + m = make_optimization_model(X,P) + + # For instance 13 in the param sweep, these options give a quite interesting local + # solution. + solver = config.get_optimization_solver() + intermediate_cb = solver.config.intermediate_callback + htimer = HierarchicalTimer() + timer = TicTocTimer() + timer.tic("starting timer") + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True, timer=htimer) + cond_num = calculate_condition_number(m) + dT = timer.toc("end timer") + f_eval_time = htimer.timers["solve"].timers["function"].total_time + j_eval_time = htimer.timers["solve"].timers["jacobian"].total_time + h_eval_time = htimer.timers["solve"].timers["hessian"].total_time + df[list(df.keys())[0]].append(X) + df[list(df.keys())[1]].append(P) + df[list(df.keys())[2]].append(results.solver.termination_condition) + df[list(df.keys())[3]].append(cond_num) + if pyo.check_optimal_termination(results): + df["Time"].append(dT) + df["Objective"].append(pyo.value(m.fs.product.mole_frac_comp[0,'H2'])) + df["Steam"].append(pyo.value(m.fs.reformer_mix.steam_inlet.flow_mol[0])) + df["Bypass Frac"].append(pyo.value(m.fs.reformer_bypass.split_fraction[0,'bypass_outlet'])) + df["CH4 Feed"].append(pyo.value(m.fs.feed.outlet.flow_mol[0])) + df["Iterations"].append(len(intermediate_cb.iterate_data)) + df["function-time"].append(f_eval_time) + df["jacobian-time"].append(j_eval_time) + df["hessian-time"].append(h_eval_time) + else: + # If the solver didn't converge, we don't care about the solve time, + # the objective, or any of the degree of freedom values. + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): + df[key].append(INVALID) + + +if __name__ == "__main__": + argparser = config.get_sweep_argparser() + argparser.add_argument( + "--fname", + default="fullspace-sweep.csv", + help="Base file name for parameter sweep results" + ) + args = argparser.parse_args() + xp_samples = config.get_parameter_samples(args) + + fpath = os.path.join(args.data_dir, args.fname) + + for i, (X, P) in enumerate(xp_samples): + print(f"Running sample {i} with X={X}, P={P}") + try: + main(X,P) + except AssertionError: + df[list(df.keys())[0]].append(X) + df[list(df.keys())[1]].append(P) + df[list(df.keys())[2]].append("AssertionError") + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): + df[key].append(INVALID) + except OverflowError: + df[list(df.keys())[0]].append(X) + df[list(df.keys())[1]].append(P) + df[list(df.keys())[2]].append("OverflowError") + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): + df[key].append(INVALID) + except RuntimeError: + df[list(df.keys())[0]].append(X) + df[list(df.keys())[1]].append(P) + df[list(df.keys())[2]].append("RuntimeError") + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): + df[key].append(INVALID) + + df = pd.DataFrame(df) + print(df) + if args.no_save: + print(f"--no-save set. Not saving results") + else: + print(f"Writing sweep results to {fpath}") + df.to_csv(fpath) diff --git a/svi/auto_thermal_reformer/plot_condition_num_grid.py b/svi/auto_thermal_reformer/plot_condition_num_grid.py new file mode 100644 index 0000000..412c9b9 --- /dev/null +++ b/svi/auto_thermal_reformer/plot_condition_num_grid.py @@ -0,0 +1,117 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# __________________________________________________________________________ + +import os +import pandas as pd +import matplotlib.pyplot as plt +from matplotlib.patches import Patch +import seaborn as sns +import svi.auto_thermal_reformer.config as config +from matplotlib.colors import LogNorm + +def plot_cond_num( + df, + legend=True, + show_training_bounds=False, +): + # TODO: Make this (as well as font.family) a global option set + # when we get the argparser? Or maybe just make this configurable from the + # command line with a global default? + # But the same font size might not be appropriate for all plots. + plt.rcParams["font.size"] = 20 + fig = plt.figure() + + conversion_list = list(sorted(set(df["X"]))) + pressure_list = list(sorted(set(df["P"]))) + n_conversion = len(conversion_list) + n_pressure = len(pressure_list) + cond_num_lookup = { + (df["X"][i], df["P"][i]): df["Condition Number"][i] + for i in range(len(df)) + } + cond_num_array = [ + [cond_num_lookup[conversion_list[j], pressure_list[i]] for i in range(n_pressure)] + for j in range(n_conversion) + ] + + cbar = legend + cbar_kws = dict(label="Condition Number") + ax = sns.heatmap( + cond_num_array, + cbar=cbar, + norm=LogNorm(vmin=1e19, vmax=1e27), + cbar_kws=cbar_kws, + square=True, + cmap="Reds", + ) + + xtick_positions = [i+0.5 for i in range(n_pressure)] + ytick_positions = [i+0.5 for i in range(n_conversion)] + xtick_labels = ["%1.2f" % (p / 1e6) if i%2 else "" for i, p in enumerate(pressure_list)] + ytick_labels = ["%1.2f" % x if i%2 else "" for i, x in enumerate(conversion_list)] + ax.set_xticks(xtick_positions, labels=xtick_labels) + ax.set_yticks(ytick_positions, labels=ytick_labels, rotation=0) + ax.set_xlabel("Pressure (MPa)") + ax.set_ylabel("Conversion") + + plt.gca().invert_yaxis() + ax.set_facecolor("black") + + return fig, ax + + +def main(args): + df = pd.read_csv(args.cond_num_fpath) + + fig, ax = plot_cond_num( + df, + legend=not args.no_legend, + show_training_bounds=args.show_training_bounds, + ) + + if args.title is not None: + ax.set_title(args.title) + + fig.tight_layout() + + if not args.no_save: + if args.plot_fname is None: + plot_fname = os.path.basename(args.cond_num_fpath) + data_ext = "." + plot_fname.split(".")[-1] + ext_len = len(data_ext) + plot_fname = plot_fname[:-ext_len] + ".pdf" + else: + plot_fname = args.plot_fname + + plot_fpath = os.path.join(args.results_dir, plot_fname) + fig.savefig(plot_fpath, transparent=not args.opaque) + + if args.show: + plt.show() + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + argparser.add_argument( + "cond_num_fpath", help="Path to CSV file containing condition numbers to plot" + ) + args = argparser.parse_args() + main(args) From 9d7372ac5e09899b7cb6226a9a6fb2e282ffefd4 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Wed, 1 May 2024 11:03:32 -0600 Subject: [PATCH 13/36] script to see what block is having the largest increase in condition number --- .../block_condition_numbers.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 svi/auto_thermal_reformer/block_condition_numbers.py diff --git a/svi/auto_thermal_reformer/block_condition_numbers.py b/svi/auto_thermal_reformer/block_condition_numbers.py new file mode 100644 index 0000000..c5d1231 --- /dev/null +++ b/svi/auto_thermal_reformer/block_condition_numbers.py @@ -0,0 +1,84 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import pyomo.environ as pyo +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP +from svi.auto_thermal_reformer.fullspace_flowsheet import ( + make_optimization_model, + make_simulation_model, +) + +from pyomo.contrib.incidence_analysis import IncidenceGraphInterface +import svi.auto_thermal_reformer.config as config +import numpy as np + +# SUBSETS_FULLSPACE represents tuples (X,P,iters), where iters = number of iterations +# before the solve experiences a jump in the condition number. +# E.g., Instance X = 0.94, P = 1.94 MPa experiences a jump in iteration 12 + 1 = 13. + +SUBSETS_FULLSPACE = [ + (0.94, 1947379.0, 12), + #(0.90, 1727379.0, 12), + #(0.92, 1587379.0, 12), + #(0.93, 1657379.0, 12) +] + +def calculate_condition_number(m): + result = {"block": [], "condition number": []} + nlp = PyomoNLP(m) + igraph = IncidenceGraphInterface(m, include_inequality=False) + vblocks, cblocks = igraph.block_triangularize() + for i, (vblock, cblock) in enumerate(zip(vblocks, cblocks)): + submatrix = nlp.extract_submatrix_jacobian(vblock, cblock) + cond = np.linalg.cond(submatrix.toarray()) + if cond > 1e6: + result["block"].append(i) + result["condition number"].append(cond) + return result + +def full(X=0.94, P=1947379.0, iters=300): + m = make_optimization_model(X, P) + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True) + m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) + m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) + m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) + result = calculate_condition_number(m) + return result + +def identify_blocks_high_cn(dict1, dict2): + increased_blocks = [] + for block1, cond1, cond2 in zip(dict1['block'], dict1['condition number'], dict2['condition number']): + if cond2 > cond1: + increased_blocks.append(block1) + return increased_blocks + +def main(): + for i, (full_subset) in enumerate(zip(SUBSETS_FULLSPACE), 1): + before = full(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]) + after = full(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]+1) + block_to_analyze = identify_blocks_high_cn(before, after) + print(f"Block(s) causing the instance to enter a region of high condition number: {block_to_analyze}.") + +if __name__ == "__main__": + main() From efee74bd7a3610cf662120f40cc3dc432aff917c Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Wed, 1 May 2024 15:38:40 -0600 Subject: [PATCH 14/36] script to identify what variables are causing ill-conditioning --- svi/auto_thermal_reformer/identify_vars.py | 117 +++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 svi/auto_thermal_reformer/identify_vars.py diff --git a/svi/auto_thermal_reformer/identify_vars.py b/svi/auto_thermal_reformer/identify_vars.py new file mode 100644 index 0000000..5f7b21d --- /dev/null +++ b/svi/auto_thermal_reformer/identify_vars.py @@ -0,0 +1,117 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import pyomo.environ as pyo +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP +from svi.auto_thermal_reformer.fullspace_flowsheet import ( + make_optimization_model, + make_simulation_model, +) +import pandas as pd +from svi.auto_thermal_reformer.implicit_flowsheet import make_implicit +from svi.external import add_external_function_libraries_to_environment +from pyomo.contrib.pynumero.interfaces.external_pyomo_model import ExternalPyomoModel +from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock +from pyomo.contrib.pynumero.algorithms.solvers.implicit_functions import ( + CyIpoptSolverWrapper +) + +from pyomo.core.expr.visitor import identify_variables +from pyomo.util.subsystems import create_subsystem_block +from pyomo.contrib.incidence_analysis import IncidenceGraphInterface +import svi.auto_thermal_reformer.config as config +import numpy as np + +# SUBSETS_FULLSPACE represents tuples (X,P,iters), where iters = number of iterations +# before the solve experiences a jump in the condition number. +# E.g., Instance X = 0.94, P = 1.94 MPa experiences a jump in iteration 12 + 1 = 13. + +SUBSETS_FULLSPACE = [ + (0.94, 1947379.0, 12), + #(0.90, 1727379.0, 12), + #(0.92, 1587379.0, 12), + #(0.93, 1657379.0, 12) +] + +def get_vars_related_to_block(m, block=524): + input_and_block_vars = {"Variable":[], "Value":[]} + nlp = PyomoNLP(m) + igraph = IncidenceGraphInterface(m, include_inequality=False) + vblocks, cblocks = igraph.block_triangularize() + vblock = vblocks[block] + cblock = cblocks[block] + for con in cblock: + for var in identify_variables(con.expr): + if 'params' not in var.name: + input_and_block_vars["Variable"].append(var.name) + input_and_block_vars["Value"].append(var.value) + return input_and_block_vars + +def full(X=0.94, P=1947379.0, iters=300): + m = make_optimization_model(X, P) + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True) + m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) + m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) + m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) + result = get_vars_related_to_block(m) + return result + +def implicit(X=0.94, P=1947379.0, iters=300): + m = make_optimization_model(X, P) + add_external_function_libraries_to_environment(m) + m_implicit = make_implicit(m) + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m_implicit, tee=True) + m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) + m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) + m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) + cond_num = get_vars_related_to_block(m) + return cond_num + +def variable_with_jump(dict1, dict2): + variables_with_jump = {"Variable":[], "Value_initial":[], "Value_final":[]} + for var, value1, value2 in zip(dict1["Variable"], dict1["Value"], dict2["Value"]): + if value2 != value1: + percentage_difference = abs((value2 - value1) / value1) * 100 + if percentage_difference > 10: + variables_with_jump["Variable"].append(var) + variables_with_jump["Value_initial"].append(value1) + variables_with_jump["Value_final"].append(value2) + return variables_with_jump + +def dict_to_csv(data, filename): + df = pd.DataFrame.from_dict(data) + df.to_csv(filename, index=False) + +def main(): + for i, (full_subset) in enumerate(zip(SUBSETS_FULLSPACE), 1): + before = implicit(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]) + after = implicit(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]+1) + variables_with_jump = variable_with_jump(before, after) + dict_to_csv(variables_with_jump, 'vars_jump_more_than_10pt.csv') + + +if __name__ == "__main__": + main() From 06c9072e12e18d3a4cdfad556cefeefd02e81ed3 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Fri, 3 May 2024 11:46:03 -0600 Subject: [PATCH 15/36] delete this file with a lot of repeated code --- .../cond_run_fullspace_sweep.py | 133 ------------------ 1 file changed, 133 deletions(-) delete mode 100644 svi/auto_thermal_reformer/cond_run_fullspace_sweep.py diff --git a/svi/auto_thermal_reformer/cond_run_fullspace_sweep.py b/svi/auto_thermal_reformer/cond_run_fullspace_sweep.py deleted file mode 100644 index ed6b089..0000000 --- a/svi/auto_thermal_reformer/cond_run_fullspace_sweep.py +++ /dev/null @@ -1,133 +0,0 @@ -# ___________________________________________________________________________ -# -# Surrogate vs. Implicit: Experiments comparing nonlinear optimization -# formulations -# -# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. -# -# This program was produced under U.S. Government contract 89233218CNA000001 -# for Los Alamos National Laboratory (LANL), which is operated by Triad -# National Security, LLC for the U.S. Department of Energy/National Nuclear -# Security Administration. All rights in the program are reserved by Triad -# National Security, LLC, and the U.S. Department of Energy/National Nuclear -# Security Administration. The Government is granted for itself and others -# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license -# in this material to reproduce, prepare derivative works, distribute copies -# to the public, perform publicly and display publicly, and to permit others -# to do so. -# -# This software is distributed under the 3-clause BSD license. -# ___________________________________________________________________________ - -import os -import pyomo.environ as pyo -from pyomo.common.timing import TicTocTimer, HierarchicalTimer -from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP -from pyomo.contrib.incidence_analysis import solve_strongly_connected_components -from svi.auto_thermal_reformer.fullspace_flowsheet import ( - make_optimization_model, -) -import svi.auto_thermal_reformer.config as config -import pandas as pd -import numpy as np -from idaes.core.solvers import get_solver - -df = {key: [] for key in config.PARAM_SWEEP_KEYS} -index_to_insert = 3 -new_key = "Condition Number" -df_keys = list(df.keys()) -df_keys.insert(index_to_insert, new_key) -df = {key: [] for key in df_keys} - -INVALID = None - -def calculate_condition_number(m): - nlp = PyomoNLP(m) - jac = nlp.evaluate_jacobian() - cond_num = np.linalg.cond(jac.toarray()) - return cond_num - -def main(X,P): - m = make_optimization_model(X,P) - - # For instance 13 in the param sweep, these options give a quite interesting local - # solution. - solver = config.get_optimization_solver() - intermediate_cb = solver.config.intermediate_callback - htimer = HierarchicalTimer() - timer = TicTocTimer() - timer.tic("starting timer") - print(f"Solving sample with X={X}, P={P}") - results = solver.solve(m, tee=True, timer=htimer) - cond_num = calculate_condition_number(m) - dT = timer.toc("end timer") - f_eval_time = htimer.timers["solve"].timers["function"].total_time - j_eval_time = htimer.timers["solve"].timers["jacobian"].total_time - h_eval_time = htimer.timers["solve"].timers["hessian"].total_time - df[list(df.keys())[0]].append(X) - df[list(df.keys())[1]].append(P) - df[list(df.keys())[2]].append(results.solver.termination_condition) - df[list(df.keys())[3]].append(cond_num) - if pyo.check_optimal_termination(results): - df["Time"].append(dT) - df["Objective"].append(pyo.value(m.fs.product.mole_frac_comp[0,'H2'])) - df["Steam"].append(pyo.value(m.fs.reformer_mix.steam_inlet.flow_mol[0])) - df["Bypass Frac"].append(pyo.value(m.fs.reformer_bypass.split_fraction[0,'bypass_outlet'])) - df["CH4 Feed"].append(pyo.value(m.fs.feed.outlet.flow_mol[0])) - df["Iterations"].append(len(intermediate_cb.iterate_data)) - df["function-time"].append(f_eval_time) - df["jacobian-time"].append(j_eval_time) - df["hessian-time"].append(h_eval_time) - else: - # If the solver didn't converge, we don't care about the solve time, - # the objective, or any of the degree of freedom values. - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): - df[key].append(INVALID) - - -if __name__ == "__main__": - argparser = config.get_sweep_argparser() - argparser.add_argument( - "--fname", - default="fullspace-sweep.csv", - help="Base file name for parameter sweep results" - ) - args = argparser.parse_args() - xp_samples = config.get_parameter_samples(args) - - fpath = os.path.join(args.data_dir, args.fname) - - for i, (X, P) in enumerate(xp_samples): - print(f"Running sample {i} with X={X}, P={P}") - try: - main(X,P) - except AssertionError: - df[list(df.keys())[0]].append(X) - df[list(df.keys())[1]].append(P) - df[list(df.keys())[2]].append("AssertionError") - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): - df[key].append(INVALID) - except OverflowError: - df[list(df.keys())[0]].append(X) - df[list(df.keys())[1]].append(P) - df[list(df.keys())[2]].append("OverflowError") - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): - df[key].append(INVALID) - except RuntimeError: - df[list(df.keys())[0]].append(X) - df[list(df.keys())[1]].append(P) - df[list(df.keys())[2]].append("RuntimeError") - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): - df[key].append(INVALID) - - df = pd.DataFrame(df) - print(df) - if args.no_save: - print(f"--no-save set. Not saving results") - else: - print(f"Writing sweep results to {fpath}") - df.to_csv(fpath) From 14f9bd8d88cffd33a3db129c841cda03ed1bc766 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Fri, 3 May 2024 11:46:23 -0600 Subject: [PATCH 16/36] add condition number calculation option --- .../run_fullspace_sweep.py | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/svi/auto_thermal_reformer/run_fullspace_sweep.py b/svi/auto_thermal_reformer/run_fullspace_sweep.py index 541a00b..e5241ef 100644 --- a/svi/auto_thermal_reformer/run_fullspace_sweep.py +++ b/svi/auto_thermal_reformer/run_fullspace_sweep.py @@ -27,18 +27,21 @@ make_optimization_model, make_simulation_model, ) +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP import svi.auto_thermal_reformer.config as config import pandas as pd import numpy as np - -df = {key: [] for key in config.PARAM_SWEEP_KEYS} - - INVALID = None +def calculate_condition_number(m): + nlp = PyomoNLP(m) + jac = nlp.evaluate_jacobian() + cond_num = np.linalg.cond(jac.toarray()) + return cond_num -def main(X,P): +def main(X,P, calc_condition_number = False): + m = make_optimization_model(X,P) # For instance 13 in the param sweep, these options give a quite interesting local @@ -50,6 +53,10 @@ def main(X,P): timer.tic("starting timer") print(f"Solving sample with X={X}, P={P}") results = solver.solve(m, tee=True, timer=htimer) + if calc_condition_number: + cond_num = calculate_condition_number(m) + else: + cond_num = None dT = timer.toc("end timer") f_eval_time = htimer.timers["solve"].timers["function"].total_time j_eval_time = htimer.timers["solve"].timers["jacobian"].total_time @@ -57,6 +64,8 @@ def main(X,P): df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append(results.solver.termination_condition) + if new_key: + df[list(df.keys())[3]].append(cond_num) if pyo.check_optimal_termination(results): df["Time"].append(dT) df["Objective"].append(pyo.value(m.fs.product.mole_frac_comp[0,'H2'])) @@ -70,19 +79,37 @@ def main(X,P): else: # If the solver didn't converge, we don't care about the solve time, # the objective, or any of the degree of freedom values. - for key in config.PARAM_SWEEP_KEYS: - if key not in ("X", "P", "Termination"): + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): df[key].append(INVALID) if __name__ == "__main__": + argparser = config.get_sweep_argparser() + argparser.add_argument( "--fname", default="fullspace-sweep.csv", help="Base file name for parameter sweep results" ) + + argparser.add_argument( + "--calc_condition_number", + action="store_true", + help="Whether to calculate the condition number or not" + ) + args = argparser.parse_args() + + df = {key: [] for key in config.PARAM_SWEEP_KEYS} + index_to_insert = 3 + new_key = "Condition Number" if args.calc_condition_number else None + df_keys = list(df.keys()) + if new_key: + df_keys.insert(index_to_insert, new_key) + df = {key: [] for key in df_keys} + xp_samples = config.get_parameter_samples(args) fpath = os.path.join(args.data_dir, args.fname) @@ -90,27 +117,27 @@ def main(X,P): for i, (X, P) in enumerate(xp_samples): print(f"Running sample {i} with X={X}, P={P}") try: - main(X,P) + main(X,P,args.calc_condition_number) except AssertionError: df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append("AssertionError") - for key in config.PARAM_SWEEP_KEYS: - if key not in ("X", "P", "Termination"): + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): df[key].append(INVALID) except OverflowError: df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append("OverflowError") - for key in config.PARAM_SWEEP_KEYS: - if key not in ("X", "P", "Termination"): + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): df[key].append(INVALID) except RuntimeError: df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append("RuntimeError") - for key in config.PARAM_SWEEP_KEYS: - if key not in ("X", "P", "Termination"): + for key in df_keys: + if key not in ("X", "P", "Termination", "Condition Number"): df[key].append(INVALID) df = pd.DataFrame(df) From e5c1b5e399ce5f5014b2e5a9b04abe6108810919 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 5 May 2024 10:46:11 -0600 Subject: [PATCH 17/36] remove whitespace and change condition number arg to --calc-condition-number for consistency --- svi/auto_thermal_reformer/run_fullspace_sweep.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/svi/auto_thermal_reformer/run_fullspace_sweep.py b/svi/auto_thermal_reformer/run_fullspace_sweep.py index e5241ef..26026e9 100644 --- a/svi/auto_thermal_reformer/run_fullspace_sweep.py +++ b/svi/auto_thermal_reformer/run_fullspace_sweep.py @@ -87,21 +87,21 @@ def main(X,P, calc_condition_number = False): if __name__ == "__main__": argparser = config.get_sweep_argparser() - + argparser.add_argument( "--fname", default="fullspace-sweep.csv", help="Base file name for parameter sweep results" ) - + argparser.add_argument( - "--calc_condition_number", + "--calc-condition-number", action="store_true", help="Whether to calculate the condition number or not" ) - + args = argparser.parse_args() - + df = {key: [] for key in config.PARAM_SWEEP_KEYS} index_to_insert = 3 new_key = "Condition Number" if args.calc_condition_number else None @@ -109,7 +109,7 @@ def main(X,P, calc_condition_number = False): if new_key: df_keys.insert(index_to_insert, new_key) df = {key: [] for key in df_keys} - + xp_samples = config.get_parameter_samples(args) fpath = os.path.join(args.data_dir, args.fname) From 3c8af83d1ad2fea6913e4fcb35a514d74a969b29 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 5 May 2024 10:47:30 -0600 Subject: [PATCH 18/36] append -condition suffix to filename --- svi/auto_thermal_reformer/plot_condition_num_grid.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/svi/auto_thermal_reformer/plot_condition_num_grid.py b/svi/auto_thermal_reformer/plot_condition_num_grid.py index 412c9b9..335b2cd 100644 --- a/svi/auto_thermal_reformer/plot_condition_num_grid.py +++ b/svi/auto_thermal_reformer/plot_condition_num_grid.py @@ -97,11 +97,12 @@ def main(args): plot_fname = os.path.basename(args.cond_num_fpath) data_ext = "." + plot_fname.split(".")[-1] ext_len = len(data_ext) - plot_fname = plot_fname[:-ext_len] + ".pdf" + plot_fname = plot_fname[:-ext_len] + "-condition.pdf" else: plot_fname = args.plot_fname plot_fpath = os.path.join(args.results_dir, plot_fname) + print(f"Saving figure to {plot_fpath}") fig.savefig(plot_fpath, transparent=not args.opaque) if args.show: From a58a190b8dae1803c8010d776cf9faca4c5a31f1 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Sun, 5 May 2024 13:35:12 -0600 Subject: [PATCH 19/36] DF_KEYS, removed Condition Number calc when errors found --- .../run_fullspace_sweep.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/svi/auto_thermal_reformer/run_fullspace_sweep.py b/svi/auto_thermal_reformer/run_fullspace_sweep.py index 26026e9..2ba9f96 100644 --- a/svi/auto_thermal_reformer/run_fullspace_sweep.py +++ b/svi/auto_thermal_reformer/run_fullspace_sweep.py @@ -79,7 +79,7 @@ def main(X,P, calc_condition_number = False): else: # If the solver didn't converge, we don't care about the solve time, # the objective, or any of the degree of freedom values. - for key in df_keys: + for key in DF_KEYS: if key not in ("X", "P", "Termination", "Condition Number"): df[key].append(INVALID) @@ -105,10 +105,10 @@ def main(X,P, calc_condition_number = False): df = {key: [] for key in config.PARAM_SWEEP_KEYS} index_to_insert = 3 new_key = "Condition Number" if args.calc_condition_number else None - df_keys = list(df.keys()) + DF_KEYS = list(df.keys()) if new_key: - df_keys.insert(index_to_insert, new_key) - df = {key: [] for key in df_keys} + DF_KEYS.insert(index_to_insert, new_key) + df = {key: [] for key in DF_KEYS} xp_samples = config.get_parameter_samples(args) @@ -122,22 +122,22 @@ def main(X,P, calc_condition_number = False): df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append("AssertionError") - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): + for key in DF_KEYS: + if key not in ("X", "P", "Termination"): df[key].append(INVALID) except OverflowError: df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append("OverflowError") - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): + for key in DF_KEYS: + if key not in ("X", "P", "Termination"): df[key].append(INVALID) except RuntimeError: df[list(df.keys())[0]].append(X) df[list(df.keys())[1]].append(P) df[list(df.keys())[2]].append("RuntimeError") - for key in df_keys: - if key not in ("X", "P", "Termination", "Condition Number"): + for key in DF_KEYS: + if key not in ("X", "P", "Termination"): df[key].append(INVALID) df = pd.DataFrame(df) From 6d34331f52281dc9c28a640ed0646286e73f67d1 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Sun, 5 May 2024 16:19:10 -0600 Subject: [PATCH 20/36] plot condition number vs iteration for successful and unsuccessful instances --- .../condition_num_plots.py | 152 ++++++++++-------- 1 file changed, 89 insertions(+), 63 deletions(-) diff --git a/svi/auto_thermal_reformer/condition_num_plots.py b/svi/auto_thermal_reformer/condition_num_plots.py index 21276d6..59bd3de 100644 --- a/svi/auto_thermal_reformer/condition_num_plots.py +++ b/svi/auto_thermal_reformer/condition_num_plots.py @@ -21,7 +21,7 @@ import os import pyomo.environ as pyo -from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP +from svi.cyipopt import ConditioningCallback from pyomo.contrib.incidence_analysis import solve_strongly_connected_components from svi.auto_thermal_reformer.fullspace_flowsheet import ( make_optimization_model, @@ -50,119 +50,145 @@ import pandas as pd import numpy as np import matplotlib.pyplot as plt - -# This script will plot the condition number of the Jacobian vs Ipopt iteration -# for 4 successful instances of the full space and implicit formulations. +import argparse # SUBSETS represent a tuple containing (X,P,iters), where iters = number of iterations # for the {full-space, implicit function, ALAMO, NN} formulations to successfully converge. +# For the unsuccessful instances, implicit function displays eval error after iter 12. The +# full space and NN formulations do not find a solution after 300 iters. data_dir = config.get_data_dir() -SUBSETS_FULLSPACE = [ +SUCCESS_SUBSETS_FULLSPACE = [ (0.94, 1947379.0, 89), (0.90, 1727379.0, 123), (0.92, 1587379.0, 58), (0.93, 1657379.0, 52) ] -SUBSETS_IMPLICIT = [ +SUCCESS_SUBSETS_IMPLICIT = [ (0.94, 1947379.0, 45), (0.90, 1727379.0, 35), (0.92, 1587379.0, 41), (0.93, 1657379.0, 37) ] -SUBSETS_ALAMO = [ +SUCCESS_SUBSETS_ALAMO = [ (0.94, 1947379.0, 29), (0.90, 1727379.0, 26), (0.92, 1587379.0, 32), (0.93, 1657379.0, 27) ] -SUBSETS_NN = [ +SUCCESS_SUBSETS_NN = [ (0.94, 1947379.0, 53), (0.90, 1727379.0, 48), (0.92, 1587379.0, 47), (0.93, 1657379.0, 44) ] -def calculate_condition_number(m): - nlp = PyomoNLP(m) - jac = nlp.evaluate_jacobian() - cond_num = np.linalg.cond(jac.toarray()) - return cond_num +NOSUCCESS_SUBSETS_FULLSPACE = [ + (0.96, 1447379.0, 150), + (0.97, 1447379.0, 150), + (0.96, 1587379.0, 150), + (0.97, 1587379.0, 150) +] + +NOSUCCESS_SUBSETS_IMPLICIT = [ + (0.96, 1447379.0, 12), + (0.97, 1447379.0, 12), + (0.96, 1587379.0, 12), + (0.97, 1587379.0, 12) +] + +NOSUCCESS_SUBSETS_NN = [ + (0.96, 1447379.0, 150), + (0.97, 1447379.0, 150), + (0.96, 1587379.0, 150), + (0.97, 1587379.0, 150) +] + +def solver(m, iters = 300): + solver = config.get_optimization_solver(iters = iters) + callback = ConditioningCallback() + solver.config.intermediate_callback = callback + solver.solve(m, tee=True) + condition_numbers_list = callback.condition_numbers + return condition_numbers_list[1:] def full(X=0.94, P=1947379.0, iters=300): m = make_optimization_model(X, P) - solver = config.get_optimization_solver(iters=iters) print(f"Solving sample with X={X}, P={P}") - results = solver.solve(m, tee=True) - cond_num = calculate_condition_number(m) - return cond_num + condition_numbers_list = solver(m, iters = iters) + return condition_numbers_list def implicit(X=0.94, P=1947379.0, iters=300): m = make_optimization_model(X, P) add_external_function_libraries_to_environment(m) m_implicit = make_implicit(m) - solver = config.get_optimization_solver(iters=iters) print(f"Solving sample with X={X}, P={P}") - results = solver.solve(m_implicit, tee=True) - cond_num = calculate_condition_number(m) - return cond_num + condition_numbers_list = solver(m_implicit, iters = iters) + return condition_numbers_list def alamo(X=0.94, P=1947379.0, iters=300): m = create_alamo_instance(X, P, surrogate_fname=os.path.join(data_dir, DEFAULT_ALAMO_SURROGATE_FNAME)) initialize_alamo_atr_flowsheet(m) m.fs.reformer_bypass.inlet.temperature.unfix() m.fs.reformer_bypass.inlet.flow_mol.unfix() - solver = config.get_optimization_solver(iters=iters) print(f"Solving sample with X={X}, P={P}") - results = solver.solve(m, tee=True) - cond_num = calculate_condition_number(m) - return cond_num + condition_numbers_list = solver(m, iters = iters) + return condition_numbers_list def nn(X=0.94, P=1947379.0, iters=300): m = create_nn_instance(X, P, surrogate_fname=os.path.join(data_dir, DEFAULT_NN_SURROGATE_FNAME)) initialize_nn_atr_flowsheet(m) m.fs.reformer_bypass.inlet.temperature.unfix() m.fs.reformer_bypass.inlet.flow_mol.unfix() - solver = config.get_optimization_solver(iters=iters) print(f"Solving sample with X={X}, P={P}") - results = solver.solve(m, tee=True) - cond_num = calculate_condition_number(m) - return cond_num - -plt.figure(figsize=(12, 8)) - -for i, (full_subset, implicit_subset, alamo_subset, nn_subset) in enumerate(zip(SUBSETS_FULLSPACE, - SUBSETS_IMPLICIT, - SUBSETS_ALAMO, - SUBSETS_NN), 1): - plt.subplot(2, 2, i) - full_list = [] - implicit_list = [] - alamo_list = [] - nn_list = [] - for iters in range(1, full_subset[2] + 1): - full_list.append(full(X=full_subset[0], P=full_subset[1], iters=iters)) - for iters in range(1, implicit_subset[2] + 1): - implicit_list.append(implicit(X=implicit_subset[0], P=implicit_subset[1], iters=iters)) - for iters in range(1, alamo_subset[2] + 1): - alamo_list.append(alamo(X=alamo_subset[0], P=alamo_subset[1], iters=iters)) - for iters in range(1, nn_subset[2] + 1): - nn_list.append(nn(X=nn_subset[0], P=nn_subset[1], iters=iters)) - plt.plot(full_list, label='Full-space') - plt.plot(implicit_list, label='Implicit function') - plt.plot(alamo_list, label='ALAMO surrogate') - plt.plot(nn_list, label='Neural Network surrogate') - plt.xlabel('Iteration') - plt.ylabel('Condition number of constraint Jacobian') - plt.title(f'Subset {i}: X={full_subset[0]}, P={full_subset[1]} Pa') - plt.legend() - plt.yscale('log') - plt.grid(True) - -plt.tight_layout() -plt.show() - + condition_numbers_list = solver(m, iters = iters) + return condition_numbers_list + +def plot_subsets(SUBSETS_FULLSPACE, SUBSETS_IMPLICIT, SUBSETS_NN, SUBSETS_ALAMO=None, unsuccessful=False): + plt.figure(figsize=(12, 8)) + + for i, (full_subset, implicit_subset, nn_subset) in enumerate(zip(SUBSETS_FULLSPACE, SUBSETS_IMPLICIT, SUBSETS_NN), 1): + plt.subplot(2, 2, i) + full_list = full(X=full_subset[0], P=full_subset[1], iters=full_subset[2]) + implicit_list = implicit(X=implicit_subset[0], P=implicit_subset[1], iters=implicit_subset[2]) + nn_list = nn(X=nn_subset[0], P=nn_subset[1], iters=nn_subset[2]) + plt.plot(full_list, label='Full-space') + plt.plot(implicit_list, label='Implicit function') + plt.plot(nn_list, label='Neural Network surrogate') + if SUBSETS_ALAMO: + alamo_subset = SUBSETS_ALAMO[i - 1] + alamo_list = alamo(X=alamo_subset[0], P=alamo_subset[1], iters=alamo_subset[2]) + plt.plot(alamo_list, label='ALAMO surrogate') + plt.xlabel('Iteration') + plt.ylabel('Condition number of constraint Jacobian') + plt.title(f'Subset {i}: X={full_subset[0]}, P={full_subset[1]} Pa') + if unsuccessful: + plt.legend(loc="lower right") + else: + plt.legend(loc="upper right") + plt.yscale('log') + plt.grid(True) + plt.tight_layout() + + plt.show() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Plot subsets") + parser.add_argument("--unsuccessful", action="store_true", help="Whether to use an unsuccessful instance or not") + args = parser.parse_args() + + if args.unsuccessful: + SUBSETS_FULLSPACE = NOSUCCESS_SUBSETS_FULLSPACE + SUBSETS_IMPLICIT = NOSUCCESS_SUBSETS_IMPLICIT + SUBSETS_NN = NOSUCCESS_SUBSETS_NN + fig = plot_subsets(SUBSETS_FULLSPACE, SUBSETS_IMPLICIT, SUBSETS_NN, unsuccessful=True) + else: + SUBSETS_FULLSPACE = SUCCESS_SUBSETS_FULLSPACE + SUBSETS_IMPLICIT = SUCCESS_SUBSETS_IMPLICIT + SUBSETS_ALAMO = SUCCESS_SUBSETS_ALAMO + SUBSETS_NN = SUCCESS_SUBSETS_NN + fig = plot_subsets(SUBSETS_FULLSPACE, SUBSETS_IMPLICIT, SUBSETS_NN, SUBSETS_ALAMO) From fdcea9b29625815c9208ce5b78466b6cdcbc5a44 Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Sun, 5 May 2024 16:52:44 -0600 Subject: [PATCH 21/36] get block causing jump in condition number --- .../get_block_causing_jump.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 svi/auto_thermal_reformer/get_block_causing_jump.py diff --git a/svi/auto_thermal_reformer/get_block_causing_jump.py b/svi/auto_thermal_reformer/get_block_causing_jump.py new file mode 100644 index 0000000..3be4eaa --- /dev/null +++ b/svi/auto_thermal_reformer/get_block_causing_jump.py @@ -0,0 +1,85 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import pyomo.environ as pyo +from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP +from svi.auto_thermal_reformer.fullspace_flowsheet import ( + make_optimization_model, + make_simulation_model, +) + +from pyomo.contrib.incidence_analysis import IncidenceGraphInterface +import svi.auto_thermal_reformer.config as config +import numpy as np + +# SUBSETS_FULLSPACE represents tuples (X,P,iters), where iters = number of iterations +# before the solve experiences a jump in the condition number. +# E.g., Instance X = 0.94, P = 1.94 MPa experiences a jump in iteration 12 + 1 = 13. +# This code exemplifies how to extract the blocks that are causing this jump for this +# specific instance. + +SUBSETS_FULLSPACE = [ + (0.94, 1947379.0, 12), +] + +def calculate_condition_number(m, condition_number_threshold = 1e6): + result = {"block": [], "condition number": []} + nlp = PyomoNLP(m) + igraph = IncidenceGraphInterface(m, include_inequality=False) + vblocks, cblocks = igraph.block_triangularize() + for i, (vblock, cblock) in enumerate(zip(vblocks, cblocks)): + submatrix = nlp.extract_submatrix_jacobian(vblock, cblock) + cond = np.linalg.cond(submatrix.toarray()) + if cond > condition_number_threshold: + result["block"].append(i) + result["condition number"].append(cond) + return result + +def full_space(X=0.94, P=1947379.0, iters=300): + m = make_optimization_model(X, P) + solver = config.get_optimization_solver(iters=iters) + print(f"Solving sample with X={X}, P={P}") + results = solver.solve(m, tee=True) + m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) + m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) + m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) + result = calculate_condition_number(m) + return result + +def id_blocks_high_condition_number(dict1, dict2): + increased_blocks = [] + for block1, cond1, cond2 in zip(dict1['block'], dict1['condition number'], dict2['condition number']): + if cond2 > cond1: + increased_blocks.append(block1) + return increased_blocks + +def main(): + for i, (full_subset) in enumerate(zip(SUBSETS_FULLSPACE), 1): + before_jump = full_space(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]) + after_jump = full_space(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]+1) + block_to_analyze = id_blocks_high_condition_number(before_jump, after_jump) + # Maybe also print equations and variables present in that block? For now, all we need is the number. + # With this number we will proceed to identify and print variable values participating in this block. + print(f"Block(s) causing the instance to enter a region of high condition number: {block_to_analyze}.") + +if __name__ == "__main__": + main() From aef8a8189d49418d7ca6047df02c9f28cd1f77fb Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Sun, 5 May 2024 16:53:17 -0600 Subject: [PATCH 22/36] delete outdated file --- .../block_condition_numbers.py | 84 ------------------- 1 file changed, 84 deletions(-) delete mode 100644 svi/auto_thermal_reformer/block_condition_numbers.py diff --git a/svi/auto_thermal_reformer/block_condition_numbers.py b/svi/auto_thermal_reformer/block_condition_numbers.py deleted file mode 100644 index c5d1231..0000000 --- a/svi/auto_thermal_reformer/block_condition_numbers.py +++ /dev/null @@ -1,84 +0,0 @@ -# ___________________________________________________________________________ -# -# Surrogate vs. Implicit: Experiments comparing nonlinear optimization -# formulations -# -# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. -# -# This program was produced under U.S. Government contract 89233218CNA000001 -# for Los Alamos National Laboratory (LANL), which is operated by Triad -# National Security, LLC for the U.S. Department of Energy/National Nuclear -# Security Administration. All rights in the program are reserved by Triad -# National Security, LLC, and the U.S. Department of Energy/National Nuclear -# Security Administration. The Government is granted for itself and others -# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license -# in this material to reproduce, prepare derivative works, distribute copies -# to the public, perform publicly and display publicly, and to permit others -# to do so. -# -# This software is distributed under the 3-clause BSD license. -# ___________________________________________________________________________ - -import os -import pyomo.environ as pyo -from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP -from svi.auto_thermal_reformer.fullspace_flowsheet import ( - make_optimization_model, - make_simulation_model, -) - -from pyomo.contrib.incidence_analysis import IncidenceGraphInterface -import svi.auto_thermal_reformer.config as config -import numpy as np - -# SUBSETS_FULLSPACE represents tuples (X,P,iters), where iters = number of iterations -# before the solve experiences a jump in the condition number. -# E.g., Instance X = 0.94, P = 1.94 MPa experiences a jump in iteration 12 + 1 = 13. - -SUBSETS_FULLSPACE = [ - (0.94, 1947379.0, 12), - #(0.90, 1727379.0, 12), - #(0.92, 1587379.0, 12), - #(0.93, 1657379.0, 12) -] - -def calculate_condition_number(m): - result = {"block": [], "condition number": []} - nlp = PyomoNLP(m) - igraph = IncidenceGraphInterface(m, include_inequality=False) - vblocks, cblocks = igraph.block_triangularize() - for i, (vblock, cblock) in enumerate(zip(vblocks, cblocks)): - submatrix = nlp.extract_submatrix_jacobian(vblock, cblock) - cond = np.linalg.cond(submatrix.toarray()) - if cond > 1e6: - result["block"].append(i) - result["condition number"].append(cond) - return result - -def full(X=0.94, P=1947379.0, iters=300): - m = make_optimization_model(X, P) - solver = config.get_optimization_solver(iters=iters) - print(f"Solving sample with X={X}, P={P}") - results = solver.solve(m, tee=True) - m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) - m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) - m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) - result = calculate_condition_number(m) - return result - -def identify_blocks_high_cn(dict1, dict2): - increased_blocks = [] - for block1, cond1, cond2 in zip(dict1['block'], dict1['condition number'], dict2['condition number']): - if cond2 > cond1: - increased_blocks.append(block1) - return increased_blocks - -def main(): - for i, (full_subset) in enumerate(zip(SUBSETS_FULLSPACE), 1): - before = full(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]) - after = full(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]+1) - block_to_analyze = identify_blocks_high_cn(before, after) - print(f"Block(s) causing the instance to enter a region of high condition number: {block_to_analyze}.") - -if __name__ == "__main__": - main() From 85383acb241d6a682d5fe84befdc66c2bebc8dba Mon Sep 17 00:00:00 2001 From: Sergio Ivan Bugosen Tannous Date: Tue, 7 May 2024 12:16:11 -0600 Subject: [PATCH 23/36] code can handle lists of blocks, added arguments --- svi/auto_thermal_reformer/identify_vars.py | 82 ++++++++++++++-------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/svi/auto_thermal_reformer/identify_vars.py b/svi/auto_thermal_reformer/identify_vars.py index 5f7b21d..1f8c822 100644 --- a/svi/auto_thermal_reformer/identify_vars.py +++ b/svi/auto_thermal_reformer/identify_vars.py @@ -40,6 +40,7 @@ from pyomo.contrib.incidence_analysis import IncidenceGraphInterface import svi.auto_thermal_reformer.config as config import numpy as np +import argparse # SUBSETS_FULLSPACE represents tuples (X,P,iters), where iters = number of iterations # before the solve experiences a jump in the condition number. @@ -47,26 +48,31 @@ SUBSETS_FULLSPACE = [ (0.94, 1947379.0, 12), - #(0.90, 1727379.0, 12), - #(0.92, 1587379.0, 12), - #(0.93, 1657379.0, 12) ] -def get_vars_related_to_block(m, block=524): - input_and_block_vars = {"Variable":[], "Value":[]} +# We want to see what variable values are causing this jump in condition number. +# For this specific subset, jump in condition number occurs only in block 524. + +def get_vars_related_to_blocks(m, blocks=[524]): + input_and_block_vars = [] nlp = PyomoNLP(m) igraph = IncidenceGraphInterface(m, include_inequality=False) vblocks, cblocks = igraph.block_triangularize() - vblock = vblocks[block] - cblock = cblocks[block] - for con in cblock: - for var in identify_variables(con.expr): - if 'params' not in var.name: - input_and_block_vars["Variable"].append(var.name) - input_and_block_vars["Value"].append(var.value) + for block in blocks: + block_vars = {"Block": block, "Variable": [], "Value": []} + vblock = vblocks[block] + cblock = cblocks[block] + + for con in cblock: + for var in identify_variables(con.expr): + if 'params' not in var.name: + block_vars["Variable"].append(var.name) + block_vars["Value"].append(var.value) + + input_and_block_vars.append(block_vars) return input_and_block_vars -def full(X=0.94, P=1947379.0, iters=300): +def full_space(X=0.94, P=1947379.0, iters=300, blocks=[524]): m = make_optimization_model(X, P) solver = config.get_optimization_solver(iters=iters) print(f"Solving sample with X={X}, P={P}") @@ -74,10 +80,10 @@ def full(X=0.94, P=1947379.0, iters=300): m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) - result = get_vars_related_to_block(m) - return result + dicts = get_vars_related_to_blocks(m, blocks = blocks) + return dicts -def implicit(X=0.94, P=1947379.0, iters=300): +def implicit(X=0.94, P=1947379.0, iters=300, blocks = [524]): m = make_optimization_model(X, P) add_external_function_libraries_to_environment(m) m_implicit = make_implicit(m) @@ -87,15 +93,19 @@ def implicit(X=0.94, P=1947379.0, iters=300): m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].fix(m.fs.reformer_bypass.split_fraction[0, "bypass_outlet"].value) m.fs.reformer_mix.steam_inlet.flow_mol.fix(m.fs.reformer_mix.steam_inlet.flow_mol[0].value) m.fs.feed.outlet.flow_mol.fix(m.fs.feed.outlet.flow_mol[0].value) - cond_num = get_vars_related_to_block(m) - return cond_num + dicts = get_vars_related_to_blocks(m, blocks = blocks) + return dicts + +# The function below is written under the premise that a jump in condition number is due to +# a significant jump in some variables values. The magnitude of this jump for each variable +# is unknown; percentage_diff argument can me modified according to the user's experience. -def variable_with_jump(dict1, dict2): +def variable_with_jump(dict1, dict2, percentage_diff = 10): variables_with_jump = {"Variable":[], "Value_initial":[], "Value_final":[]} for var, value1, value2 in zip(dict1["Variable"], dict1["Value"], dict2["Value"]): if value2 != value1: percentage_difference = abs((value2 - value1) / value1) * 100 - if percentage_difference > 10: + if percentage_difference > percentage_diff: variables_with_jump["Variable"].append(var) variables_with_jump["Value_initial"].append(value1) variables_with_jump["Value_final"].append(value2) @@ -105,13 +115,29 @@ def dict_to_csv(data, filename): df = pd.DataFrame.from_dict(data) df.to_csv(filename, index=False) -def main(): - for i, (full_subset) in enumerate(zip(SUBSETS_FULLSPACE), 1): - before = implicit(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]) - after = implicit(X=full_subset[0][0], P=full_subset[0][1], iters=full_subset[0][2]+1) - variables_with_jump = variable_with_jump(before, after) - dict_to_csv(variables_with_jump, 'vars_jump_more_than_10pt.csv') - +def main(blocks, percentage_diff, use_full_space, full_subsets): + for full_subset in full_subsets: + if use_full_space: + before_jump = full_space(X=full_subset[0], P=full_subset[1], iters=full_subset[2], blocks = blocks) + after_jump = full_space(X=full_subset[0], P=full_subset[1], iters=full_subset[2] + 1, blocks = blocks) + else: + before_jump = implicit(X=full_subset[0], P=full_subset[1], iters=full_subset[2], blocks = blocks) + after_jump = implicit(X=full_subset[0], P=full_subset[1], iters=full_subset[2] + 1, blocks = blocks) + + for before, after in zip(before_jump, after_jump): + block_number = before["Block"] + variables_with_jump = variable_with_jump(before, after, percentage_diff) + filename = f'vars_jump_more_than_{percentage_diff}pt_block_{block_number}.csv' + dict_to_csv(variables_with_jump, filename) if __name__ == "__main__": - main() + parser = argparse.ArgumentParser() + parser.add_argument("--blocks", nargs="+", type=int, default=[524], help="List of blocks") + parser.add_argument("--percentage_diff", type=float, default=10, help="Threshold for percentage difference") + parser.add_argument("--use_full_space", action="store_true", help="Use full_space function") + args = parser.parse_args() + + full_subsets = SUBSETS_FULLSPACE + main(args.blocks, args.percentage_diff, args.use_full_space, full_subsets) + + From 63c0f63b0cbd3ae7edd4752debaaf0cd2944caee Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 12 Jun 2024 14:45:54 -0600 Subject: [PATCH 24/36] script to generate iterate data from a solve --- .../generate_iterate_data.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 svi/auto_thermal_reformer/generate_iterate_data.py diff --git a/svi/auto_thermal_reformer/generate_iterate_data.py b/svi/auto_thermal_reformer/generate_iterate_data.py new file mode 100644 index 0000000..61f594c --- /dev/null +++ b/svi/auto_thermal_reformer/generate_iterate_data.py @@ -0,0 +1,91 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import svi.auto_thermal_reformer.config as config +from svi.cyipopt import FullStateCallback +import pandas as pd + + +def main(args): + if args.sample is None: + conversion = 0.94 + pressure = 1550000.0 + else: + xp_samples = config.get_parameter_samples(args) + conversion, pressure = xp_samples[args.sample] + m = config.CONSTRUCTOR_LOOKUP[args.model](conversion, pressure) + callback = FullStateCallback() + solver = config.get_optimization_solver(callback=callback) + + results = solver.solve(m, tee=True) + + if args.fname is None: + fname = f"{args.model}-iterates" + if args.sample is not None: + fname += f"-{args.sample}" + fname += ".csv" + else: + fname = args.fname + fpath = os.path.join(args.data_dir, fname) + + iterate_data = dict(callback.iterate_data) + # TODO: Should the keys here be updated to indicate that they are primal + # values and residuals? Probably, but I'll deal with that later. + iterate_data.update(callback.primal_values) + iterate_data.update(callback.primal_residuals) + # Make sure nothing pathological happened, like we had a variable named "inf_du" + assert len(iterate_data) == ( + len(callback.iterate_data) + len(callback.primal_values) + len(callback.primal_residuals) + ) + + df = pd.DataFrame(iterate_data) + if not args.no_save: + print(f"Saving iterate data to {fpath}") + df.to_csv(fpath) + else: + print("--no-save is set. Not saving iterate data") + + +if __name__ == "__main__": + argparser = config.get_sweep_argparser() + + argparser.add_argument( + "--model", + default="fullspace", + help="Options are 'fullspace', 'implcit', 'alamo', or 'nn-full'. Default is 'fullspace'.", + ) + argparser.add_argument( + "--fname", + default=None, + help="Basename for iterate data CSV file. Default is: {model}-iterates-{sample}.csv", + ) + argparser.add_argument( + "--sample", + default=None, + type=int, + help="Index of conversion/pressure parameter sample to use. Default is X=0.95, P=1.55 MPa", + ) + + args = argparser.parse_args() + if args.subset is not None: + raise RuntimeError("--subset cannot be provided. Use --sample instead") + main(args) From fab0cb1f985bd217a491000f7bc277ec2d5915cc Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:26:36 -0600 Subject: [PATCH 25/36] add consistent-call-signature constructors to config to support scripts that take a --model argument --- svi/auto_thermal_reformer/config.py | 69 +++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/svi/auto_thermal_reformer/config.py b/svi/auto_thermal_reformer/config.py index 8242050..540f464 100644 --- a/svi/auto_thermal_reformer/config.py +++ b/svi/auto_thermal_reformer/config.py @@ -23,6 +23,60 @@ import itertools import pyomo.environ as pyo from svi.cyipopt import TimedPyomoCyIpoptSolver, Callback +from svi.external import add_external_function_libraries_to_environment + + +"""Consistent-signature callbacks to construct each model + +All models returned are initialized and ready to solve. + +Note that these constructors use local imports to avoid circular dependencies. +The right solution here is probably to separate the single-model simulation drivers +(which depend on config) from the model constructing "library" functions (which we +need for these constructor callbacks). + +""" + +def fullspace_constructor(X, P, **kwds): + from svi.auto_thermal_reformer.fullspace_flowsheet import make_optimization_model as create_fullspace_instance + m = create_fullspace_instance(X, P) + return m + + +def alamo_constructor(X, P, **kwds): + from svi.auto_thermal_reformer.alamo_flowsheet import ( + create_instance as create_alamo_instance, + initialize_alamo_atr_flowsheet, + ) + surrogate_fname = kwds.pop("surrogate_fname", None) + m = create_alamo_instance(X, P, surrogate_fname=surrogate_fname) + initialize_alamo_atr_flowsheet(m) + m.fs.reformer_bypass.inlet.temperature.unfix() + m.fs.reformer_bypass.inlet.flow_mol.unfix() + return m + + +def nn_full_constructor(X, P, **kwds): + from svi.auto_thermal_reformer.nn_flowsheet import ( + create_instance as create_nn_instance, + initialize_nn_atr_flowsheet, + ) + surrogate_fname = kwds.pop("surrogate_fname", None) + # Note that KerasSurrogate.Formulation.FULL_SPACE is the default + m = create_nn_instance(X, P, surrogate_fname=surrogate_fname) + initialize_nn_atr_flowsheet(m) + m.fs.reformer_bypass.inlet.temperature.unfix() + m.fs.reformer_bypass.inlet.flow_mol.unfix() + return m + + +def implicit_constructor(X, P, **kwds): + from svi.auto_thermal_reformer.fullspace_flowsheet import make_optimization_model as create_fullspace_instance + from svi.auto_thermal_reformer.implicit_flowsheet import make_implicit + m = create_fullspace_instance(X, P) + add_external_function_libraries_to_environment(m) + m_implicit = make_implicit(m) + return m_implicit filedir = os.path.dirname(__file__) @@ -43,13 +97,22 @@ 'CH4 Feed', ] -def get_optimization_solver(options=None, iters = 300): + +CONSTRUCTOR_LOOKUP = { + "fullspace": fullspace_constructor, + "implicit": implicit_constructor, + "alamo": alamo_constructor, + "nn-full": nn_full_constructor, +} + +def get_optimization_solver(options=None, iters=300, callback=None): # Use cyipopt for everything for Ipopt version consistency among all # formulations #solver = pyo.SolverFactory("cyipopt") # This is a very simple callback we just use to get iteration counts. - cb = Callback() - solver = TimedPyomoCyIpoptSolver(intermediate_callback=cb) + if callback is None: + callback = Callback() + solver = TimedPyomoCyIpoptSolver(intermediate_callback=callback) if options is None: options = {} solver.config.options["max_iter"] = iters From 323a280276bb9b562618bcc1b4cfd2f77af49616 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:38:05 -0600 Subject: [PATCH 26/36] use config.get_optimization_solver in fullspace flowsheet __main__ --- svi/auto_thermal_reformer/fullspace_flowsheet.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/svi/auto_thermal_reformer/fullspace_flowsheet.py b/svi/auto_thermal_reformer/fullspace_flowsheet.py index 841cf1a..84c27cd 100644 --- a/svi/auto_thermal_reformer/fullspace_flowsheet.py +++ b/svi/auto_thermal_reformer/fullspace_flowsheet.py @@ -70,6 +70,7 @@ from svi.external import add_external_function_libraries_to_environment from svi.auto_thermal_reformer.reactor_model import add_reactor_model +import svi.auto_thermal_reformer.config as config import argparse @@ -383,6 +384,7 @@ def add_obj_and_constraints(m): ####### OBJECTIVE IS TO MAXIMIZE H2 COMPOSITION IN PRODUCT STREAM ####### m.fs.obj = pyo.Objective( expr=m.fs.product.mole_frac_comp[0, "H2"], sense=pyo.maximize + #expr=-m.fs.product.mole_frac_comp[0, "H2"], sense=pyo.minimize ) # MINIMUM PRODUCT FLOW OF 3500 mol/s IN PRODUCT STREAM @@ -465,10 +467,15 @@ def make_optimization_model(X,P,initialize=True): simulate = not args.optimize if args.optimize: - P = 1600000.0 + P = 1500000.0 X = 0.95 m = make_optimization_model(X, P, initialize=True) - res = pyo.SolverFactory("ipopt").solve(m, tee=True) + m.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT) + m.ipopt_zL_out = pyo.Suffix(direction=pyo.Suffix.IMPORT) + m.ipopt_zU_out = pyo.Suffix(direction=pyo.Suffix.IMPORT) + + solver = config.get_optimization_solver() + res = solver.solve(m, tee=True) elif simulate: P = 1600000.0 From a7c0836cea0bcefd119054476c26fe497ea1658b Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:40:48 -0600 Subject: [PATCH 27/36] get surrogate from results_dir rather than data_dir --- svi/auto_thermal_reformer/generate_alamo_surrogate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svi/auto_thermal_reformer/generate_alamo_surrogate.py b/svi/auto_thermal_reformer/generate_alamo_surrogate.py index 4298c09..c508f49 100644 --- a/svi/auto_thermal_reformer/generate_alamo_surrogate.py +++ b/svi/auto_thermal_reformer/generate_alamo_surrogate.py @@ -124,7 +124,7 @@ def main(): args = argparser.parse_args() - surrogate_fname = os.path.join(args.data_dir, args.surrogate_fname) + surrogate_fname = os.path.join(args.results_dir, args.surrogate_fname) train_plot = os.path.join(args.results_dir, args.train_plot) val_plot = os.path.join(args.results_dir, args.val_plot) From 46a832a4bc853a3cdd4fc4a25e45ffce96039d2e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:44:04 -0600 Subject: [PATCH 28/36] ipopt callback to get full state of system and condition numbers; option to include condition numbers in iterate data --- .../generate_iterate_data.py | 25 ++- svi/cyipopt.py | 172 ++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) diff --git a/svi/auto_thermal_reformer/generate_iterate_data.py b/svi/auto_thermal_reformer/generate_iterate_data.py index 61f594c..0a46988 100644 --- a/svi/auto_thermal_reformer/generate_iterate_data.py +++ b/svi/auto_thermal_reformer/generate_iterate_data.py @@ -33,7 +33,17 @@ def main(args): xp_samples = config.get_parameter_samples(args) conversion, pressure = xp_samples[args.sample] m = config.CONSTRUCTOR_LOOKUP[args.model](conversion, pressure) - callback = FullStateCallback() + + dof_varnames = [ + "fs.reformer_bypass.split_fraction[0.0,bypass_outlet]", + "fs.reformer_mix.steam_inlet_state[0.0].flow_mol", + "fs.feed.properties[0.0].flow_mol", + ] + callback = FullStateCallback( + include_condition=args.include_condition, + include_block_condition=args.include_block_condition, + dof_varnames=dof_varnames, + ) solver = config.get_optimization_solver(callback=callback) results = solver.solve(m, tee=True) @@ -57,6 +67,11 @@ def main(args): len(callback.iterate_data) + len(callback.primal_values) + len(callback.primal_residuals) ) + if args.include_condition: + iterate_data["condition-number"] = list(callback.condition_numbers) + if args.include_block_condition: + iterate_data.update(callback.block_condition_numbers) + df = pd.DataFrame(iterate_data) if not args.no_save: print(f"Saving iterate data to {fpath}") @@ -84,6 +99,14 @@ def main(args): type=int, help="Index of conversion/pressure parameter sample to use. Default is X=0.95, P=1.55 MPa", ) + argparser.add_argument( + "--include-condition", + action="store_true", + ) + argparser.add_argument( + "--include-block-condition", + action="store_true", + ) args = argparser.parse_args() if args.subset is not None: diff --git a/svi/cyipopt.py b/svi/cyipopt.py index 8d16e2a..7546cae 100644 --- a/svi/cyipopt.py +++ b/svi/cyipopt.py @@ -16,6 +16,7 @@ from pyomo.opt.results.solution import Solution import numpy as np from scipy import sparse +from pyomo.contrib.incidence_analysis.triangularize import block_triangularize pyomo_nlp = attempt_import("pyomo.contrib.pynumero.interfaces.pyomo_nlp")[0] pyomo_grey_box = attempt_import("pyomo.contrib.pynumero.interfaces.pyomo_grey_box_nlp")[ @@ -373,6 +374,177 @@ def __call__( cond = np.linalg.cond(jac.toarray()) self.condition_numbers.append(cond) + +def extract_submatrix(coo, rows, cols): + new_rows = [] + new_cols = [] + new_data = [] + row_old2new = {r: i for i, r in enumerate(rows)} + col_old2new = {c: i for i, c in enumerate(cols)} + for r, c, d in zip(coo.row, coo.col, coo.data): + if r in row_old2new and c in col_old2new: + new_rows.append(row_old2new[r]) + new_cols.append(col_old2new[c]) + new_data.append(d) + new_coo = sparse.coo_matrix( + (new_data, (new_rows, new_cols)), + shape=(len(rows), len(cols)), + ) + return new_coo + + +class FullStateCallback: + + def __init__( + self, + include_condition=False, + include_block_condition=False, + dof_varnames=None, + ): + # TODO: We really don't want to re-use any any of this data from + # solve to solve. We need to store it, somehow, so we can access + # if after the callback function. Probably just need an explicit + # method to clear the cached data? Or check the NLP and make sure + # we're not being called with different NLPs? + # + # TODO: Option to compute condition numbers? + self.iterate_data = { + "alg_mod": [], + "iter_count": [], + "obj_value": [], + "inf_pr": [], + "inf_du": [], + "mu": [], + "d_norm": [], + "regularization_size": [], + "alpha_du": [], + "alpha_pr": [], + "ls_trials": [], + } + self.primal_values = {} + self.primal_residuals = {} + # TODO: Dual variables/residuals? + + self._cached_var_names = None + self._cached_con_names = None + + if include_condition: + self.condition_numbers = [] + else: + self.condition_numbers = None + + # Don't initialize with an empty list as we don't know how many elements + # this list will need. + self._include_block_condition = include_block_condition + if self._include_block_condition: + if dof_varnames is None: + raise RuntimeError("Degrees of freedom must be provided") + self._dof_varnames = dof_varnames + self.block_coords = None + self.block_condition_numbers = None + + def __call__( + self, + nlp, + ipopt_problem, + alg_mod, + iter_count, + obj_value, + inf_pr, + inf_du, + mu, + d_norm, + regularization_size, + alpha_du, + alpha_pr, + ls_trials, + ): + self.iterate_data["alg_mod"].append(alg_mod) + self.iterate_data["iter_count"].append(iter_count) + self.iterate_data["obj_value"].append(obj_value) + self.iterate_data["inf_pr"].append(inf_pr) + self.iterate_data["inf_du"].append(inf_du) + self.iterate_data["mu"].append(mu) + self.iterate_data["d_norm"].append(d_norm) + self.iterate_data["regularization_size"].append(regularization_size) + self.iterate_data["alpha_du"].append(alpha_du) + self.iterate_data["alpha_pr"].append(alpha_pr) + self.iterate_data["ls_trials"].append(ls_trials) + + iterate = ipopt_problem.get_current_iterate(scaled=False) + # NOTE: I'm assuming that the Ipopt iterate vector has the same order as the NLP. + # I'm pretty sure I've verified this before... + primal_values = iterate["x"] + if self._cached_var_names is None: + #self._cached_var_names = [var.name for var in nlp.get_pyomo_variables()] + self._cached_var_names = nlp.primals_names() + if not self.primal_values: + # If we haven't initialized this dict with variable names + for name, value in zip(self._cached_var_names, primal_values): + self.primal_values[name] = [value] + else: + # We have initialized with variable names + for name, value in zip(self._cached_var_names, primal_values): + self.primal_values[name].append(value) + + infeas = ipopt_problem.get_current_violations(scaled=False) + primal_residuals = infeas["g_violation"] + if self._cached_con_names is None: + #self._cached_con_names = [con.name for con in nlp.get_pyomo_constraints()] + self._cached_con_names = nlp.constraint_names() + if not self.primal_residuals: + for name, value in zip(self._cached_con_names, primal_residuals): + self.primal_residuals[name] = [value] + else: + for name, value in zip(self._cached_con_names, primal_residuals): + self.primal_residuals[name].append(value) + + # TODO: Could also track multipliers and dual infeasibility. This could be + # useful. + if self.condition_numbers is not None: + jac = nlp.evaluate_jacobian() + cond = np.linalg.cond(jac.toarray()) + self.condition_numbers.append(cond) + + if self._include_block_condition: + dof_varnames = set(self._dof_varnames) + var_coords = [i for i, vname in enumerate(self._cached_var_names) if vname not in dof_varnames] + square_varnames = [vname for i, vname in enumerate(self._cached_var_names) if vname not in dof_varnames] + name_to_new_coord = { + vname: i + for i, vname in enumerate(square_varnames) + if vname not in dof_varnames + } + con_coords = list(range(nlp.n_eq_constraints())) + vcoord_set = set(var_coords) + + jac = nlp.evaluate_jacobian_eq() + + row = [] + col = [] + data = [] + for r, c, val in zip(jac.row, jac.col, jac.data): + if c in vcoord_set: + new_coord = name_to_new_coord[self._cached_var_names[c]] + row.append(r) + col.append(new_coord) + data.append(val) + square_jac = sparse.coo_matrix((data, (row, col)), shape=(len(con_coords), len(var_coords))) + # Row and column blocks + rblocks, cblocks = block_triangularize(square_jac) + + if self.block_condition_numbers is None: + self.block_condition_numbers = { + f"block-{i}-cond": [] + for i in range(len(rblocks)) if len(rblocks[i]) > 1 + } + for i, (rb, cb) in enumerate(zip(rblocks, cblocks)): + if len(rb) > 1: + submat = extract_submatrix(square_jac, rb, cb) + cond = np.linalg.cond(submat.toarray()) + self.block_condition_numbers[f"block-{i}-cond"].append(cond) + + def get_gradient_of_lagrangian( nlp, primal_lb_multipliers, From c0063aa3456b37f4259aa188246c79aa4eb3df7a Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:44:25 -0600 Subject: [PATCH 29/36] move keys onto new lines --- .../generate_training_data.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/svi/auto_thermal_reformer/generate_training_data.py b/svi/auto_thermal_reformer/generate_training_data.py index 10aba3a..8b44d7f 100644 --- a/svi/auto_thermal_reformer/generate_training_data.py +++ b/svi/auto_thermal_reformer/generate_training_data.py @@ -64,8 +64,26 @@ def atr_data_gen( num_samples=600, regular_samples=False, ): - df = {'Fin_CH4':[], 'Tin_CH4':[], 'Fin_H2O':[], 'Conversion': [], 'HeatDuty':[], 'Fout':[], 'Tout':[], 'H2':[], - 'CO':[], 'H2O':[], 'CO2':[], 'CH4':[], 'C2H6':[], 'C3H8':[], 'C4H10':[], 'N2':[], 'O2':[], 'Ar':[]} + df = { + 'Fin_CH4':[], + 'Tin_CH4':[], + 'Fin_H2O':[], + 'Conversion': [], + 'HeatDuty':[], + 'Fout':[], + 'Tout':[], + 'H2':[], + 'CO':[], + 'H2O':[], + 'CO2':[], + 'CH4':[], + 'C2H6':[], + 'C3H8':[], + 'C4H10':[], + 'N2':[], + 'O2':[], + 'Ar':[], + } # Inputs are in the order conversion, F_H2O, F_CH4, T input_ranges = [(0.8, 0.95), (200.0, 350.0), (600.0, 900.0), (600.0, 900.0)] From a94166193a9c9cc2bd22c2da583d3f14bbe0efb8 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:55:40 -0600 Subject: [PATCH 30/36] check for idaes < 2.5 before adding inert components to to_exclude --- svi/auto_thermal_reformer/implicit_flowsheet.py | 9 +++++++-- svi/auto_thermal_reformer/plot_implicit_imat.py | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/svi/auto_thermal_reformer/implicit_flowsheet.py b/svi/auto_thermal_reformer/implicit_flowsheet.py index 50316e4..d6f1677 100644 --- a/svi/auto_thermal_reformer/implicit_flowsheet.py +++ b/svi/auto_thermal_reformer/implicit_flowsheet.py @@ -38,6 +38,7 @@ from idaes.models.properties.modular_properties import GenericParameterBlock from idaes.core.util.model_statistics import degrees_of_freedom +import idaes from idaes.models.unit_models import ( Mixer, Heater, @@ -118,8 +119,12 @@ def make_implicit(m, **kwds): # coefficients with values of zero were handled. This should be fixed in # recent Pyomo main, but we leave this explicit filtering in here as we # often run using the latest release. - to_exclude.add(m.fs.reformer.lagrange_mult[0, "N"]) - to_exclude.add(m.fs.reformer.lagrange_mult[0, "Ar"]) + # + # NOTE: As of IDAES 2.5, lagrange multipliers for inert components are not + # included in the Gibbs reactor. + if (idaes.ver.package_version.major, idaes.ver.package_version.minor) < (2, 5): + to_exclude.add(m.fs.reformer.lagrange_mult[0, "N"]) + to_exclude.add(m.fs.reformer.lagrange_mult[0, "Ar"]) external_vars = [var for var in reformer_igraph.variables if var not in to_exclude] external_var_set = ComponentSet(external_vars) diff --git a/svi/auto_thermal_reformer/plot_implicit_imat.py b/svi/auto_thermal_reformer/plot_implicit_imat.py index 3c2e50b..ebbd49b 100644 --- a/svi/auto_thermal_reformer/plot_implicit_imat.py +++ b/svi/auto_thermal_reformer/plot_implicit_imat.py @@ -37,6 +37,7 @@ from idaes.models.properties.modular_properties import GenericParameterBlock from idaes.core.util.model_statistics import degrees_of_freedom +import idaes from idaes.models.unit_models import ( Mixer, Heater, @@ -112,8 +113,12 @@ def make_implicit(m): # coefficients with values of zero were handled. This should be fixed in # recent Pyomo main, but we leave this explicit filtering in here as we # often run using the latest release. - to_exclude.add(m.fs.reformer.lagrange_mult[0, "N"]) - to_exclude.add(m.fs.reformer.lagrange_mult[0, "Ar"]) + # + # NOTE: As of IDAES v... (2.5 or so), these inert components don't exist in + # the model. + if (idaes.ver.package_version.major, idaes.ver.package_version.minor) < (2, 5): + to_exclude.add(m.fs.reformer.lagrange_mult[0, "N"]) + to_exclude.add(m.fs.reformer.lagrange_mult[0, "Ar"]) external_vars = [var for var in reformer_igraph.variables if var not in to_exclude] external_var_set = ComponentSet(external_vars) From 06507b18332843d09ea120699025261a6cfc6e6e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:55:59 -0600 Subject: [PATCH 31/36] use --tune flag in makefile --- svi/auto_thermal_reformer/makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svi/auto_thermal_reformer/makefile b/svi/auto_thermal_reformer/makefile index d7e381f..836f4f5 100644 --- a/svi/auto_thermal_reformer/makefile +++ b/svi/auto_thermal_reformer/makefile @@ -3,7 +3,7 @@ SUBSET = "0,2,4,6,7,8,14,18,21,24,25,26,27,30,32,35,36,37,38,39,41,42,44,45,47" train: python generate_training_data.py --regular-samples python generate_alamo_surrogate.py data/training-data.csv - python nn_tuning_training.py data/training-data.csv + python nn_tuning_training.py data/training-data.csv --tune sweep: python run_fullspace_sweep.py From 0b7dd42848f9d2198f8cbcc83a44a3d8f329ffa2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 15:59:19 -0600 Subject: [PATCH 32/36] print info for nn every time we save a new best --- svi/auto_thermal_reformer/nn_tuning_training.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/svi/auto_thermal_reformer/nn_tuning_training.py b/svi/auto_thermal_reformer/nn_tuning_training.py index 4cfccb0..d92edf7 100644 --- a/svi/auto_thermal_reformer/nn_tuning_training.py +++ b/svi/auto_thermal_reformer/nn_tuning_training.py @@ -136,6 +136,14 @@ def gibbs_to_nn( keras_surrogate.save_to_folder(surrogate_fname) print(f"Saved NN surrogate model to {surrogate_fname}") + print( + f"Parameters of model that was just saved:" + f"activation={activation}," + f"optimizer={optimizer}," + f"n_hidden_layers={n_hidden_layers}," + f"n_nodes_per_payer={n_nodes_per_layer}," + ) + t1 = time.time() total_time = t1 - t0 print("Total time: ", total_time) From 7e3e93ea5ea2e33c6c4deffb94fcba541e499581 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 16:00:33 -0600 Subject: [PATCH 33/36] print variables and constraints of reactor model --- svi/auto_thermal_reformer/reactor_model.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/svi/auto_thermal_reformer/reactor_model.py b/svi/auto_thermal_reformer/reactor_model.py index 6d07242..ed51450 100644 --- a/svi/auto_thermal_reformer/reactor_model.py +++ b/svi/auto_thermal_reformer/reactor_model.py @@ -194,3 +194,29 @@ def create_instance( solver.solve(m, tee=True) tol = 1e-5 validate_solution(m, tolerance=tol) + + print("Variables") + print("---------") + for var in igraph.variables: + print(var.name) + print() + + print("Constraints") + print("-----------") + for con in igraph.constraints: + print(con.name) + print() + + vblocks, cblocks = igraph.block_triangularize() + for i, (vb, cb) in enumerate(zip(vblocks, cblocks)): + print(f"Block {i}") + print(f"=========") + print("Variables") + print("---------") + for var in vb: + print(f" {var.name}") + print("Constraints") + print("-----------") + for con in cb: + print(f" {con.name}") + print() From 65ccdcca8cfc88b061763298c078c27790b7d607 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 16:01:51 -0600 Subject: [PATCH 34/36] remove parity plot pdfs; these can be re-constructed --- .../results/parity_train_atr_NN.pdf | Bin 104976 -> 0 bytes .../results/parity_val_atr_nn.pdf | Bin 73165 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 svi/auto_thermal_reformer/results/parity_train_atr_NN.pdf delete mode 100644 svi/auto_thermal_reformer/results/parity_val_atr_nn.pdf diff --git a/svi/auto_thermal_reformer/results/parity_train_atr_NN.pdf b/svi/auto_thermal_reformer/results/parity_train_atr_NN.pdf deleted file mode 100644 index 4c16c14ca14f0ea177dc8ab6ef80425c0a9fb886..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104976 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
^Ag2jp}eeni4L zIRyc`b@LaG#IBbOBos7V-7g-w9%sPGb!!s{fq_u Date: Thu, 13 Jun 2024 16:02:47 -0600 Subject: [PATCH 35/36] more plotting scripts --- .../plot_iterate_condition_number.py | 143 ++++++++++++++++++ .../plot_primal_dual_infeas.py | 84 ++++++++++ .../plot_residual_histogram.py | 92 +++++++++++ .../plot_sum_constraint_residual.py | 107 +++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 svi/auto_thermal_reformer/plot_iterate_condition_number.py create mode 100644 svi/auto_thermal_reformer/plot_primal_dual_infeas.py create mode 100644 svi/auto_thermal_reformer/plot_residual_histogram.py create mode 100644 svi/auto_thermal_reformer/plot_sum_constraint_residual.py diff --git a/svi/auto_thermal_reformer/plot_iterate_condition_number.py b/svi/auto_thermal_reformer/plot_iterate_condition_number.py new file mode 100644 index 0000000..afe74d6 --- /dev/null +++ b/svi/auto_thermal_reformer/plot_iterate_condition_number.py @@ -0,0 +1,143 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import svi.auto_thermal_reformer.config as config +import pandas as pd +import matplotlib.pyplot as plt + + +LABEL_LOOKUP = { + "inf_pr": "Primal infeasibility", + "inf_du": "Dual infeasibility", + "condition-number": "Condition number", + "fullspace": "Full-space", + "implicit": "Implicit function", + "nn-full": "Neural network", + "alamo": "ALAMO surrogate", +} + + +FONTSIZE = 16 + + +def plot_trajectory( + df, + keys, + labels=None, + fig=None, + ax=None, + iter_lim=None, +): + plt.rcParams["font.size"] = FONTSIZE + if fig is None or ax is None: + fig, ax = plt.subplots() + + iterations = list(range(len(df))) + + for i, key in enumerate(keys): + if labels is None: + label = LABEL_LOOKUP[key] if key in LABEL_LOOKUP else key + else: + label = labels[i] + ax.plot( + iterations, + list(df[key]), + label=label, + linewidth=2, + ) + if not args.no_legend: + ax.legend() + ax.set_yscale("log") + ax.xaxis.set_tick_params(length=0) + ax.yaxis.set_tick_params(length=0) + ax.set_xlabel("Iteration number") + if iter_lim is not None: + ax.set_xlim(None, iter_lim) + return fig, ax + + +def main(args): + fpaths = args.fpaths.split(",") + models = [] + for fpath in fpaths: + model = None + for name in config.CONSTRUCTOR_LOOKUP: + if name in os.path.basename(fpath): + model = name + break + if model is None: + raise RuntimeError( + f"Cannot infer model from fpath {fpath}" + ) + models.append(model) + + dfs = [pd.read_csv(fpath) for fpath in fpaths] + #keys = [key for key in args.keys.split(",") if key != ""] + keys = ["condition-number"] + labels = [LABEL_LOOKUP[model] for model in models] + + plt.rcParams["font.size"] = FONTSIZE + fig, ax = plt.subplots() + for i, df in enumerate(dfs): + plot_trajectory( + df, + keys, + # Use the labels arg here to override the default, which is to lookup + # labels by keys. + labels=[labels[i]], + fig=fig, + ax=ax, + iter_lim=args.iter_lim, + ) + + if args.show: + plt.show() + + if not args.no_save: + fig.tight_layout() + # Assume file name is NAME.ext + if len(models) == 1: + name = os.path.basename(args.fpath).split(".")[0] + fname = name + "-condition.pdf" + fpath = os.path.join(args.results_dir, fname) + else: + fname = "-".join(models) + "-iterates-condition.pdf" + fpath = os.path.join(args.results_dir, fname) + fig.savefig(fpath, transparent=not args.opaque) + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + + argparser.add_argument( + "fpaths", + help="Comma-separated list of files containing iterate data to plot", + ) + argparser.add_argument( + "--iter-lim", + type=int, + default=None, + help="Upper bound on x-axis", + ) + + args = argparser.parse_args() + main(args) diff --git a/svi/auto_thermal_reformer/plot_primal_dual_infeas.py b/svi/auto_thermal_reformer/plot_primal_dual_infeas.py new file mode 100644 index 0000000..4172458 --- /dev/null +++ b/svi/auto_thermal_reformer/plot_primal_dual_infeas.py @@ -0,0 +1,84 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import svi.auto_thermal_reformer.config as config +import pandas as pd +import matplotlib.pyplot as plt + + +LABEL_LOOKUP = { + "inf_pr": "Primal infeasibility", + "inf_du": "Dual infeasibility", +} + + +def plot_trajectory( + df, + keys, + labels=None, +): + plt.rcParams["font.size"] = 16 + fig, ax = plt.subplots() + + iterations = list(range(len(df))) + + for i, key in enumerate(keys): + label = LABEL_LOOKUP[key] if key in LABEL_LOOKUP else key + ax.plot( + iterations, + list(df[key]), + label=label, + linewidth=2, + ) + ax.legend() + ax.set_yscale("log") + ax.xaxis.set_tick_params(length=0) + ax.yaxis.set_tick_params(length=0) + ax.set_xlabel("Iteration number") + return fig, ax + + +def main(args): + df = pd.read_csv(args.fpath) + #keys = [key for key in args.keys.split(",") if key != ""] + keys = ["inf_pr", "inf_du"] + fig, ax = plot_trajectory(df, keys) + + if args.show: + plt.show() + + if not args.no_save: + fig.tight_layout() + # Assume file name is NAME.ext + name = os.path.basename(args.fpath).split(".")[0] + fname = name + "-pdinfeas.pdf" + fpath = os.path.join(args.results_dir, fname) + fig.savefig(fpath, transparent=not args.opaque) + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + + argparser.add_argument("fpath", help="File containing iterate data to plot") + + args = argparser.parse_args() + main(args) diff --git a/svi/auto_thermal_reformer/plot_residual_histogram.py b/svi/auto_thermal_reformer/plot_residual_histogram.py new file mode 100644 index 0000000..99d1c9a --- /dev/null +++ b/svi/auto_thermal_reformer/plot_residual_histogram.py @@ -0,0 +1,92 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import svi.auto_thermal_reformer.config as config +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from pyomo.core.base.constraint import Constraint + + +def main(args): + df = pd.read_csv(args.fpath) + + model = None + for key in config.CONSTRUCTOR_LOOKUP: + if key in os.path.basename(args.fpath): + model = key + if model is None: + raise RuntimeError("Could not infer model from filename") + # Just choose some dummy values of conversion and pressure. We won't + # actually use this model numerically + X = 0.94 + P = 1550000.0 + m = config.CONSTRUCTOR_LOOKUP[model](X, P) + connames = [con.name for con in m.component_data_objects(Constraint)] + + con_infeas_count = {} + for name in connames: + resid_array = np.array(df[name]) + violated = resid_array > args.infeas_threshold + n_violated = sum(violated) + con_infeas_count[name] = n_violated + + constraints_by_infeas = sorted( + connames, + reverse=True, + key=lambda name: con_infeas_count[name], + ) + infeas_counts = [con_infeas_count[name] for name in constraints_by_infeas] + constraint_indices = list(range(len(constraints_by_infeas))) + + plt.rcParams["font.size"] = 20 + fig, ax = plt.subplots() + ax.bar(constraint_indices, infeas_counts) + ax.set_ylabel("Number of\niterations", rotation=0) + ax.set_xlabel("Constraint indices") + ax.yaxis.set_label_coords(-0.3, 0.5) + ax.xaxis.set_tick_params(length=0) + ax.yaxis.set_tick_params(length=0) + + w, h = fig.get_size_inches() + fig.set_size_inches(w*1.3, h) + + fig.tight_layout() + if args.show: + plt.show() + + if not args.no_save: + # Assume file name is NAME.ext + name = os.path.basename(args.fpath).split(".")[0] + fname = name + "-resid-histogram.pdf" + fpath = os.path.join(args.results_dir, fname) + fig.savefig(fpath, transparent=not args.opaque) + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + + argparser.add_argument("fpath", help="File containing iterate data to plot") + argparser.add_argument("--infeas-threshold", type=float, default=1e-3) + + args = argparser.parse_args() + main(args) diff --git a/svi/auto_thermal_reformer/plot_sum_constraint_residual.py b/svi/auto_thermal_reformer/plot_sum_constraint_residual.py new file mode 100644 index 0000000..81cd18e --- /dev/null +++ b/svi/auto_thermal_reformer/plot_sum_constraint_residual.py @@ -0,0 +1,107 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import svi.auto_thermal_reformer.config as config +import pandas as pd +import matplotlib.pyplot as plt + + +LABEL_LOOKUP = { + "inf_pr": "Primal infeasibility", + "inf_du": "Dual infeasibility", + "fs.reformer.control_volume.properties_out[0.0].sum_mole_frac_out": "Reformer sum mole fraction", + "fs.reformer_recuperator.hot_side.properties_out[0.0].sum_mole_frac_out": "Recuperator sum mole fraction", +} + + +def plot_trajectory( + df, + keys, + labels=None, + iter_lim=None, + legend=True, +): + plt.rcParams["font.size"] = 16 + fig, ax = plt.subplots() + + iterations = list(range(len(df))) + + w, h = fig.get_size_inches() + fig.set_size_inches(w*1.5, h) + for i, key in enumerate(keys): + label = LABEL_LOOKUP[key] if key in LABEL_LOOKUP else key + ax.plot( + iterations, + list(df[key]), + label=label, + linewidth=2, + ) + if legend: + ax.legend(loc=(0.6, 0.5)) + ax.set_yscale("log") + ax.xaxis.set_tick_params(length=0) + ax.yaxis.set_tick_params(length=0) + ax.set_xlabel("Iteration number") + if iter_lim is not None: + ax.set_xlim(-1, iter_lim) + return fig, ax + + +def main(args): + df = pd.read_csv(args.fpath) + #keys = [key for key in args.keys.split(",") if key != ""] + keys = [ + "fs.reformer.control_volume.properties_out[0.0].sum_mole_frac_out", + "fs.reformer_recuperator.hot_side.properties_out[0.0].sum_mole_frac_out", + ] + fig, ax = plot_trajectory( + df, + keys, + iter_lim=args.iter_lim, + legend=not args.no_legend, + ) + + fig.tight_layout() + if args.show: + plt.show() + + if not args.no_save: + # Assume file name is NAME.ext + name = os.path.basename(args.fpath).split(".")[0] + fname = name + "-sum-mole-frac.pdf" + fpath = os.path.join(args.results_dir, fname) + fig.savefig(fpath, transparent=not args.opaque) + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + + argparser.add_argument("fpath", help="File containing iterate data to plot") + argparser.add_argument( + "--iter-lim", + type=int, + default=None, + help="Upper bound on x-axis", + ) + + args = argparser.parse_args() + main(args) From a63a2d40c4f1a7d30d58d0234d194a41fc314cea Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 16:03:28 -0600 Subject: [PATCH 36/36] script to plot condition numbers of diagonal blocks --- .../plot_iterate_block_condition_number.py | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 svi/auto_thermal_reformer/plot_iterate_block_condition_number.py diff --git a/svi/auto_thermal_reformer/plot_iterate_block_condition_number.py b/svi/auto_thermal_reformer/plot_iterate_block_condition_number.py new file mode 100644 index 0000000..c7f1cd1 --- /dev/null +++ b/svi/auto_thermal_reformer/plot_iterate_block_condition_number.py @@ -0,0 +1,118 @@ +# ___________________________________________________________________________ +# +# Surrogate vs. Implicit: Experiments comparing nonlinear optimization +# formulations +# +# Copyright (c) 2023. Triad National Security, LLC. All rights reserved. +# +# This program was produced under U.S. Government contract 89233218CNA000001 +# for Los Alamos National Laboratory (LANL), which is operated by Triad +# National Security, LLC for the U.S. Department of Energy/National Nuclear +# Security Administration. All rights in the program are reserved by Triad +# National Security, LLC, and the U.S. Department of Energy/National Nuclear +# Security Administration. The Government is granted for itself and others +# acting on its behalf a nonexclusive, paid-up, irrevocable worldwide license +# in this material to reproduce, prepare derivative works, distribute copies +# to the public, perform publicly and display publicly, and to permit others +# to do so. +# +# This software is distributed under the 3-clause BSD license. +# ___________________________________________________________________________ + +import os +import svi.auto_thermal_reformer.config as config +import pandas as pd +import matplotlib.pyplot as plt + + +LABEL_LOOKUP = { + "inf_pr": "Primal infeasibility", + "inf_du": "Dual infeasibility", + "condition-number": "Condition number", + "fullspace": "Full-space", + "implicit": "Implicit function", + "nn-full": "Neural network", + "alamo": "ALAMO surrogate", +} + + +FONTSIZE = 16 + + +def plot_trajectory( + df, + keys, + labels=None, + fig=None, + ax=None, + iter_lim=None, + legend=True, +): + plt.rcParams["font.size"] = FONTSIZE + if fig is None or ax is None: + fig, ax = plt.subplots() + + iterations = list(range(len(df))) + + for i, key in enumerate(keys): + if labels is None: + label = LABEL_LOOKUP[key] if key in LABEL_LOOKUP else key + else: + label = labels[i] + ax.plot( + iterations, + list(df[key]), + label=label, + linewidth=2, + ) + if legend: + ax.legend() + ax.set_yscale("log") + ax.xaxis.set_tick_params(length=0) + ax.yaxis.set_tick_params(length=0) + ax.set_xlabel("Iteration number") + if iter_lim is not None: + ax.set_xlim(None, iter_lim) + return fig, ax + + +def main(args): + fpath = args.fpath + df = pd.read_csv(fpath) + keys = [colname for colname in df.columns if colname.startswith("block") and colname.endswith("cond")] + + plot_trajectory( + df, + keys, + iter_lim=args.iter_lim, + legend=not args.no_legend, + ) + + if args.show: + plt.show() + + if not args.no_save: + fig.tight_layout() + # Assume file name is NAME.ext + name = os.path.basename(args.fpath).split(".")[0] + fname = name + "-block-condition.pdf" + fpath = os.path.join(args.results_dir, fname) + fig.savefig(fpath, transparent=not args.opaque) + + +if __name__ == "__main__": + argparser = config.get_plot_argparser() + + argparser.add_argument( + "fpath", + help="File containing iterate data to plot", + ) + argparser.add_argument( + "--iter-lim", + type=int, + default=None, + help="Upper bound on x-axis", + ) + + args = argparser.parse_args() + main(args)