From 84c832f48f5879d1b5fad82cd71cea3da33f3884 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 08:28:54 -0700 Subject: [PATCH 01/60] working on a module for flexibility analysis --- idaes/apps/__init__.py | 0 idaes/apps/flexibility_analysis/__init__.py | 6 + .../decision_rules/__init__.py | 0 .../decision_rules/dr_config.py | 7 + .../decision_rules/dr_enum.py | 6 + .../decision_rules/linear_dr.py | 62 +++ .../decision_rules/relu_dr.py | 97 ++++ .../decision_rules/tests/__init__.py | 0 .../decision_rules/tests/test_linear_dr.py | 52 ++ .../flexibility_analysis/examples/__init__.py | 0 .../examples/idaes_hx_network.py | 352 +++++++++++++ .../examples/linear_hx_network.py | 113 ++++ .../examples/nonlin_hx_network.py | 90 ++++ .../examples/tests/__init__.py | 0 idaes/apps/flexibility_analysis/flex_index.py | 183 +++++++ idaes/apps/flexibility_analysis/flextest.py | 493 ++++++++++++++++++ idaes/apps/flexibility_analysis/indices.py | 62 +++ .../flexibility_analysis/inner_problem.py | 212 ++++++++ idaes/apps/flexibility_analysis/kkt.py | 169 ++++++ idaes/apps/flexibility_analysis/sampling.py | 163 ++++++ .../flexibility_analysis/tests/__init__.py | 0 .../tests/test_flextest.py | 104 ++++ .../tests/test_indices.py | 57 ++ .../tests/test_sampling.py | 134 +++++ .../tests/test_uncertain_params.py | 113 ++++ .../tests/test_var_utils.py | 85 +++ .../flexibility_analysis/uncertain_params.py | 53 ++ idaes/apps/flexibility_analysis/var_utils.py | 57 ++ 28 files changed, 2670 insertions(+) create mode 100644 idaes/apps/__init__.py create mode 100644 idaes/apps/flexibility_analysis/__init__.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/__init__.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/dr_config.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/dr_enum.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/linear_dr.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/relu_dr.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/tests/__init__.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py create mode 100644 idaes/apps/flexibility_analysis/examples/__init__.py create mode 100644 idaes/apps/flexibility_analysis/examples/idaes_hx_network.py create mode 100644 idaes/apps/flexibility_analysis/examples/linear_hx_network.py create mode 100644 idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py create mode 100644 idaes/apps/flexibility_analysis/examples/tests/__init__.py create mode 100644 idaes/apps/flexibility_analysis/flex_index.py create mode 100644 idaes/apps/flexibility_analysis/flextest.py create mode 100644 idaes/apps/flexibility_analysis/indices.py create mode 100644 idaes/apps/flexibility_analysis/inner_problem.py create mode 100644 idaes/apps/flexibility_analysis/kkt.py create mode 100644 idaes/apps/flexibility_analysis/sampling.py create mode 100644 idaes/apps/flexibility_analysis/tests/__init__.py create mode 100644 idaes/apps/flexibility_analysis/tests/test_flextest.py create mode 100644 idaes/apps/flexibility_analysis/tests/test_indices.py create mode 100644 idaes/apps/flexibility_analysis/tests/test_sampling.py create mode 100644 idaes/apps/flexibility_analysis/tests/test_uncertain_params.py create mode 100644 idaes/apps/flexibility_analysis/tests/test_var_utils.py create mode 100644 idaes/apps/flexibility_analysis/uncertain_params.py create mode 100644 idaes/apps/flexibility_analysis/var_utils.py diff --git a/idaes/apps/__init__.py b/idaes/apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py new file mode 100644 index 0000000000..cf072f7a8e --- /dev/null +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -0,0 +1,6 @@ +from .flextest import (solve_flextest, SamplingStrategy, FlexTestConfig, FlexTestMethod, + FlexTestTermination, FlexTestResults, SamplingConfig, FlexTest) +from .decision_rules.dr_enum import DecisionRuleTypes +from .decision_rules.linear_dr import LinearDRConfig +from .decision_rules.relu_dr import ReluDRConfig +from .flex_index import solve_flex_index diff --git a/idaes/apps/flexibility_analysis/decision_rules/__init__.py b/idaes/apps/flexibility_analysis/decision_rules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py new file mode 100644 index 0000000000..266f2b72d0 --- /dev/null +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py @@ -0,0 +1,7 @@ +from pyomo.common.config import ConfigDict + + +class DRConfig(ConfigDict): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, + visibility=visibility) diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py b/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py new file mode 100644 index 0000000000..cdfb732343 --- /dev/null +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py @@ -0,0 +1,6 @@ +import enum + + +class DecisionRuleTypes(enum.Enum): + linear = enum.auto() + relu_nn = enum.auto() diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py new file mode 100644 index 0000000000..ca664f380e --- /dev/null +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -0,0 +1,62 @@ +import pyomo.environ as pe +from typing import MutableMapping, Sequence +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.block import _BlockData +from pyomo.core.expr.numeric_expr import LinearExpression +from .dr_config import DRConfig +from pyomo.common.config import ConfigValue + + +class LinearDRConfig(DRConfig): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, + visibility=visibility) + self.solver = self.declare('solver', ConfigValue(default=pe.SolverFactory('appsi_gurobi'))) + + +def construct_linear_decision_rule(input_vals: MutableMapping[_GeneralVarData, Sequence[float]], + output_vals: MutableMapping[_GeneralVarData, Sequence[float]], + config: LinearDRConfig) -> _BlockData: + n_inputs = len(input_vals) + n_outputs = len(output_vals) + + res = pe.Block(concrete=True) + res.output_set = pe.Set(initialize=list(range(n_outputs))) + res.decision_rule = pe.Constraint(res.output_set) + + for out_ndx, (output_var, out_samples) in enumerate(output_vals.items()): + trainer = pe.ConcreteModel() + + n_samples = len(out_samples) + trainer.input_set = pe.Set(initialize=list(range(n_inputs))) + trainer.sample_set = pe.Set(initialize=list(range(n_samples))) + + trainer.const = pe.Var() + trainer.coefs = pe.Var(trainer.input_set) + trainer.out_est = pe.Var(trainer.sample_set) + + obj_expr = sum((trainer.out_est[i] - out_samples[i])**2 for i in trainer.sample_set) + trainer.objective = pe.Objective(expr=obj_expr) + + trainer.est_cons = pe.Constraint(trainer.sample_set) + for ndx in trainer.sample_set: + lin_coefs = [v[ndx] for k, v in input_vals.items()] + lin_vars = list(trainer.coefs.values()) + lin_coefs.append(1) + lin_vars.append(trainer.const) + lin_coefs.append(-1) + lin_vars.append(trainer.out_est[ndx]) + expr = LinearExpression(linear_coefs=lin_coefs, linear_vars=lin_vars) + trainer.est_cons[ndx] = (expr, 0) + + results = config.solver.solve(trainer) + pe.assert_optimal_termination(results) + + lin_coefs = [v.value for v in trainer.coefs.values()] + lin_vars = [v for v in input_vals.keys()] + lin_coefs.append(-1) + lin_vars.append(output_var) + dr_expr = LinearExpression(constant=trainer.const.value, linear_coefs=lin_coefs, linear_vars=lin_vars) + res.decision_rule[out_ndx] = (dr_expr, 0) + + return res diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py new file mode 100644 index 0000000000..e6b441ee64 --- /dev/null +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -0,0 +1,97 @@ +import numpy as np +import tensorflow as tf +from tensorflow import keras +from tensorflow.keras import layers +import pyomo.environ as pe +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.block import _BlockData +from typing import MutableMapping, Sequence +from omlt import OmltBlock, OffsetScaling +from omlt.neuralnet import ReluBigMFormulation +from omlt.io import load_keras_sequential +from .dr_config import DRConfig +from pyomo.common.config import ConfigValue + + +class ReluDRConfig(DRConfig): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, + visibility=visibility) + self.n_layers: int = self.declare('n_layers', ConfigValue(domain=int, default=4)) + self.n_nodes_per_layer: int = self.declare('n_nodes_per_layer', ConfigValue(domain=int, default=4)) + self.tensorflow_seed: int = self.declare('tensorflow_seed', ConfigValue(domain=int, default=0)) + self.scale_inputs: bool = self.declare('scale_inputs', ConfigValue(domain=bool, default=True)) + self.scale_outputs: bool = self.declare('scale_outputs', ConfigValue(domain=bool, default=True)) + self.epochs: int = self.declare('epochs', ConfigValue(domain=int, default=2000)) + self.batch_size: int = self.declare('batch_size', ConfigValue(domain=int, default=20)) + self.plot_history: bool = self.declare('plot_history', ConfigValue(domain=bool, default=False)) + + +def construct_relu_decision_rule(input_vals: MutableMapping[_GeneralVarData, Sequence[float]], + output_vals: MutableMapping[_GeneralVarData, Sequence[float]], + config: ReluDRConfig) -> _BlockData: + tf.random.set_seed(config.tensorflow_seed) + inputs = list(input_vals.keys()) + outputs = list(output_vals.keys()) + n_samples = len(input_vals[inputs[0]]) + + config: ReluDRConfig = config() + if config.batch_size > n_samples: + config.batch_size = n_samples + + training_input = np.empty((n_samples, len(inputs))) + for ndx, inp in enumerate(inputs): + training_input[:,ndx] = np.array(input_vals[inp], dtype=np.float64) + + training_output = np.empty((n_samples, len(outputs))) + for ndx, outp in enumerate(outputs): + training_output[:, ndx] = np.array(output_vals[outp], dtype=np.float64) + + if config.scale_inputs: + input_mean = training_input.mean(axis=0) + input_std = training_input.std(axis=0) + for ndx in range(len(inputs)): + training_input[:, ndx] = (training_input[:, ndx] - input_mean[ndx]) / input_std[ndx] + else: + input_mean = [0] * len(inputs) + input_std = [1] * len(inputs) + + if config.scale_outputs: + output_mean = training_output.mean(axis=0) + output_std = training_output.std(axis=0) + for ndx in range(len(outputs)): + training_output[:, ndx] = (training_output[:, ndx] - output_mean[ndx]) / output_std[ndx] + else: + output_mean = [0] * len(outputs) + output_std = [1] * len(outputs) + + nn = keras.Sequential() + nn.add(layers.Dense(units=config.n_nodes_per_layer, input_dim=len(inputs), activation='relu')) + for layer_ndx in range(config.n_layers - 1): + nn.add(layers.Dense(config.n_nodes_per_layer, activation='relu')) + nn.add(layers.Dense(len(outputs))) + nn.compile(optimizer=keras.optimizers.Adam(), loss='mse') + history = nn.fit(training_input, training_output, batch_size=config.batch_size, epochs=config.epochs, verbose=0) + + if config.plot_history: + import matplotlib.pyplot as plt + plt.scatter(history.epoch, history.history['loss']) + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.yscale('log') + plt.show() + plt.close() + + res = pe.Block(concrete=True) + res.nn = OmltBlock() + + scaler = OffsetScaling(offset_inputs=[float(i) for i in input_mean], + factor_inputs=[float(i) for i in input_std], + offset_outputs=[float(i) for i in output_mean], + factor_outputs=[float(i) for i in output_std]) + input_bounds = [(v.lb, v.ub) for v in inputs] + net = load_keras_sequential(nn, scaler, input_bounds) + formulation = ReluBigMFormulation(net) + res.nn.build_formulation(formulation, input_vars=inputs, output_vars=outputs) + + return res diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/__init__.py b/idaes/apps/flexibility_analysis/decision_rules/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py new file mode 100644 index 0000000000..aa3a149718 --- /dev/null +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -0,0 +1,52 @@ +import unittest +import pyomo.environ as pe +from flexibility.decision_rules.linear_dr import construct_linear_decision_rule +from pyomo.contrib.appsi.solvers import Gurobi +import numpy as np +from pyomo.core.expr.compare import compare_expressions + + +def y1_func(x1, x2): + return 3*x1 - 2*x2 + 5 + + +def y2_func(x1, x2): + return -x1 + 0.5*x2 + + +class TestLinearDecisionRule(unittest.TestCase): + def test_construct_linear_dr(self): + x1_samples = [float(i) for i in np.linspace(-5, 5, 100)] + x2_samples = [float(i) for i in np.linspace(-5, 5, 100)] + x1_samples.extend(float(i) for i in np.linspace(-5, 5, 100)) + x2_samples.extend(float(i) for i in np.linspace(5, -5, 100)) + + x1_samples = np.array(x1_samples) + x2_samples = np.array(x2_samples) + y1_samples = y1_func(x1_samples, x2_samples) + y2_samples = y2_func(x1_samples, x2_samples) + + m = pe.ConcreteModel() + m.x1 = pe.Var(initialize=1.7) + m.x2 = pe.Var(initialize=-3.1) + m.y1 = pe.Var(initialize=0.2) + m.y2 = pe.Var(initialize=2.5) + + input_vals = pe.ComponentMap() + input_vals[m.x1] = [float(i) for i in x1_samples] + input_vals[m.x2] = [float(i) for i in x2_samples] + + output_vals = pe.ComponentMap() + output_vals[m.y1] = [float(i) for i in y1_samples] + output_vals[m.y2] = [float(i) for i in y2_samples] + + opt = Gurobi() + m.dr = construct_linear_decision_rule(input_vals=input_vals, output_vals=output_vals, solver=opt) + + self.assertEqual(pe.value(m.dr.decision_rule[0].lower), 0) + self.assertEqual(pe.value(m.dr.decision_rule[0].lower), 0) + self.assertAlmostEqual(pe.value(m.dr.decision_rule[0].body), y1_func(m.x1.value, m.x2.value) - m.y1.value) + + self.assertEqual(pe.value(m.dr.decision_rule[1].lower), 0) + self.assertEqual(pe.value(m.dr.decision_rule[1].lower), 0) + self.assertAlmostEqual(pe.value(m.dr.decision_rule[1].body), y2_func(m.x1.value, m.x2.value) - m.y2.value) diff --git a/idaes/apps/flexibility_analysis/examples/__init__.py b/idaes/apps/flexibility_analysis/examples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py new file mode 100644 index 0000000000..283416ca79 --- /dev/null +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -0,0 +1,352 @@ +import pyomo.environ as pe +from idaes.generic_models.properties.activity_coeff_models.BTX_activity_coeff_VLE import BTXParameterBlock +from idaes.core import FlowsheetBlock +from idaes.generic_models.unit_models.heater import Heater +import coramin +import logging +from pyomo.contrib.fbbt.fbbt import fbbt +from pyomo.network import Arc +from idaes.core.util.initialization import propagate_state +from pyomo.util.infeasible import log_infeasible_constraints, log_infeasible_bounds +from idaes.core.control_volume_base import ControlVolumeBlockData +from pyomo.core.base.block import _BlockData +import numpy as np +import flexibility +from flexibility.var_utils import BoundsManager +from pyomo.core.expr.numvalue import polynomial_degree +from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression +from pyomo.repn.standard_repn import generate_standard_repn + + +logging.basicConfig(level=logging.INFO) + + +def create_model(): + m = pe.ConcreteModel() + m.fs = FlowsheetBlock(default={'dynamic': False}) + m.fs.properties = BTXParameterBlock(default={'valid_phase': 'Vap', + 'activity_coeff_model': 'Ideal', + 'state_vars': 'FTPz'}) + + m.fs.heater1 = Heater(default={'property_package': m.fs.properties}) + m.fs.cooler1 = Heater(default={'property_package': m.fs.properties}) + m.fs.heater2 = Heater(default={'property_package': m.fs.properties}) + m.fs.cooler2 = Heater(default={'property_package': m.fs.properties}) + m.fs.heater3 = Heater(default={'property_package': m.fs.properties}) + m.fs.cooler3 = Heater(default={'property_package': m.fs.properties}) + m.fs.cooler4 = Heater(default={'property_package': m.fs.properties}) + + m.fs.s1 = Arc(source=m.fs.heater1.outlet, destination=m.fs.heater2.inlet) + m.fs.s2 = Arc(source=m.fs.cooler1.outlet, destination=m.fs.cooler4.inlet) + m.fs.s3 = Arc(source=m.fs.cooler2.outlet, destination=m.fs.cooler3.inlet) + + m.fs.duty_cons = pe.ConstraintList() + m.fs.duty_cons.add(m.fs.heater1.heat_duty[0] == -m.fs.cooler1.heat_duty[0]) + m.fs.duty_cons.add(m.fs.heater2.heat_duty[0] == -m.fs.cooler2.heat_duty[0]) + m.fs.duty_cons.add(m.fs.heater3.heat_duty[0] == -m.fs.cooler3.heat_duty[0]) + m.fs.duty_cons.add(m.fs.cooler1.heat_duty[0] <= 0) + m.fs.duty_cons.add(m.fs.cooler2.heat_duty[0] <= 0) + m.fs.duty_cons.add(m.fs.cooler3.heat_duty[0] <= 0) + m.fs.duty_cons.add(m.fs.cooler4.heat_duty[0] <= 0) + + approach_limit = 5 + m.fs.temp_approach = pe.ConstraintList() + m.fs.temp_approach.add(m.fs.cooler1.inlet.temperature[0] >= m.fs.heater1.outlet.temperature[0] + approach_limit) + m.fs.temp_approach.add(m.fs.cooler1.outlet.temperature[0] >= m.fs.heater1.inlet.temperature[0] + approach_limit) + m.fs.temp_approach.add(m.fs.cooler2.inlet.temperature[0] >= m.fs.heater2.outlet.temperature[0] + approach_limit) + m.fs.temp_approach.add(m.fs.cooler2.outlet.temperature[0] >= m.fs.heater2.inlet.temperature[0] + approach_limit) + m.fs.temp_approach.add(m.fs.cooler3.inlet.temperature[0] >= m.fs.heater3.outlet.temperature[0] + approach_limit) + m.fs.temp_approach.add(m.fs.cooler3.outlet.temperature[0] >= m.fs.heater3.inlet.temperature[0] + approach_limit) + + # specs + m.fs.heater1.inlet.temperature[0].fix(488) + m.fs.cooler1.inlet.temperature[0].fix(720) + m.fs.cooler2.inlet.temperature[0].fix(683) + m.fs.heater2.outlet.temperature[0].fix(663) + m.fs.heater3.inlet.temperature[0].fix(400) + m.fs.heater3.outlet.temperature[0].fix(550) + m.fs.cooler4.outlet.temperature[0].fix(450) + m.fs.cooler3_out_temp_spec = pe.Constraint(expr=m.fs.cooler3.outlet.temperature[0] <= 423) + + m.fs.heater1.inlet.flow_mol[0].fix(1) + m.fs.cooler1.inlet.flow_mol[0].fix(1) + m.fs.cooler2.inlet.flow_mol[0].fix(1) + # m.fs.heater3.inlet.flow_mol[0].fix(1) + m.fs.heater3.inlet.flow_mol[0].setlb(0.8) + m.fs.heater3.inlet.flow_mol[0].setub(1.2) + + m.fs.heater1.inlet.pressure[0].fix(101325) + m.fs.cooler1.inlet.pressure[0].fix(101325) + m.fs.cooler2.inlet.pressure[0].fix(101325) + m.fs.heater3.inlet.pressure[0].fix(101325) + + m.fs.heater1.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) + m.fs.cooler1.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) + m.fs.cooler2.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) + m.fs.heater3.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) + + m.fs.heater1.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) + m.fs.cooler1.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) + m.fs.cooler2.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) + m.fs.heater3.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) + + m.obj = pe.Objective(expr=-m.fs.cooler4.heat_duty[0]) + + pe.TransformationFactory('network.expand_arcs').apply_to(m) + scale_model(m) + + nominal_values = pe.ComponentMap() + # nominal_values[m.fs.cooler1.inlet.temperature[0]] = 7.20 + nominal_values[m.fs.heater1.inlet.temperature[0]] = 4.88 + nominal_values[m.fs.cooler2.inlet.temperature[0]] = 6.83 + # nominal_values[m.fs.heater3.inlet.temperature[0]] = 4.00 + nominal_values[m.fs.heater1.inlet.flow_mol[0]] = 1 + # nominal_values[m.fs.cooler1.inlet.flow_mol[0]] = 1 + nominal_values[m.fs.cooler2.inlet.flow_mol[0]] = 1 + # nominal_values[m.fs.heater3.inlet.flow_mol[0]] = 1 + + param_bounds = pe.ComponentMap() + # p = m.fs.cooler1.inlet.temperature[0] + # param_bounds[p] = (nominal_values[p] - 0.10, nominal_values[p] + 0.10) + p = m.fs.heater1.inlet.temperature[0] + param_bounds[p] = (nominal_values[p] - 0.10, nominal_values[p] + 0.10) + p = m.fs.cooler2.inlet.temperature[0] + param_bounds[p] = (nominal_values[p] - 0.10, nominal_values[p] + 0.10) + # p = m.fs.heater3.inlet.temperature[0] + # param_bounds[p] = (nominal_values[p] - 0.10, nominal_values[p] + 0.10) + p = m.fs.heater1.inlet.flow_mol[0] + param_bounds[p] = (nominal_values[p] - 0.2, nominal_values[p] + 0.2) + # p = m.fs.cooler1.inlet.flow_mol[0] + # param_bounds[p] = (nominal_values[p] - 0.2, nominal_values[p] + 0.2) + p = m.fs.cooler2.inlet.flow_mol[0] + param_bounds[p] = (nominal_values[p] - 0.2, nominal_values[p] + 0.2) + # p = m.fs.heater3.inlet.flow_mol[0] + # param_bounds[p] = (nominal_values[p] - 0.2, nominal_values[p] + 0.2) + + for p in nominal_values.keys(): + assert p.is_fixed() + p.unfix() + + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): + body_degree = polynomial_degree(c.body) + lower, body, upper = c.lower, c.body, c.upper + if body_degree == 5: + om, se = sympyify_expression(body) + se = se.simplify() + body = sympy2pyomo_expression(se, om) + c.set_value((c.lower, body, c.upper)) + print('simplified: ', c.body) + + for p in nominal_values.keys(): + assert not p.is_fixed() + p.fix() + + return m, nominal_values, param_bounds + + +def remove_inequalities(m): + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): + if not c.equality: + c.deactivate() + + for v in m.component_data_objects(pe.Var, descend_into=True): + v.setlb(None) + v.setub(None) + + +def initialize(m): + m.fs.heater1.heat_duty[0].fix(0.1000) + m.fs.heater1.initialize() + m.fs.heater1.heat_duty[0].unfix() + propagate_state(m.fs.s1) + m.fs.cooler1.heat_duty[0].fix(-m.fs.heater1.heat_duty[0]) + m.fs.cooler1.initialize() + m.fs.cooler1.heat_duty[0].unfix() + m.fs.heater2.inlet.temperature[0].fix() + m.fs.heater2.initialize() + m.fs.heater2.inlet.temperature[0].unfix() + m.fs.cooler2.heat_duty[0].fix(-m.fs.heater2.heat_duty[0].value) + m.fs.cooler2.initialize() + m.fs.cooler2.heat_duty[0].unfix() + m.fs.heater3.initialize() + propagate_state(m.fs.s3) + m.fs.cooler3.inlet.temperature[0].fix() + m.fs.cooler3.heat_duty[0].fix(-m.fs.heater3.heat_duty[0].value) + m.fs.cooler3.initialize() + m.fs.cooler3.heat_duty[0].unfix() + m.fs.cooler3.inlet.temperature[0].unfix() + propagate_state(m.fs.s2) + m.fs.cooler4.inlet.temperature[0].fix() + m.fs.cooler4.initialize() + m.fs.cooler4.inlet.temperature[0].unfix() + + +def get_var_bounds(m: _BlockData, param_bounds): + for p, (p_lb, p_ub) in param_bounds.items(): + p.unfix() + p.setlb(p_lb) + p.setub(p_ub) + bounds_manager = BoundsManager(m) + bounds_manager.save_bounds() + for b in m.block_data_objects(active=True, descend_into=True): + if isinstance(b, ControlVolumeBlockData): + b.properties_in[0].flow_mol.setlb(0) + b.properties_in[0].flow_mol.setub(5) + b.properties_out[0].flow_mol.setlb(0) + b.properties_out[0].flow_mol.setub(5) + b.properties_in[0].flow_mol_phase['Vap'].setlb(0) + b.properties_in[0].flow_mol_phase['Vap'].setub(5) + b.properties_out[0].flow_mol_phase['Vap'].setlb(0) + b.properties_out[0].flow_mol_phase['Vap'].setub(5) + b.properties_in[0].mole_frac_comp['benzene'].setlb(0) + b.properties_in[0].mole_frac_comp['benzene'].setub(1) + b.properties_in[0].mole_frac_comp['toluene'].setlb(0) + b.properties_in[0].mole_frac_comp['toluene'].setub(1) + b.properties_out[0].mole_frac_comp['benzene'].setlb(0) + b.properties_out[0].mole_frac_comp['benzene'].setub(1) + b.properties_out[0].mole_frac_comp['toluene'].setlb(0) + b.properties_out[0].mole_frac_comp['toluene'].setub(1) + b.properties_in[0].mole_frac_phase_comp['Vap','benzene'].setlb(0) + b.properties_in[0].mole_frac_phase_comp['Vap','benzene'].setub(1) + b.properties_in[0].mole_frac_phase_comp['Vap','toluene'].setlb(0) + b.properties_in[0].mole_frac_phase_comp['Vap','toluene'].setub(1) + b.properties_out[0].mole_frac_phase_comp['Vap','benzene'].setlb(0) + b.properties_out[0].mole_frac_phase_comp['Vap','benzene'].setub(1) + b.properties_out[0].mole_frac_phase_comp['Vap','toluene'].setlb(0) + b.properties_out[0].mole_frac_phase_comp['Vap','toluene'].setub(1) + b.properties_in[0].pressure.setlb(1.01325) + b.properties_in[0].pressure.setub(1.01325) + b.properties_out[0].pressure.setlb(1.01325) + b.properties_out[0].pressure.setub(1.01325) + b.properties_in[0].temperature.setlb(3.00) + b.properties_in[0].temperature.setub(10.00) + b.properties_out[0].temperature.setlb(3.00) + b.properties_out[0].temperature.setub(10.00) + cons_to_fbbt = list() + cons_to_fbbt.extend(b.properties_in[0].eq_enth_mol_phase_comp.values()) + cons_to_fbbt.extend(b.properties_out[0].eq_enth_mol_phase_comp.values()) + cons_to_fbbt.extend(b.properties_in[0].eq_enth_mol_phase.values()) + cons_to_fbbt.extend(b.properties_out[0].eq_enth_mol_phase.values()) + cons_to_fbbt.extend(b.enthalpy_balances.values()) + for c in cons_to_fbbt: + assert c.active + if c.active: + fbbt(c) + else: + c.activate() + fbbt(c) + c.deactivate() + res = pe.ComponentMap() + for v in m.component_data_objects(pe.Var, descend_into=True): + res[v] = (v.lb, v.ub) + bounds_manager.pop_bounds() + return res + + +def scale_model(m): + m.scaling_factor = pe.Suffix(direction=pe.Suffix.EXPORT) + for b in m.block_data_objects(active=True, descend_into=True): + if isinstance(b, ControlVolumeBlockData): + m.scaling_factor[b.heat[0]] = 1e-4 + m.scaling_factor[b.properties_in[0].pressure] = 1e-5 + m.scaling_factor[b.properties_out[0].pressure] = 1e-5 + m.scaling_factor[b.properties_in[0].temperature] = 1e-2 + m.scaling_factor[b.properties_out[0].temperature] = 1e-2 + m.scaling_factor[b.properties_in[0].enth_mol_phase['Vap']] = 1e-4 + m.scaling_factor[b.properties_out[0].enth_mol_phase['Vap']] = 1e-4 + m.scaling_factor[b.properties_in[0].enth_mol_phase_comp['Vap', 'benzene']] = 1e-4 + m.scaling_factor[b.properties_out[0].enth_mol_phase_comp['Vap', 'benzene']] = 1e-4 + m.scaling_factor[b.properties_in[0].enth_mol_phase_comp['Vap', 'toluene']] = 1e-4 + m.scaling_factor[b.properties_out[0].enth_mol_phase_comp['Vap', 'toluene']] = 1e-4 + for c in b.enthalpy_balances.values(): + m.scaling_factor[c] = 1e-4 + for c in b.pressure_balance.values(): + m.scaling_factor[c] = 1e-4 + for c in b.properties_in[0].eq_enth_mol_phase.values(): + m.scaling_factor[c] = 1e-4 + for c in b.properties_out[0].eq_enth_mol_phase.values(): + m.scaling_factor[c] = 1e-4 + for c in b.properties_in[0].eq_enth_mol_phase_comp.values(): + m.scaling_factor[c] = 1e-4 + for c in b.properties_out[0].eq_enth_mol_phase_comp.values(): + m.scaling_factor[c] = 1e-4 + for c in m.fs.duty_cons.values(): + m.scaling_factor[c] = 1e-4 + m.scaling_factor[m.fs.s1_expanded.pressure_equality[0]] = 1e-4 + m.scaling_factor[m.fs.s2_expanded.pressure_equality[0]] = 1e-4 + m.scaling_factor[m.fs.s3_expanded.pressure_equality[0]] = 1e-4 + m.scaling_factor[coramin.utils.get_objective(m)] = 1e-4 + + pe.TransformationFactory('core.scale_model').apply_to(m, rename=False) + + +def nominal_optimization(): + m, nominal_values, param_bounds = create_model() + initialize(m) + + log_infeasible_constraints(m, log_variables=False) + log_infeasible_bounds(m) + + opt = pe.SolverFactory('ipopt') + res = opt.solve(m, tee=True) + pe.assert_optimal_termination(res) + + m.fs.heater1.report() + m.fs.cooler1.report() + m.fs.heater2.report() + m.fs.cooler2.report() + m.fs.heater3.report() + m.fs.cooler3.report() + m.fs.cooler4.report() + + # for v in ComponentSet(m.component_data_objects(pe.Var, descend_into=True)): + # print(v, v.value) + + +def main(method): + m, nominal_values, param_bounds = create_model() + initialize(m) + var_bounds = get_var_bounds(m, param_bounds) + config = flexibility.FlexTestConfig() + config.feasibility_tol = 1e-6 + config.terminate_early = False + config.method = method + config.minlp_solver = pe.SolverFactory('scip') + config.minlp_solver.options['limits/time'] = 300 + config.sampling_config.solver = pe.SolverFactory('appsi_ipopt') + config.sampling_config.strategy = flexibility.SamplingStrategy.lhs + config.sampling_config.num_points = 100 + if method == flexibility.FlexTestMethod.linear_decision_rule: + config.decision_rule_config = flexibility.LinearDRConfig() + config.decision_rule_config.solver = pe.SolverFactory('appsi_gurobi') + elif method == flexibility.FlexTestMethod.relu_decision_rule: + config.decision_rule_config = flexibility.ReluDRConfig() + config.decision_rule_config.n_layers = 1 + config.decision_rule_config.n_nodes_per_layer = 10 + config.decision_rule_config.epochs = 10000 + config.decision_rule_config.batch_size = 50 + config.decision_rule_config.scale_inputs = True + config.decision_rule_config.scale_outputs = True + results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, param_bounds=param_bounds, + controls=[m.fs.cooler4.control_volume.heat[0], + m.fs.heater3.control_volume.properties_in[0].flow_mol], + valid_var_bounds=var_bounds, config=config) + print(results) + # results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), + # param_nominal_values=nominal_values, param_bounds=param_bounds, + # controls=[m.qc], valid_var_bounds=var_bounds, config=config) + # print(results) + + +if __name__ == '__main__': + print('\n\n********************Active Constraint**************************') + main(flexibility.FlexTestMethod.active_constraint) + # print('\n\n********************Linear Decision Rule**************************') + # main(flexibility.FlexTestMethod.linear_decision_rule) + # print('\n\n********************Vertex Enumeration**************************') + # main(flexibility.FlexTestMethod.vertex_enumeration) + # print('\n\n********************Sampling**************************') + # main(flexibility.FlexTestMethod.sampling) + # print('\n\n********************ReLU Decision rule**************************') + # main(flexibility.FlexTestMethod.relu_decision_rule) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py new file mode 100644 index 0000000000..9108e4f57c --- /dev/null +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -0,0 +1,113 @@ +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData +import flexibility +from typing import Tuple, Mapping +from pyomo.contrib.fbbt import interval + + +def create_model() -> Tuple[_BlockData, Mapping, Mapping]: + """ + This example is from + + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693. + """ + + print("""This example is based off of \n\n + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693.\n\n""") + + m = pe.ConcreteModel() + + m.uncertain_temps_set = pe.Set(initialize=[1, 3, 5, 8]) + m.uncertain_temps = pe.Param(m.uncertain_temps_set, mutable=True, initialize={1: 620, + 3: 388, + 5: 583, + 8: 313}) + nominal_values = pe.ComponentMap() + for p in m.uncertain_temps.values(): + nominal_values[p] = p.value + + param_bounds = pe.ComponentMap() + for p in m.uncertain_temps.values(): + param_bounds[p] = (p.value - 10.0, p.value + 10.0) + + m.variable_temps_set = pe.Set(initialize=[2, 4, 6, 7]) + m.variable_temps = pe.Var(m.variable_temps_set, bounds=(0, 1000)) + m.qc = pe.Var() + + m.balances = pe.Constraint([1, 2, 3, 4]) + m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * (m.variable_temps[4] - m.uncertain_temps[3]) + m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * (563 - m.variable_temps[4]) + m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * (393 - m.uncertain_temps[8]) + m.balances[4] = m.qc == 1.5 * (m.variable_temps[2] - 350) + + m.temp_approaches = pe.Constraint([1, 2, 3, 4]) + m.temp_approaches[1] = m.variable_temps[2] >= m.uncertain_temps[3] + m.temp_approaches[2] = m.variable_temps[6] >= m.variable_temps[4] + m.temp_approaches[3] = m.variable_temps[7] >= m.uncertain_temps[8] + m.temp_approaches[4] = m.variable_temps[6] >= 393 + + m.performance = pe.Constraint(expr=m.variable_temps[7] <= 323) + + #m.ineq1 = pe.Constraint(expr=-0.67*m.qc + m.uncertain_temps[3] - 350 <= 0) + #m.ineq2 = pe.Constraint(expr=-m.uncertain_temps[5] - 0.75*m.uncertain_temps[1] + 0.5*m.qc - m.uncertain_temps[3] + 1388.5 <= 0) + #m.ineq3 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] + 2044 <= 0) + #m.ineq4 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] - 2*m.uncertain_temps[8] + 2830 <= 0) + #m.ineq5 = pe.Constraint(expr=m.uncertain_temps[5] + 1.5*m.uncertain_temps[1] - m.qc + 2*m.uncertain_temps[3] + 3*m.uncertain_temps[8] - 3153 <= 0) + + return m, nominal_values, param_bounds + + +def get_var_bounds(m): + res = pe.ComponentMap() + for v in m.variable_temps.values(): + res[v] = (100, 1000) + res[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) + return res + + +def main(method): + m, nominal_values, param_bounds = create_model() + var_bounds = get_var_bounds(m) + config = flexibility.FlexTestConfig() + config.feasibility_tol = 1e-6 + config.terminate_early = False + config.method = method + config.minlp_solver = pe.SolverFactory('gurobi_direct') + config.sampling_config.solver = pe.SolverFactory('appsi_gurobi') + config.sampling_config.strategy = flexibility.SamplingStrategy.lhs + config.sampling_config.num_points = 200 + if method == flexibility.FlexTestMethod.linear_decision_rule: + config.decision_rule_config = flexibility.LinearDRConfig() + config.decision_rule_config.solver = pe.SolverFactory('appsi_gurobi') + elif method == flexibility.FlexTestMethod.relu_decision_rule: + config.decision_rule_config = flexibility.ReluDRConfig() + config.decision_rule_config.n_layers = 1 + config.decision_rule_config.n_nodes_per_layer = 10 + config.decision_rule_config.epochs = 3000 + config.decision_rule_config.batch_size = 50 + config.decision_rule_config.scale_inputs = True + config.decision_rule_config.scale_outputs = True + config.decision_rule_config.plot_history = True + # results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), + # param_nominal_values=nominal_values, param_bounds=param_bounds, + # controls=[m.qc], valid_var_bounds=var_bounds, config=config) + # print(results) + results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, param_bounds=param_bounds, + controls=[m.qc], valid_var_bounds=var_bounds, config=config) + print(results) + + +if __name__ == '__main__': + print('\n\n********************Active Constraint**************************') + main(flexibility.FlexTestMethod.active_constraint) + print('\n\n********************Linear Decision Rule**************************') + main(flexibility.FlexTestMethod.linear_decision_rule) + print('\n\n********************Vertex Enumeration**************************') + main(flexibility.FlexTestMethod.vertex_enumeration) + print('\n\n********************ReLU Decision rule**************************') + main(flexibility.FlexTestMethod.relu_decision_rule) diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py new file mode 100644 index 0000000000..9644c9c456 --- /dev/null +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -0,0 +1,90 @@ +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData +from pyomo.core.base.param import _ParamData +from pyomo.core.base.var import _GeneralVarData +import flexibility +from typing import Tuple, MutableMapping, Union + + +def create_model() -> Tuple[_BlockData, + MutableMapping[_ParamData, float], + MutableMapping[_ParamData, Tuple[float, float]]]: + """ + This example is from + + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693. + """ + + print("""This example is based off of \n\n + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693.\n\n""") + + m = pe.ConcreteModel() + + m.qc = pe.Var() + m.fh1 = pe.Param(mutable=True, initialize=1.4) + + m.f1 = pe.Constraint(expr=-25 + m.qc*((1/m.fh1) - 0.5) + 10/m.fh1 <= 0) + m.f2 = pe.Constraint(expr=-190 + 10/m.fh1 + m.qc/m.fh1 <= 0) + m.f3 = pe.Constraint(expr=-270 + 250/m.fh1 + m.qc/m.fh1 <= 0) + m.f4 = pe.Constraint(expr=260 - 250/m.fh1 - m.qc/m.fh1 <= 0) + + nominal_values = pe.ComponentMap() + nominal_values[m.fh1] = 1 + + param_bounds = pe.ComponentMap() + param_bounds[m.fh1] = (1, 1.8) + + return m, nominal_values, param_bounds + + +def get_var_bounds(m): + res = pe.ComponentMap() + res[m.qc] = (-1000, 1000) + return res + + +def main(method): + m, nominal_values, param_bounds = create_model() + var_bounds = get_var_bounds(m) + config = flexibility.FlexTestConfig() + config.feasibility_tol = 1e-6 + config.terminate_early = False + config.method = method + config.minlp_solver = pe.SolverFactory('scip') + config.sampling_config.solver = pe.SolverFactory('gurobi_direct') + config.sampling_config.strategy = flexibility.SamplingStrategy.lhs + config.sampling_config.num_points = 100 + if method == flexibility.FlexTestMethod.linear_decision_rule: + config.decision_rule_config = flexibility.LinearDRConfig() + config.decision_rule_config.solver = pe.SolverFactory('appsi_gurobi') + elif method == flexibility.FlexTestMethod.relu_decision_rule: + config.decision_rule_config = flexibility.ReluDRConfig() + config.decision_rule_config.n_layers = 1 + config.decision_rule_config.n_nodes_per_layer = 10 + config.decision_rule_config.epochs = 3000 + config.decision_rule_config.batch_size = 50 + config.decision_rule_config.scale_inputs = True + config.decision_rule_config.scale_outputs = True + # results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), + # param_nominal_values=nominal_values, param_bounds=param_bounds, + # controls=[m.qc], valid_var_bounds=var_bounds, config=config) + # print(results) + results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, param_bounds=param_bounds, + controls=[m.qc], valid_var_bounds=var_bounds, config=config) + print(results) + + +if __name__ == '__main__': + print('\n\n********************Active Constraint**************************') + main(flexibility.FlexTestMethod.active_constraint) + print('\n\n********************Linear Decision Rule**************************') + main(flexibility.FlexTestMethod.linear_decision_rule) + print('\n\n********************Vertex Enumeration**************************') + main(flexibility.FlexTestMethod.vertex_enumeration) + print('\n\n********************ReLU Decision rule**************************') + main(flexibility.FlexTestMethod.relu_decision_rule) diff --git a/idaes/apps/flexibility_analysis/examples/tests/__init__.py b/idaes/apps/flexibility_analysis/examples/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py new file mode 100644 index 0000000000..13f7fe8df1 --- /dev/null +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -0,0 +1,183 @@ +from pyomo.core.base.block import _BlockData +import pyomo.environ as pe +from typing import Sequence, Union, Mapping, MutableMapping, Optional +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData +from flexibility.sampling import SamplingStrategy +from .flextest import FlexTestConfig, solve_flextest, FlexTestMethod, FlexTestTermination, FlexTest +import math +import logging + + +logger = logging.getLogger(__name__) + + +def _get_param_bounds(orig_param_bounds, nominal_values, flex_index): + tmp_param_bounds = pe.ComponentMap() + for p, (p_lb, p_ub) in orig_param_bounds.items(): + p_nom = nominal_values[p] + tmp_param_bounds[p] = (p_nom - (p_nom - p_lb) * flex_index, p_nom + (p_ub - p_nom) * flex_index) + return tmp_param_bounds + + +def _add_table_row(log_level, outer_iter, flex_index_lower, flex_index_upper, fi_lb_max_viol, fi_ub_max_viol): + if flex_index_lower is None: + lb_str = str(flex_index_lower) + else: + lb_str = f'{flex_index_lower:12.3e}' + if flex_index_upper is None: + ub_str = str(flex_index_upper) + else: + ub_str = f'{flex_index_upper:12.3e}' + if fi_lb_max_viol is None: + lb_mv_str = str(fi_lb_max_viol) + else: + lb_mv_str = f'{fi_lb_max_viol:12.3e}' + if fi_ub_max_viol is None: + ub_mv_str = str(fi_ub_max_viol) + else: + ub_mv_str = f'{fi_ub_max_viol:12.3e}' + logger.log(log_level, f"{outer_iter:<12}{lb_str:<12}{ub_str:<12}{lb_mv_str:<15}{ub_mv_str:<15}") + + +def solve_flex_index(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], + in_place: bool = False, + cap_index_at_1: bool = True, + reconstruct_decision_rule: Optional[bool] = None, + config: Optional[FlexTestConfig] = None, + log_level: int = logging.INFO) -> float: + original_uncertain_params = uncertain_params + original_param_nominal_values = param_nominal_values + original_param_bounds = param_bounds + original_controls = controls + original_valid_var_bounds = valid_var_bounds + if not in_place: + m = m.clone() + uncertain_params = [m.find_component(i) for i in original_uncertain_params] + param_nominal_values = pe.ComponentMap((p, original_param_nominal_values[orig_p]) for orig_p, p in zip(original_uncertain_params, uncertain_params)) + param_bounds = pe.ComponentMap((p, original_param_bounds[orig_p]) for orig_p, p in zip(original_uncertain_params, uncertain_params)) + controls = [m.find_component(i) for i in original_controls] + valid_var_bounds = pe.ComponentMap((m.find_component(v), bnds) for v, bnds in original_valid_var_bounds.items()) + + if config.method == FlexTestMethod.linear_decision_rule and reconstruct_decision_rule is None: + reconstruct_decision_rule = True + elif config.method == FlexTestMethod.relu_decision_rule and reconstruct_decision_rule is None: + reconstruct_decision_rule = False + elif reconstruct_decision_rule is None: + reconstruct_decision_rule = False + + logger.log(log_level, f"{'Iter':<12}{'FI LB':<12}{'FI UB':<12}{'FI LB Max Viol':<15}{'FI UB Max Viol':<15}") + + fi_lb_max_viol = -math.inf + fi_ub_max_viol = math.inf + + outer_iter = 0 + _add_table_row(log_level, outer_iter, None, None, None, None) + + flex_index_lower = 0 + # make sure the nominal point is feasible + nominal_bounds = pe.ComponentMap() + for p, val in param_nominal_values.items(): + nominal_bounds[p] = (val, val) + nominal_config: FlexTestConfig = config() + nominal_config.method = FlexTestMethod.sampling + nominal_config.terminate_early = True + nominal_config.sampling_config.strategy = SamplingStrategy.grid + nominal_config.sampling_config.num_points = 1 + nominal_config.sampling_config.enable_progress_bar = False + nominal_res = solve_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, + param_bounds=nominal_bounds, controls=controls, valid_var_bounds=valid_var_bounds, + in_place=False, config=nominal_config) + if nominal_res.termination != FlexTestTermination.proven_feasible: + raise RuntimeError('Nominal point is infeasible') + + outer_iter += 1 + fi_lb_max_viol = nominal_res.max_constraint_violation + _add_table_row(log_level, outer_iter, flex_index_lower, None, fi_lb_max_viol, fi_ub_max_viol) + + flextest_config: FlexTestConfig = config() + flextest_config.terminate_early = True + flextest_config.sampling_config.enable_progress_bar = False + flex_index_upper = 1 + + if not cap_index_at_1: + # Find an upper bound on the flexibility index (i.e., a point where the flextest fails) + found_infeasible_point = False + for _iter in range(10): + tmp_param_bounds = _get_param_bounds(param_bounds, param_nominal_values, flex_index_upper) + upper_res = solve_flextest(m=m, uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=tmp_param_bounds, controls=controls, + valid_var_bounds=valid_var_bounds, + in_place=False, config=flextest_config) + outer_iter += 1 + if upper_res.termination == FlexTestTermination.found_infeasible_point: + fi_ub_max_viol = upper_res.max_constraint_violation + _add_table_row(log_level, outer_iter, + flex_index_lower, flex_index_upper, + fi_lb_max_viol, fi_ub_max_viol) + found_infeasible_point = True + break + elif upper_res.termination == FlexTestTermination.proven_feasible: + flex_index_lower = flex_index_upper + flex_index_upper *= 2 + fi_lb_max_viol = upper_res.max_constraint_violation + _add_table_row(log_level, outer_iter, flex_index_lower, None, fi_lb_max_viol, fi_ub_max_viol) + else: + raise RuntimeError('Unexpected termination from flexibility test') + + if not found_infeasible_point: + raise RuntimeError('Could not find an upper bound on the flexibility index') + + max_param_bounds = _get_param_bounds(param_bounds, param_nominal_values, flex_index_upper) + if cap_index_at_1: + if reconstruct_decision_rule: + res = solve_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, + param_bounds=max_param_bounds, controls=controls, + valid_var_bounds=valid_var_bounds, in_place=False, config=flextest_config) + else: + ft = FlexTest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, + max_param_bounds=max_param_bounds, controls=controls, valid_var_bounds=valid_var_bounds, + config=flextest_config) + res = ft.solve(max_param_bounds) + outer_iter += 1 + if res.termination == FlexTestTermination.proven_feasible: + flex_index_lower = 1 + fi_lb_max_viol = res.max_constraint_violation + fi_ub_max_viol = res.max_constraint_violation + elif res.termination == FlexTestTermination.found_infeasible_point: + fi_ub_max_viol = res.max_constraint_violation + else: + raise RuntimeError('Unexpected termination from flexibility test') + _add_table_row(log_level, outer_iter, flex_index_lower, flex_index_upper, fi_lb_max_viol, fi_ub_max_viol) + elif not reconstruct_decision_rule: + ft = FlexTest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, + max_param_bounds=max_param_bounds, controls=controls, valid_var_bounds=valid_var_bounds, + config=flextest_config) + + while (flex_index_upper - flex_index_lower) > 1e-3: + midpoint = 0.5 * (flex_index_lower + flex_index_upper) + tmp_param_bounds = _get_param_bounds(param_bounds, param_nominal_values, midpoint) + if reconstruct_decision_rule: + res = solve_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, + param_bounds=tmp_param_bounds, controls=controls, + valid_var_bounds=valid_var_bounds, in_place=False, config=flextest_config) + else: + res = ft.solve(param_bounds=tmp_param_bounds) + outer_iter += 1 + if res.termination == FlexTestTermination.proven_feasible: + flex_index_lower = midpoint + fi_lb_max_viol = res.max_constraint_violation + elif res.termination == FlexTestTermination.found_infeasible_point: + flex_index_upper = midpoint + fi_ub_max_viol = res.max_constraint_violation + else: + raise RuntimeError('Unexpected termination from flexibility test') + _add_table_row(log_level, outer_iter, flex_index_lower, flex_index_upper, fi_lb_max_viol, fi_ub_max_viol) + + return flex_index_lower diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py new file mode 100644 index 0000000000..dcd3f3e928 --- /dev/null +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -0,0 +1,493 @@ +import numpy as np +from .kkt import add_kkt_with_milp_complementarity_conditions +from pyomo.core.base.block import _BlockData +from coramin.utils import get_objective +import pyomo.environ as pe +from .var_utils import get_used_unfixed_variables, BoundsManager, _remove_var_bounds, _apply_var_bounds +from .indices import _VarIndex, _ConIndex +from .uncertain_params import _replace_uncertain_params +from .inner_problem import _build_inner_problem +from .scaling_check import check_bounds_and_scaling +import logging +from typing import Sequence, Union, Mapping, MutableMapping, Optional, Tuple +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData +from flexibility.decision_rules.linear_dr import construct_linear_decision_rule +from flexibility.decision_rules.relu_dr import construct_relu_decision_rule +from flexibility.sampling import SamplingStrategy, perform_sampling, SamplingConfig, _perform_sampling +import enum +from pyomo.common.config import ConfigDict, ConfigValue, PositiveFloat, InEnum, MarkImmutable +from flexibility.scaling_check import _get_longest_name + + +logger = logging.getLogger(__name__) + + +class FlexTestMethod(enum.Enum): + active_constraint = enum.auto() + linear_decision_rule = enum.auto() + relu_decision_rule = enum.auto() + custom_decision_rule = enum.auto() + vertex_enumeration = enum.auto() + sampling = enum.auto() + + +class FlexTestConfig(ConfigDict): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, + visibility=visibility) + self.feasibility_tol: float = self.declare('feasibility_tol', ConfigValue(domain=PositiveFloat, default=1e-6)) + self.terminate_early: bool = self.declare('terminate_early', ConfigValue(domain=bool, default=False)) + self.method: FlexTestMethod = self.declare('method', ConfigValue(domain=InEnum(FlexTestMethod), + default=FlexTestMethod.active_constraint)) + self.minlp_solver = self.declare('minlp_solver', ConfigValue(default=pe.SolverFactory('scip'))) + self.sampling_config: SamplingConfig = self.declare('sampling_config', SamplingConfig()) + self.decision_rule_config = self.declare('decision_rule_config', ConfigValue(default=None)) + + +class FlexTestTermination(enum.Enum): + found_infeasible_point = enum.auto() + proven_feasible = enum.auto() + uncertain = enum.auto() + + +class FlexTestResults(object): + def __init__(self): + self.termination = FlexTestTermination.uncertain + self.max_constraint_violation: Optional[float] = None + self.unc_param_values_at_max_violation: \ + Optional[MutableMapping[Union[_GeneralVarData, _ParamData], float]] = None + + def __str__(self): + s = f'Termination: {self.termination}\n' + s += f'Maximum constraint violation: {self.max_constraint_violation}\n' + if self.unc_param_values_at_max_violation is not None: + s += f'Uncertain parameter values at maximum constraint violation: \n' + longest_param_name = _get_longest_name(self.unc_param_values_at_max_violation.keys()) + s += f'{"Param":<{longest_param_name + 5}}{"Value":>12}\n' + for k, v in self.unc_param_values_at_max_violation.items(): + s += f'{str(k):<{longest_param_name + 5}}{v:>12.2e}\n' + return s + + +def _get_dof(m: _BlockData): + n_cons = len(set(i for i in m.component_data_objects(pe.Constraint, active=True, descend_into=True) if i.equality)) + n_vars = len(get_used_unfixed_variables(m)) + return n_vars - n_cons + + +dr_construction_map = dict() +dr_construction_map[FlexTestMethod.linear_decision_rule] = construct_linear_decision_rule +dr_construction_map[FlexTestMethod.relu_decision_rule] = construct_relu_decision_rule + + +def build_flextest_with_dr(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: FlexTestConfig): + # enforce_equalities must be true for this method, or the resulting + # problem will be unbounded; the key is degrees of freedom + + # perform sampling + tmp = perform_sampling(m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + in_place=False, + config=config.sampling_config) + + _sample_points, max_violation_values, control_values = tmp + + # replace uncertain parameters with variables + _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) + for v in m.unc_param_vars.values(): + valid_var_bounds[v] = (v.lb, v.ub) + v.fix() # these should be fixed before we check the degrees of freedom + + if _get_dof(m) != len(controls): + raise ValueError('The number of controls must match the number of degrees of freedom') + + # check the scaling of the model + # this has to be done with valid_var_bounds (original bounds removed) to ensure we have + # an entry in valid_var_bounds for every variable + for v in m.unc_param_vars.values(): + v.unfix() + bounds_manager = BoundsManager(m) + bounds_manager.save_bounds() + _remove_var_bounds(m) + _apply_var_bounds(valid_var_bounds) + check_bounds_and_scaling(m) + bounds_manager.pop_bounds() + + # construct the decision rule + # the keys of sample_points need to be the new variables that replaced the uncertain parameters + sample_points: MutableMapping[_GeneralVarData, Sequence[float]] = pe.ComponentMap() + for p in uncertain_params: + ndx = _VarIndex(p, None) + p_var = m.unc_param_vars[ndx] + sample_points[p_var] = _sample_points[p] + + dr = dr_construction_map[config.method](input_vals=sample_points, output_vals=control_values, + config=config.decision_rule_config) + + _build_inner_problem(m=m, enforce_equalities=True, unique_constraint_violations=True, + valid_var_bounds=valid_var_bounds) + _apply_var_bounds(valid_var_bounds) + m.decision_rule = dr + + obj = get_objective(m) + obj.deactivate() + + m.max_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation, sense=pe.maximize) + + +def build_active_constraint_flextest(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + default_M=None): + enforce_equalities = False + _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) + for v in m.unc_param_vars.values(): + valid_var_bounds[v] = (v.lb, v.ub) + + bounds_manager = BoundsManager(m) + bounds_manager.save_bounds() + _remove_var_bounds(m) + _apply_var_bounds(valid_var_bounds) + check_bounds_and_scaling(m) + bounds_manager.pop_bounds() + + orig_equality_cons = [c for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True) if c.equality] + + _build_inner_problem(m=m, enforce_equalities=enforce_equalities, unique_constraint_violations=False, + valid_var_bounds=valid_var_bounds) + + for v in m.unc_param_vars.values(): + v.fix() + n_dof = _get_dof(m) + for v in m.unc_param_vars.values(): + v.unfix() + + add_kkt_with_milp_complementarity_conditions(m=m, + uncertain_params=list(m.unc_param_vars.values()), + valid_var_bounds=valid_var_bounds, + default_M=default_M) + + m.equality_cuts = pe.ConstraintList() + max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] + for c in orig_equality_cons: + key1 = _ConIndex(c, 'lb') + key2 = _ConIndex(m.ineq_violation_cons[key1], 'ub') + y1 = m.active_indicator[key2] + key1 = _ConIndex(c, 'ub') + key2 = _ConIndex(m.ineq_violation_cons[key1], 'ub') + y2 = m.active_indicator[key2] + m.equality_cuts.add(m.max_constraint_violation <= (1 - y1*y2) * max_viol_ub) + m.equality_cuts.add(m.max_constraint_violation >= (1 - y1*y2) * max_viol_lb) + + m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) + + m.max_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation, sense=pe.maximize) + + +def _solve_flextest_active_constraint(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None) -> FlexTestResults: + build_active_constraint_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, + param_bounds=param_bounds, valid_var_bounds=valid_var_bounds) + opt = config.minlp_solver + res = opt.solve(m) + pe.assert_optimal_termination(res) + + results = FlexTestResults() + results.max_constraint_violation = m.max_constraint_violation.value + if results.max_constraint_violation > config.feasibility_tol: + results.termination = FlexTestTermination.found_infeasible_point + else: + results.termination = FlexTestTermination.proven_feasible + results.unc_param_values_at_max_violation = pe.ComponentMap() + for key, v in m.unc_param_vars.items(): + results.unc_param_values_at_max_violation[key.var] = v.value + return results + + +def _solve_flextest_decision_rule(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None) -> FlexTestResults: + build_flextest_with_dr(m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + config=config) + opt = config.minlp_solver + res = opt.solve(m) + pe.assert_optimal_termination(res) + + results = FlexTestResults() + results.max_constraint_violation = m.max_constraint_violation.value + if results.max_constraint_violation > config.feasibility_tol: + results.termination = FlexTestTermination.found_infeasible_point + else: + results.termination = FlexTestTermination.proven_feasible + results.unc_param_values_at_max_violation = pe.ComponentMap() + for key, v in m.unc_param_vars.items(): + results.unc_param_values_at_max_violation[key.var] = v.value + return results + + +def _solve_flextest_sampling(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None) -> FlexTestResults: + tmp = perform_sampling(m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + in_place=True, + config=config.sampling_config) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = int(np.argmax(max_violation_values)) + + results = FlexTestResults() + results.max_constraint_violation = max_violation_values[max_viol_ndx] + if results.max_constraint_violation > config.feasibility_tol: + results.termination = FlexTestTermination.found_infeasible_point + else: + results.termination = FlexTestTermination.proven_feasible + results.unc_param_values_at_max_violation = pe.ComponentMap() + for key, vals in sample_points.items(): + results.unc_param_values_at_max_violation[key] = vals[max_viol_ndx] + return results + + +def _solve_flextest_vertex_enumeration(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None) -> FlexTestResults: + config: FlexTestConfig = config() + config.sampling_config.num_points = 2 + config.sampling_config.strategy = SamplingStrategy.grid + tmp = perform_sampling(m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + in_place=True, + config=config.sampling_config) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = int(np.argmax(max_violation_values)) + + results = FlexTestResults() + results.max_constraint_violation = max_violation_values[max_viol_ndx] + if results.max_constraint_violation > config.feasibility_tol: + results.termination = FlexTestTermination.found_infeasible_point + else: + results.termination = FlexTestTermination.proven_feasible + results.unc_param_values_at_max_violation = pe.ComponentMap() + for key, vals in sample_points.items(): + results.unc_param_values_at_max_violation[key] = vals[max_viol_ndx] + return results + + +_flextest_map = dict() +_flextest_map[FlexTestMethod.active_constraint] = _solve_flextest_active_constraint +_flextest_map[FlexTestMethod.sampling] = _solve_flextest_sampling +_flextest_map[FlexTestMethod.vertex_enumeration] = _solve_flextest_vertex_enumeration +_flextest_map[FlexTestMethod.linear_decision_rule] = _solve_flextest_decision_rule +_flextest_map[FlexTestMethod.relu_decision_rule] = _solve_flextest_decision_rule + + +def solve_flextest(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + in_place: bool = False, + config: Optional[FlexTestConfig] = None) -> FlexTestResults: + if config is None: + config = FlexTestConfig() + + original_model = m + original_uncertain_params = uncertain_params + original_param_nominal_values = param_nominal_values + original_param_bounds = param_bounds + original_controls = controls + original_valid_var_bounds = valid_var_bounds + if not in_place: + m = m.clone() + uncertain_params = [m.find_component(i) for i in original_uncertain_params] + param_nominal_values = pe.ComponentMap((p, original_param_nominal_values[orig_p]) + for orig_p, p in zip(original_uncertain_params, uncertain_params)) + param_bounds = pe.ComponentMap((p, original_param_bounds[orig_p]) + for orig_p, p in zip(original_uncertain_params, uncertain_params)) + controls = [m.find_component(i) for i in original_controls] + valid_var_bounds = pe.ComponentMap((m.find_component(v), bnds) for v, bnds in original_valid_var_bounds.items()) + results = _flextest_map[config.method](m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + config=config) + if not in_place: + unc_param_values = pe.ComponentMap() + for v, val in results.unc_param_values_at_max_violation.items(): + unc_param_values[original_model.find_component(v)] = val + results.unc_param_values_at_max_violation = unc_param_values + return results + + +class FlexTest(object): + def __init__(self, + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + max_param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], + config: Optional[FlexTestConfig] = None): + if config is None: + self.config: FlexTestConfig = FlexTestConfig() + else: + self.config: FlexTestConfig = config() + MarkImmutable(self.config.get('method')) + if self.config.method == FlexTestMethod.vertex_enumeration: + self.config.sampling_config.strategy = SamplingStrategy.grid + self.config.sampling_config.num_points = 2 + MarkImmutable(self.config.sampling_config.get('strategy')) + MarkImmutable(self.config.sampling_config.get('num_points')) + + self._original_model = m + self._model = m.clone() + m = self._model + self._uncertain_params = [m.find_component(i) for i in uncertain_params] + self._param_nominal_values = pe.ComponentMap((p, param_nominal_values[orig_p]) + for orig_p, p in zip(uncertain_params, self._uncertain_params)) + self._max_param_bounds = pe.ComponentMap((p, max_param_bounds[orig_p]) + for orig_p, p in zip(uncertain_params, self._uncertain_params)) + self._controls = [m.find_component(i) for i in controls] + self._valid_var_bounds = pe.ComponentMap((m.find_component(v), bnds) for v, bnds in valid_var_bounds.items()) + + self._orig_param_clone_param_map = pe.ComponentMap((i, j) for i, j in zip(uncertain_params, + self._uncertain_params)) + self._clone_param_orig_param_map = pe.ComponentMap((i, j) for i, j in zip(self._uncertain_params, + uncertain_params)) + + assert self.config.method in FlexTestMethod + if self.config.method == FlexTestMethod.active_constraint: + self._build_active_constraint_model() + elif self.config.method == FlexTestMethod.linear_decision_rule: + self._build_flextest_with_dr() + elif self.config.method == FlexTestMethod.relu_decision_rule: + self._build_flextest_with_dr() + elif self.config.method in {FlexTestMethod.sampling, FlexTestMethod.vertex_enumeration}: + self._build_sampling() + else: + raise ValueError(f'Unrecognized method: {self.config.method}') + + def _set_param_bounds(self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]]): + for orig_p, clone_p in self._orig_param_clone_param_map.items(): + p_lb, p_ub = param_bounds[orig_p] + ndx = _VarIndex(clone_p, None) + p_var = self._model.unc_param_vars[ndx] + p_var.setlb(p_lb) + p_var.setub(p_ub) + + def _build_active_constraint_model(self): + build_active_constraint_flextest(m=self._model, + uncertain_params=self._uncertain_params, + param_nominal_values=self._param_nominal_values, + param_bounds=self._max_param_bounds, + valid_var_bounds=self._valid_var_bounds) + + def _build_flextest_with_dr(self): + build_flextest_with_dr(m=self._model, + uncertain_params=self._uncertain_params, + param_nominal_values=self._param_nominal_values, + param_bounds=self._max_param_bounds, + controls=self._controls, + valid_var_bounds=self._valid_var_bounds, + config=self.config) + + def _build_sampling(self): + _replace_uncertain_params(m=self._model, + uncertain_params=self._uncertain_params, + param_nominal_values=self._param_nominal_values, + param_bounds=self._max_param_bounds) + _build_inner_problem(m=self._model, + enforce_equalities=True, + unique_constraint_violations=False, + valid_var_bounds=None) + + def _solve_maximization(self, param_bounds: Mapping[Union[_GeneralVarData, + _ParamData], Sequence[float]]) -> FlexTestResults: + self._set_param_bounds(param_bounds=param_bounds) + + opt = self.config.minlp_solver + res = opt.solve(self._model) + pe.assert_optimal_termination(res) + + results = FlexTestResults() + results.max_constraint_violation = self._model.max_constraint_violation.value + if results.max_constraint_violation > self.config.feasibility_tol: + results.termination = FlexTestTermination.found_infeasible_point + else: + results.termination = FlexTestTermination.proven_feasible + results.unc_param_values_at_max_violation = pe.ComponentMap() + for key, v in self._model.unc_param_vars.items(): + results.unc_param_values_at_max_violation[self._clone_param_orig_param_map[key.var]] = v.value + return results + + def _solve_sampling(self, param_bounds: Mapping[Union[_GeneralVarData, + _ParamData], Sequence[float]]) -> FlexTestResults: + self._set_param_bounds(param_bounds=param_bounds) + tmp = _perform_sampling(m=self._model, uncertain_params=self._uncertain_params, + controls=self._controls, config=self.config.sampling_config) + sample_points, max_violation_values, control_values = tmp + sample_points = pe.ComponentMap((self._clone_param_orig_param_map[p], vals) + for p, vals in sample_points.items()) + + results = FlexTestResults() + max_viol_ndx = int(np.argmax(max_violation_values)) + results.max_constraint_violation = max_violation_values[max_viol_ndx] + if results.max_constraint_violation > self.config.feasibility_tol: + results.termination = FlexTestTermination.found_infeasible_point + else: + results.termination = FlexTestTermination.proven_feasible + results.unc_param_values_at_max_violation = pe.ComponentMap() + for key, vals in sample_points.items(): + results.unc_param_values_at_max_violation[key] = vals[max_viol_ndx] + return results + + def solve(self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]]) -> FlexTestResults: + if self.config.method in {FlexTestMethod.active_constraint, FlexTestMethod.linear_decision_rule, + FlexTestMethod.relu_decision_rule}: + return self._solve_maximization(param_bounds) + elif self.config.method in {FlexTestMethod.sampling, FlexTestMethod.vertex_enumeration}: + return self._solve_sampling(param_bounds=param_bounds) + else: + raise ValueError(f'Unrecognized method: {self.config.method}') diff --git a/idaes/apps/flexibility_analysis/indices.py b/idaes/apps/flexibility_analysis/indices.py new file mode 100644 index 0000000000..d359ca24cc --- /dev/null +++ b/idaes/apps/flexibility_analysis/indices.py @@ -0,0 +1,62 @@ + + +class _ConIndex(object): + def __init__(self, con, bound): + self._con = con + self._bound = bound + + @property + def con(self): + return self._con + + @property + def bound(self): + return self._bound + + def __repr__(self): + if self.bound is None: + return str(self.con) + else: + return str((str(self.con), str(self.bound))) + + def __str__(self): + return repr(self) + + def __eq__(self, other): + if isinstance(other, _ConIndex): + return self.con is other.con and self.bound is other.bound + return False + + def __hash__(self): + return hash((self.con, self.bound)) + + +class _VarIndex(object): + def __init__(self, var, bound): + self._var = var + self._bound = bound + + @property + def var(self): + return self._var + + @property + def bound(self): + return self._bound + + def __repr__(self): + if self.bound is None: + return str(self.var) + else: + return str((str(self.var), str(self.bound))) + + def __str__(self): + return repr(self) + + def __eq__(self, other): + if isinstance(other, _VarIndex): + return self.var is other.var and self.bound is other.bound + return False + + def __hash__(self): + return hash((id(self.var), self.bound)) diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py new file mode 100644 index 0000000000..6603b18039 --- /dev/null +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -0,0 +1,212 @@ +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData +from pyomo.core.base.var import _GeneralVarData, ScalarVar +from coramin.utils import get_objective +from .var_utils import get_all_unfixed_variables, BoundsManager, _apply_var_bounds +from .indices import _ConIndex, _VarIndex +from typing import MutableMapping, Tuple, Optional, Mapping, Sequence +import math +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd + + +def _get_bounds_on_max_constraint_violation(m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]]): + bounds_manager = BoundsManager(m) + bounds_manager.save_bounds() + + _apply_var_bounds(valid_var_bounds) + + min_constraint_violation = math.inf + max_constraint_violation = -math.inf + m.max_constraint_violation.fix(0) + for c in m.ineq_violation_cons.values(): + exprs = list() + assert c.lower is None + assert c.upper is not None + e = c.body - c.upper + ders = reverse_sd(e) + assert ders[m.max_constraint_violation] == -1 + _lb, _ub = compute_bounds_on_expr(e) + if _lb < min_constraint_violation: + min_constraint_violation = _lb + if _ub > max_constraint_violation: + max_constraint_violation = _ub + + bounds_manager.pop_bounds() + m.max_constraint_violation.unfix() + + return min_constraint_violation, max_constraint_violation + + +def _get_constraint_violation_bounds(m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]]) -> MutableMapping[_GeneralVarData, Tuple[float, float]]: + bounds_manager = BoundsManager(m) + bounds_manager.save_bounds() + + _apply_var_bounds(valid_var_bounds) + + constraint_violation_bounds = pe.ComponentMap() + for key in m.ineq_violation_set: + v = m.constraint_violation[key] + v.fix(0) + + c = m.ineq_violation_cons[key] + assert c.equality + e = c.body - c.upper + ders = reverse_sd(e) + assert ders[v] == -1 + _lb, _ub = compute_bounds_on_expr(e) + constraint_violation_bounds[v] = (_lb, _ub) + v.unfix() + + bounds_manager.pop_bounds() + + return constraint_violation_bounds + + +def _build_inner_problem(m: _BlockData, + enforce_equalities: bool, + unique_constraint_violations: bool, + valid_var_bounds: Optional[MutableMapping[_GeneralVarData, Tuple[float, float]]]): + """ + If enfoce equalities is True and unique_constraint_violations is False, then this function converts + + min f(x) + s.t. + c(x) = 0 + g(x) <= 0 + + to + + min u + s.t. + c(x) = 0 + g(x) <= u + + If enfoce equalities is False and unique_constraint_violations is False, then this function converts + + min f(x) + s.t. + c(x) = 0 + g(x) <= 0 + + to + + min u + s.t. + c(x) <= u + -c(x) <= u + g(x) <= u + + If enfoce equalities is True and unique_constraint_violations is True, then this function converts + + min f(x) + s.t. + c(x) = 0 + g(x) <= 0 + + to + + min u + s.t. + c(x) = 0 + g_i(x) == u_i + u = sum(u_i * y_i) + sum(y_i) = 1 + Of course, the nonlinear constraint u = sum(u_i * y_i) gets reformulated. + + This function will also modify valid_var_bounds to include any new variables + """ + obj = get_objective(m) + if obj is not None: + obj.deactivate() + + for v in m.unc_param_vars.values(): + v.fix() + original_vars = list(get_all_unfixed_variables(m)) + for v in m.unc_param_vars.values(): + v.unfix() + + m.max_constraint_violation = ScalarVar() + m.min_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation) + + m.ineq_violation_set = pe.Set() + m.ineq_violation_cons = pe.Constraint(m.ineq_violation_set) + + if unique_constraint_violations: + m.constraint_violation = pe.Var(m.ineq_violation_set) + + for c in list(m.component_data_objects(pe.Constraint, descend_into=True, active=True)): + if c.equality and enforce_equalities: + continue + if c.lower is not None: + key = _ConIndex(c, 'lb') + m.ineq_violation_set.add(key) + if unique_constraint_violations: + m.ineq_violation_cons[key] = (c.lower - c.body - m.constraint_violation[key], 0) + else: + m.ineq_violation_cons[key] = (None, c.lower - c.body - m.max_constraint_violation, 0) + if c.upper is not None: + key = _ConIndex(c, 'ub') + m.ineq_violation_set.add(key) + if unique_constraint_violations: + m.ineq_violation_cons[key] = (c.body - c.upper - m.constraint_violation[key], 0) + else: + m.ineq_violation_cons[key] = (None, c.body - c.upper - m.max_constraint_violation, 0) + + for v in original_vars: + if v.is_integer(): + raise ValueError('Original problem must be continuous') + if v.lb is not None: + key = _VarIndex(v, 'lb') + m.ineq_violation_set.add(key) + if unique_constraint_violations: + m.ineq_violation_cons[key] = (v.lb - v - m.constraint_violation[key], 0) + else: + m.ineq_violation_cons[key] = (None, v.lb - v - m.max_constraint_violation, 0) + if v.ub is not None: + key = _VarIndex(v, 'ub') + m.ineq_violation_set.add(key) + if unique_constraint_violations: + m.ineq_violation_cons[key] = (v - v.ub - m.constraint_violation[key], 0) + else: + m.ineq_violation_cons[key] = (None, v - v.ub - m.max_constraint_violation, 0) + + for key in m.ineq_violation_set: + if isinstance(key, _ConIndex): + key.con.deactivate() + else: + key.var.setlb(None) + key.var.setub(None) + key.var.domain = pe.Reals + + if unique_constraint_violations: + # max_constraint_violation = sum(constraint_violation[i] * y[i]) + # sum(y[i]) == 1 + # reformulate as + # max_constraint_violation = sum(u_hat[i]) + # u_hat[i] = constraint_violation[i] * y[i] + # and use mccormick for the last constraint + + m.max_violation_selector = pe.Var(m.ineq_violation_set, domain=pe.Binary) # y[i] + m.one_max_violation = pe.Constraint(expr=sum(m.max_violation_selector.values()) == 1) + m.u_hat = pe.Var(m.ineq_violation_set) + m.max_violation_sum = pe.Constraint(expr=m.max_constraint_violation == sum(m.u_hat.values())) + constraint_violation_bounds = _get_constraint_violation_bounds(m, valid_var_bounds) + m.u_hat_cons = pe.ConstraintList() + for key in m.ineq_violation_set: + violation_var = m.constraint_violation[key] + viol_lb, viol_ub = constraint_violation_bounds[violation_var] + y_i = m.max_violation_selector[key] + m.u_hat_cons.add(m.u_hat[key] <= viol_ub * y_i) + m.u_hat_cons.add(m.u_hat[key] >= viol_lb * y_i) + m.u_hat_cons.add(m.u_hat[key] <= violation_var + viol_lb * y_i - viol_lb) + m.u_hat_cons.add(m.u_hat[key] >= viol_ub * y_i + violation_var - viol_ub) + valid_var_bounds.update(constraint_violation_bounds) + valid_var_bounds[m.max_constraint_violation] = (min(i[0] for i in constraint_violation_bounds.values()), max(i[1] for i in constraint_violation_bounds.values())) + for key in m.ineq_violation_set: + valid_var_bounds[m.max_violation_selector[key]] = (0, 1) + valid_var_bounds[m.u_hat[key]] = (min(0.0, constraint_violation_bounds[m.constraint_violation[key]][0]), + max(0.0, constraint_violation_bounds[m.constraint_violation[key]][1])) + else: + if valid_var_bounds is not None: + valid_var_bounds[m.max_constraint_violation] = _get_bounds_on_max_constraint_violation(m=m, valid_var_bounds=valid_var_bounds) diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py new file mode 100644 index 0000000000..a3e7dc7467 --- /dev/null +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -0,0 +1,169 @@ +import pyomo.environ as pe +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from coramin.utils import get_objective +from pyomo.core.base.block import _BlockData +from pyomo.contrib.fbbt.fbbt import fbbt +from .var_utils import get_used_unfixed_variables, _remove_var_bounds, _apply_var_bounds, BoundsManager +from typing import Sequence, Mapping, Optional +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression +from .indices import _VarIndex, _ConIndex + + +def _simplify(expr): + cm, sympy_expr = sympyify_expression(expr) + sympy_expr = sympy_expr.simplify() + pyomo_expr = sympy2pyomo_expression(sympy_expr, cm) + return pyomo_expr + + +def _add_grad_lag_constraints(m: _BlockData) -> _BlockData: + primal_vars = get_used_unfixed_variables(m) + + m.duals_eq_set = pe.Set() + m.duals_eq = pe.Var(m.duals_eq_set) + + m.duals_ineq_set = pe.Set() + m.duals_ineq = pe.Var(m.duals_ineq_set, bounds=(0, None)) + + obj = get_objective(m) + assert obj.sense == pe.minimize + if obj is None: + lagrangian = 0 + else: + lagrangian = obj.expr + + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): + if c.equality: + key = _ConIndex(c, 'eq') + m.duals_eq_set.add(key) + lagrangian += m.duals_eq[key] * (c.body - c.upper) + else: + if c.upper is not None: + key = _ConIndex(c, 'ub') + m.duals_ineq_set.add(key) + lagrangian += m.duals_ineq[key] * (c.body - c.upper) + if c.lower is not None: + key = _ConIndex(c, 'lb') + m.duals_ineq_set.add(key) + lagrangian += m.duals_ineq[key] * (c.lower - c.body) + + for v in primal_vars: + assert v.is_continuous() + if v.ub is not None: + key = _VarIndex(v, 'ub') + m.duals_ineq_set.add(key) + lagrangian += m.duals_ineq[key] * (v - v.ub) + if v.lb is not None: + key = _VarIndex(v, 'lb') + m.duals_ineq_set.add(key) + lagrangian += m.duals_ineq[key] * (v.lb - v) + + grad_lag = reverse_sd(lagrangian) + + m.grad_lag_set = pe.Set() + m.grad_lag = pe.Constraint(m.grad_lag_set) + for v in primal_vars: + if v in grad_lag and (type(grad_lag[v]) != float or grad_lag[v] != 0): + key = _VarIndex(v, None) + m.grad_lag_set.add(key) + m.grad_lag[key] = _simplify(grad_lag[v]) == 0 + + return m + + +def _introduce_inequality_slacks(m) -> _BlockData: + m.slacks = pe.Var(m.duals_ineq_set, bounds=(0, None)) + m.ineq_cons_with_slacks = pe.Constraint(m.duals_ineq_set) + + for key in m.duals_ineq_set: + s = m.slacks[key] + bnd = key.bound + + if isinstance(key, _ConIndex): + e = key.con.body + lb = key.con.lower + ub = key.con.upper + else: + assert isinstance(key, _VarIndex) + e = key.var + lb = e.lb + ub = e.ub + + if bnd == 'ub': + m.ineq_cons_with_slacks[key] = s + e - ub == 0 + else: + assert bnd == 'lb' + m.ineq_cons_with_slacks[key] = s + lb - e == 0 + + for key in m.duals_ineq_set: + if isinstance(key, _ConIndex): + key.con.deactivate() + else: + key.var.setlb(None) + key.var.setub(None) + key.var.domain = pe.Reals + + return m + + +def _do_fbbt(m, uncertain_params): + p_bounds = pe.ComponentMap() + for p in uncertain_params: + p.unfix() + p_bounds[p] = (p.lb, p.ub) + fbbt(m) + for p in uncertain_params: + if p.lb > p_bounds[p][0] + 1e-6 or p.ub < p_bounds[p][1] - 1e-6: + raise RuntimeError('The bounds provided in valid_var_bounds were proven to ' + 'be invalid for some values of the uncertain parameters.') + p.fix() + + +def add_kkt_with_milp_complementarity_conditions(m: _BlockData, + uncertain_params: Sequence[_GeneralVarData], + valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]], + default_M=None) -> _BlockData: + for v in uncertain_params: + v.fix() + + _add_grad_lag_constraints(m) + obj = get_objective(m) + obj.deactivate() + + _apply_var_bounds(valid_var_bounds) + _do_fbbt(m, uncertain_params) + + _introduce_inequality_slacks(m) + + _do_fbbt(m, uncertain_params) + + m.active_indicator = pe.Var(m.duals_ineq_set, domain=pe.Binary) + m.dual_ineq_0_if_not_active = pe.Constraint(m.duals_ineq_set) + m.slack_0_if_active = pe.Constraint(m.duals_ineq_set) + m.dual_M = pe.Param(m.duals_ineq_set, mutable=True) + m.slack_M = pe.Param(m.duals_ineq_set, mutable=True) + for key in m.duals_ineq_set: + if m.duals_ineq[key].ub is None: + if default_M is None: + raise RuntimeError(f'could not compute upper bound on multiplier for inequality {key}.') + else: + dual_M = default_M + else: + dual_M = m.duals_ineq[key].ub + if m.slacks[key].ub is None: + if default_M is None: + raise RuntimeError(f'could not compute upper bound on slack for inequality {key}') + else: + slack_M = default_M + else: + slack_M = m.slacks[key].ub + m.dual_M[key].value = dual_M + m.slack_M[key].value = slack_M + m.dual_ineq_0_if_not_active[key] = m.duals_ineq[key] <= m.active_indicator[key] * m.dual_M[key] + m.slack_0_if_active[key] = m.slacks[key] <= (1 - m.active_indicator[key]) * m.slack_M[key] + + for v in uncertain_params: + v.unfix() + + return m diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py new file mode 100644 index 0000000000..06edabbc62 --- /dev/null +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -0,0 +1,163 @@ +from pyomo.core.base.block import _BlockData +import pyomo.environ as pe +import math +import numpy as np +import itertools +from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData +from pyomo.contrib.appsi.base import PersistentSolver +from tqdm import tqdm +from .uncertain_params import _replace_uncertain_params +from .inner_problem import _build_inner_problem +import enum +from idaes.surrogate.pysmo.sampling import LatinHypercubeSampling +from .indices import _VarIndex +from pyomo.common.config import ConfigDict, ConfigValue, InEnum + + +class SamplingStrategy(enum.Enum): + grid = enum.auto() + lhs = enum.auto() + + +def _grid_sampling(uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int): + uncertain_params_values = pe.ComponentMap() + for p in uncertain_params: + uncertain_params_values[p] = list(set([float(i) for i in np.linspace(p.lb, p.ub, num_points)])) + uncertain_params_values[p].sort() + + sample_points = pe.ComponentMap() + for p in uncertain_params: + sample_points[p] = list() + + n_samples = 0 + for sample in itertools.product(*uncertain_params_values.values()): + for p, v in zip(uncertain_params, sample): + sample_points[p].append(float(v)) + n_samples += 1 + + return n_samples, sample_points + + +def _lhs_sampling(uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int): + lb_list = list() + ub_list = list() + for p in uncertain_params: + lb_list.append(p.lb) + ub_list.append(p.ub) + sampler = LatinHypercubeSampling([lb_list, ub_list], number_of_samples=num_points, sampling_type='creation') + + np.random.seed(seed) + sample_array = sampler.sample_points() + + sample_points = pe.ComponentMap() + for ndx, p in enumerate(uncertain_params): + sample_points[p] = [float(i) for i in sample_array[:, ndx]] + + return num_points, sample_points + + +_sample_strategy_map = dict() +_sample_strategy_map[SamplingStrategy.grid] = _grid_sampling +_sample_strategy_map[SamplingStrategy.lhs] = _lhs_sampling + + +class SamplingConfig(ConfigDict): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, + visibility=visibility) + self.strategy: SamplingStrategy = self.declare('strategy', ConfigValue(domain=InEnum(SamplingStrategy), + default=SamplingStrategy.lhs)) + self.lhs_seed: int = self.declare('lhs_seed', ConfigValue(domain=int, default=0)) + self.solver = self.declare('solver', ConfigValue(default=pe.SolverFactory('appsi_ipopt'))) + self.num_points: int = self.declare('num_points', ConfigValue(domain=int, default=100)) + self.enable_progress_bar: bool = self.declare('enable_progress_bar', ConfigValue(domain=bool, default=True)) + + +def _perform_sampling(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + controls: Optional[Sequence[_GeneralVarData]], + config: SamplingConfig) -> Tuple[MutableMapping[Union[_GeneralVarData, _ParamData], + Sequence[float]], + Sequence[float], + MutableMapping[_GeneralVarData, Sequence[float]]]: + if isinstance(config.solver, PersistentSolver): + using_persistent = True + else: + using_persistent = False + + unc_param_vars = list() + for p in uncertain_params: + ndx = _VarIndex(p, None) + p_var = m.unc_param_vars[ndx] + unc_param_vars.append(p_var) + n_samples, sample_points = _sample_strategy_map[config.strategy](unc_param_vars, config.num_points, config.lhs_seed) + + if using_persistent: + config.solver.set_instance(m) + original_update_config = config.solver.update_config() + config.solver.update_config.check_for_new_or_removed_constraints = False + config.solver.update_config.check_for_new_or_removed_vars = False + config.solver.update_config.check_for_new_or_removed_params = False + config.solver.update_config.update_constraints = False + config.solver.update_config.update_vars = False + config.solver.update_config.update_params = False + config.solver.update_config.update_named_expressions = False + + max_violation_values = list() + + control_values = pe.ComponentMap() + for v in controls: + control_values[v] = list() + + for sample_ndx in tqdm(list(range(n_samples)), ncols=100, desc='Sampling', disable=not config.enable_progress_bar): + for p, p_vals in sample_points.items(): + p.fix(p_vals[sample_ndx]) + + if using_persistent: + config.solver.update_variables(unc_param_vars) + + res = config.solver.solve(m) + pe.assert_optimal_termination(res) + max_violation_values.append(m.max_constraint_violation.value) + + for v in controls: + control_values[v].append(v.value) + + if using_persistent: + config.solver.update_config.set_value(original_update_config) + + unc_param_var_to_unc_param_map = pe.ComponentMap(zip(unc_param_vars, uncertain_params)) + sample_points = pe.ComponentMap((unc_param_var_to_unc_param_map[p], vals) for p, vals in sample_points.items()) + + return sample_points, max_violation_values, control_values + + +def perform_sampling(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + controls: Optional[Sequence[_GeneralVarData]], + in_place: bool, + config: SamplingConfig) -> Tuple[MutableMapping[Union[_GeneralVarData, _ParamData], + Sequence[float]], + Sequence[float], + MutableMapping[_GeneralVarData, Sequence[float]]]: + original_model = m + if not in_place: + m = m.clone() + uncertain_params = [m.find_component(p) for p in uncertain_params] + param_nominal_values = pe.ComponentMap((m.find_component(p), val) for p, val in param_nominal_values.items()) + param_bounds = pe.ComponentMap((m.find_component(p), bnds) for p, bnds in param_bounds.items()) + controls = [m.find_component(v) for v in controls] + + _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) + _build_inner_problem(m=m, enforce_equalities=True, unique_constraint_violations=False, valid_var_bounds=None) + sample_points, max_violation_values, control_values = _perform_sampling(m=m, uncertain_params=uncertain_params, + controls=controls, config=config) + + sample_points = pe.ComponentMap((original_model.find_component(p), vals) for p, vals in sample_points.items()) + control_values = pe.ComponentMap((original_model.find_component(v), vals) for v, vals in control_values.items()) + + return sample_points, max_violation_values, control_values diff --git a/idaes/apps/flexibility_analysis/tests/__init__.py b/idaes/apps/flexibility_analysis/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py new file mode 100644 index 0000000000..57bbae478a --- /dev/null +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -0,0 +1,104 @@ +import pyomo.environ as pe +from flexibility.flextest import build_active_constraint_flextest +import unittest +from flexibility.indices import _VarIndex +from pyomo.contrib.fbbt import interval + + +def create_poly_model(): + m = pe.ConcreteModel() + m.z = pe.Var(initialize=8, bounds=(-10, 15)) + m.theta = pe.Param(mutable=True) + + offset = 1.5 + + m.obj = pe.Objective(expr=m.z**2) + m.c1 = pe.Constraint(expr=0.01*(m.z-offset)**4 - 0.05*(m.z-offset)**3 - (m.z-offset)**2 - (m.z-offset) - 10 + m.theta <= 0) + m.c2 = pe.Constraint(expr=(-0.02*m.theta - 14)*m.z + (1.66*m.theta - 100) <= 0) + m.c3 = pe.Constraint(expr=30*m.z - 50 - 4*m.theta + pe.exp(-0.2*m.theta + 1) <= 0) + + nominal_values = pe.ComponentMap() + nominal_values[m.theta] = 22.5 + param_bounds = pe.ComponentMap() + param_bounds[m.theta] = (-20, 65) + + return m, nominal_values, param_bounds + + +def create_hx_network_model(): + """ + The model used for this test comes from + + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693. + """ + m = pe.ConcreteModel() + + m.uncertain_temps_set = pe.Set(initialize=[1, 3, 5, 8]) + m.uncertain_temps = pe.Param(m.uncertain_temps_set, mutable=True, initialize={1: 620, + 3: 388, + 5: 583, + 8: 313}) + nominal_values = pe.ComponentMap() + for p in m.uncertain_temps.values(): + nominal_values[p] = p.value + + param_bounds = pe.ComponentMap() + for p in m.uncertain_temps.values(): + param_bounds[p] = (p.value - 10.0, p.value + 10.0) + + m.variable_temps_set = pe.Set(initialize=[2, 4, 6, 7]) + m.variable_temps = pe.Var(m.variable_temps_set, bounds=(0, 1000)) + m.qc = pe.Var() + + m.balances = pe.Constraint([1, 2, 3, 4]) + m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * (m.variable_temps[4] - m.uncertain_temps[3]) + m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * (563 - m.variable_temps[4]) + m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * (393 - m.uncertain_temps[8]) + m.balances[4] = m.qc == 1.5 * (m.variable_temps[2] - 350) + + m.temp_approaches = pe.Constraint([1, 2, 3, 4]) + m.temp_approaches[1] = m.variable_temps[2] >= m.uncertain_temps[3] + m.temp_approaches[2] = m.variable_temps[6] >= m.variable_temps[4] + m.temp_approaches[3] = m.variable_temps[7] >= m.uncertain_temps[8] + m.temp_approaches[4] = 393 <= m.variable_temps[6] + + m.performance = pe.Constraint(expr=m.variable_temps[7] <= 323) + + return m, nominal_values, param_bounds + + +class TestFlexTest(unittest.TestCase): + def test_poly(self): + m, nominal_values, param_bounds = create_poly_model() + var_bounds = pe.ComponentMap() + var_bounds[m.z] = (-20, 20) + build_active_constraint_flextest(m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + valid_var_bounds=var_bounds) + opt = pe.SolverFactory('scip') + res = opt.solve(m, tee=False) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) + self.assertAlmostEqual(m.z.value, -2.6513, 4) + ndx = _VarIndex(m.theta, None) + self.assertAlmostEqual(m.unc_param_vars[ndx].value, 65) + + def test_hx_network(self): + m, nominal_values, param_bounds = create_hx_network_model() + var_bounds = pe.ComponentMap() + for v in m.variable_temps.values(): + var_bounds[v] = (100, 1000) + var_bounds[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) + build_active_constraint_flextest(m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + valid_var_bounds=var_bounds) + opt = pe.SolverFactory('gurobi_direct') + res = opt.solve(m, tee=False) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.max_constraint_violation.value, 4, 4) diff --git a/idaes/apps/flexibility_analysis/tests/test_indices.py b/idaes/apps/flexibility_analysis/tests/test_indices.py new file mode 100644 index 0000000000..3b48a8f1f6 --- /dev/null +++ b/idaes/apps/flexibility_analysis/tests/test_indices.py @@ -0,0 +1,57 @@ +import unittest +import pyomo.environ as pe +from flexibility.indices import _VarIndex, _ConIndex + + +class TestIndices(unittest.TestCase): + def test_var_index(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + + vi1 = _VarIndex(m.x, 'lb') + + self.assertIs(vi1.var, m.x) + self.assertEqual(vi1.bound, 'lb') + + vi2 = _VarIndex(m.x, 'lb') + self.assertEqual(vi1, vi2) + self.assertEqual(hash(vi1), hash(vi2)) + + vi2 = _VarIndex(m.x, 'ub') + self.assertNotEqual(vi1, vi2) + + vi2 = _VarIndex(m.y, 'lb') + self.assertNotEqual(vi1, vi2) + + self.assertEqual(str(vi1), "('x', 'lb')") + self.assertEqual(repr(vi1), "('x', 'lb')") + + def test_con_index(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.x == m.y) + m.c2 = pe.Constraint(expr=m.x == 2*m.y) + + ci1 = _ConIndex(m.c1, 'lb') + + self.assertIs(ci1.con, m.c1) + self.assertEqual(ci1.bound, 'lb') + + ci2 = _ConIndex(m.c1, 'lb') + self.assertEqual(ci1, ci2) + self.assertEqual(hash(ci1), hash(ci2)) + + ci2 = _ConIndex(m.c1, 'ub') + self.assertNotEqual(ci1, ci2) + + ci2 = _ConIndex(m.c2, 'lb') + self.assertNotEqual(ci1, ci2) + + self.assertEqual(str(ci1), "('c1', 'lb')") + self.assertEqual(repr(ci1), "('c1', 'lb')") + + vi1 = _VarIndex(m.x, 'lb') + self.assertNotEqual(ci1, vi1) + self.assertNotEqual(vi1, ci1) diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py new file mode 100644 index 0000000000..bb9c71cd1b --- /dev/null +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -0,0 +1,134 @@ +import pyomo.environ as pe +from flexibility.sampling import perform_sampling +import unittest +from flexibility.indices import _VarIndex +import numpy as np + + +def create_poly_model(): + m = pe.ConcreteModel() + m.z = pe.Var(initialize=8, bounds=(-10, 15)) + m.theta = pe.Param(mutable=True) + + offset = 1.5 + + m.obj = pe.Objective(expr=m.z**2) + m.c1 = pe.Constraint(expr=0.01*(m.z-offset)**4 - 0.05*(m.z-offset)**3 - (m.z-offset)**2 - (m.z-offset) - 10 + m.theta <= 0) + m.c2 = pe.Constraint(expr=(-0.02*m.theta - 14)*m.z + (1.66*m.theta - 100) <= 0) + m.c3 = pe.Constraint(expr=30*m.z - 50 - 4*m.theta + pe.exp(-0.2*m.theta + 1) <= 0) + + nominal_values = pe.ComponentMap() + nominal_values[m.theta] = 22.5 + param_bounds = pe.ComponentMap() + param_bounds[m.theta] = (-20, 65) + + return m, nominal_values, param_bounds + + +def create_hx_network_model(): + """ + The model used for this test comes from + + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693. + """ + m = pe.ConcreteModel() + + m.uncertain_temps_set = pe.Set(initialize=[1, 3, 5, 8]) + m.uncertain_temps = pe.Param(m.uncertain_temps_set, mutable=True, initialize={1: 620, + 3: 388, + 5: 583, + 8: 313}) + nominal_values = pe.ComponentMap() + for p in m.uncertain_temps.values(): + nominal_values[p] = p.value + + param_bounds = pe.ComponentMap() + for p in m.uncertain_temps.values(): + param_bounds[p] = (p.value - 10.0, p.value + 10.0) + + m.variable_temps_set = pe.Set(initialize=[2, 4, 6, 7]) + m.variable_temps = pe.Var(m.variable_temps_set, bounds=(0, 1000)) + m.qc = pe.Var() + + m.balances = pe.Constraint([1, 2, 3, 4]) + m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * (m.variable_temps[4] - m.uncertain_temps[3]) + m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * (563 - m.variable_temps[4]) + m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * (393 - m.uncertain_temps[8]) + m.balances[4] = m.qc == 1.5 * (m.variable_temps[2] - 350) + + m.temp_approaches = pe.Constraint([1, 2, 3, 4]) + m.temp_approaches[1] = m.variable_temps[2] >= m.uncertain_temps[3] + m.temp_approaches[2] = m.variable_temps[6] >= m.variable_temps[4] + m.temp_approaches[3] = m.variable_temps[7] >= m.uncertain_temps[8] + m.temp_approaches[4] = 393 <= m.variable_temps[6] + + m.performance = pe.Constraint(expr=m.variable_temps[7] <= 323) + + return m, nominal_values, param_bounds + + +class TestSampling(unittest.TestCase): + def test_poly(self): + m, nominal_values, param_bounds = create_poly_model() + opt = pe.SolverFactory('scip') + tmp = perform_sampling(m, + num_points=5, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.z], + in_place=True) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = np.argmax(max_violation_values) + self.assertAlmostEqual(max_violation_values[max_viol_ndx], -1.0142, 4) + self.assertAlmostEqual(control_values[m.z][max_viol_ndx], 4.6319, 4) + self.assertAlmostEqual(sample_points[m.theta][max_viol_ndx], 22.5) + + def test_hx_network(self): + m, nominal_values, param_bounds = create_hx_network_model() + opt = pe.SolverFactory('gurobi_direct') + tmp = perform_sampling(m, + num_points=2, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + in_place=True) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = np.argmax(max_violation_values) + self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) + + def test_hx_network2(self): + m, nominal_values, param_bounds = create_hx_network_model() + opt = pe.SolverFactory('gurobi_direct') + tmp = perform_sampling(m, + num_points=2, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + enforce_equalities=False, + controls=[m.qc], + in_place=True) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = np.argmax(max_violation_values) + self.assertAlmostEqual(max_violation_values[max_viol_ndx], 4, 4) + + def test_hx_network3(self): + m, nominal_values, param_bounds = create_hx_network_model() + opt = pe.SolverFactory('appsi_gurobi') + tmp = perform_sampling(m, + num_points=2, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + in_place=True) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = np.argmax(max_violation_values) + self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) diff --git a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py new file mode 100644 index 0000000000..647f13801a --- /dev/null +++ b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py @@ -0,0 +1,113 @@ +import pyomo.environ as pe +import unittest +from flexibility.uncertain_params import _replace_uncertain_params +from flexibility.indices import _ConIndex, _VarIndex +from pyomo.core.expr.compare import compare_expressions + + +class TestReplaceUncertainParams(unittest.TestCase): + def test_replace_mutable_parameter(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.p = pe.Param(mutable=True) + m.c1 = pe.Constraint(expr=(m.x + m.p, 0)) + m.c2 = pe.Constraint(expr=(None, m.y - m.x, 0)) + m.c3 = pe.Constraint(expr=(-1, m.y + m.p, 1)) + + nominal_values = pe.ComponentMap() + nominal_values[m.p] = 2.3 + + param_bounds = pe.ComponentMap() + param_bounds[m.p] = (1, 4) + + _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, + param_bounds=param_bounds) + + self.assertEqual(len(m.unc_cons), 4) + + self.assertEqual(len(m.unc_param_vars), 1) + v_ndx = _VarIndex(m.p, None) + self.assertEqual(m.unc_param_vars[v_ndx].lb, 1) + self.assertEqual(m.unc_param_vars[v_ndx].ub, 4) + self.assertEqual(m.unc_param_vars[v_ndx].value, 2.3) + + self.assertFalse(m.c1.active) + c_ndx = _ConIndex(m.c1, None) + self.assertEqual(m.unc_cons[c_ndx].lower, 0) + self.assertEqual(m.unc_cons[c_ndx].upper, 0) + self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.x + m.unc_param_vars[v_ndx])) + + self.assertFalse(m.c2.active) + c_ndx = _ConIndex(m.c2, 'ub') + self.assertEqual(m.unc_cons[c_ndx].lower, None) + self.assertEqual(m.unc_cons[c_ndx].upper, 0) + self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.y - m.x)) + + self.assertFalse(m.c3.active) + c_ndx = _ConIndex(m.c3, 'lb') + self.assertEqual(m.unc_cons[c_ndx].lower, -1) + self.assertEqual(m.unc_cons[c_ndx].upper, None) + self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.y + m.unc_param_vars[v_ndx])) + + c_ndx = _ConIndex(m.c3, 'ub') + self.assertEqual(m.unc_cons[c_ndx].lower, None) + self.assertEqual(m.unc_cons[c_ndx].upper, 1) + self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.y + m.unc_param_vars[v_ndx])) + + def test_replace_var(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.p = pe.Var() + m.c1 = pe.Constraint(expr=(m.x + m.p, 0)) + + nominal_values = pe.ComponentMap() + nominal_values[m.p] = 2.3 + + param_bounds = pe.ComponentMap() + param_bounds[m.p] = (1, 4) + + _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, + param_bounds=param_bounds) + + self.assertFalse(m.c1.active) + c_ndx = _ConIndex(m.c1, None) + v_ndx = _VarIndex(m.p, None) + self.assertEqual(m.unc_cons[c_ndx].lower, 0) + self.assertEqual(m.unc_cons[c_ndx].upper, 0) + self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.x + m.unc_param_vars[v_ndx])) + self.assertEqual(m.unc_param_vars[v_ndx].lb, 1) + self.assertEqual(m.unc_param_vars[v_ndx].ub, 4) + self.assertEqual(m.unc_param_vars[v_ndx].value, 2.3) + + def test_non_constant_var(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.p = pe.Param(initialize=1, mutable=True) + m.x.setlb(m.p) + + nominal_values = pe.ComponentMap() + nominal_values[m.p] = 2.3 + + param_bounds = pe.ComponentMap() + param_bounds[m.p] = (1, 4) + + with self.assertRaises(ValueError): + _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, + param_bounds=param_bounds) + + def test_non_constant_var2(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.p = pe.Param(initialize=1, mutable=True) + m.x.setub(m.p) + + nominal_values = pe.ComponentMap() + nominal_values[m.p] = 2.3 + + param_bounds = pe.ComponentMap() + param_bounds[m.p] = (1, 4) + + with self.assertRaises(ValueError): + _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, + param_bounds=param_bounds) diff --git a/idaes/apps/flexibility_analysis/tests/test_var_utils.py b/idaes/apps/flexibility_analysis/tests/test_var_utils.py new file mode 100644 index 0000000000..e54e07225d --- /dev/null +++ b/idaes/apps/flexibility_analysis/tests/test_var_utils.py @@ -0,0 +1,85 @@ +import pyomo.environ as pe +import unittest +from flexibility.var_utils import (get_all_unfixed_variables, get_used_unfixed_variables, BoundsManager, + _remove_var_bounds, _apply_var_bounds) + + +class TestGetVariables(unittest.TestCase): + def test_get_all_unfixed_variables(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.x_ref = pe.Reference(m.x) + m.y.fix(3) + + unfixed_vars = get_all_unfixed_variables(m) + self.assertEqual(len(unfixed_vars), 1) + self.assertIn(m.x, unfixed_vars) + self.assertNotIn(m.y, unfixed_vars) + + def test_get_used_unfixed_variables(self): + m = pe.ConcreteModel() + m.x = pe.Var([1,2,3,4,5,6]) + m.x[3].fix(1) + m.x[4].fix(1) + m.x[6].fix(1) + m.c1 = pe.Constraint(expr=m.x[1] == m.x[3]) + m.obj = pe.Objective(expr=m.x[5] + m.x[6]) + uuf_vars = get_used_unfixed_variables(m) + self.assertEqual(len(uuf_vars), 2) + self.assertIn(m.x[1], uuf_vars) + self.assertIn(m.x[5], uuf_vars) + + +class TestBounds(unittest.TestCase): + def test_bounds_manager1(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + + bm = BoundsManager(m) + bm.save_bounds() + m.x.setlb(-2) + self.assertEqual(m.x.lb, -2) + bm.pop_bounds() + self.assertEqual(m.x.lb, -1) + + def test_remove_var_bounds(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(domain=pe.NonNegativeReals) + + bm = BoundsManager(m) + bm.save_bounds() + + _remove_var_bounds(m) + self.assertIsNone(m.x.lb) + self.assertIsNone(m.x.ub) + self.assertIsNone(m.y.lb) + self.assertIsNone(m.y.ub) + + bm.pop_bounds() + self.assertEqual(m.x.lb, -1) + self.assertEqual(m.x.ub, 1) + self.assertEqual(m.y.lb, 0) + self.assertIsNone(m.y.ub) + + def test_remove_var_bounds_exception(self): + m = pe.ConcreteModel() + m.x = pe.Var(domain=pe.Binary) + with self.assertRaises(ValueError): + _remove_var_bounds(m) + + def test_apply_var_bounds(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var(bounds=(-1, 1)) + + new_bounds = pe.ComponentMap() + new_bounds[m.x] = (-5, 5) + new_bounds[m.y] = (0, 2) + + _apply_var_bounds(new_bounds) + self.assertEqual(m.x.lb, -5) + self.assertEqual(m.x.ub, 5) + self.assertEqual(m.y.lb, 0) + self.assertEqual(m.y.ub, 1) diff --git a/idaes/apps/flexibility_analysis/uncertain_params.py b/idaes/apps/flexibility_analysis/uncertain_params.py new file mode 100644 index 0000000000..fabc936d89 --- /dev/null +++ b/idaes/apps/flexibility_analysis/uncertain_params.py @@ -0,0 +1,53 @@ +from pyomo.core.base.block import _BlockData +import pyomo.environ as pe +from pyomo.core.expr.visitor import replace_expressions +from .indices import _VarIndex, _ConIndex +from typing import Sequence, Union, Mapping +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData +from .var_utils import get_all_unfixed_variables + + +def _replace_uncertain_params(m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]]) -> _BlockData: + for v in get_all_unfixed_variables(m): + if not pe.is_constant(v._lb): + raise ValueError(f'The lower bound on {str(v)} is not constant. All variable bounds must be constant.') + if not pe.is_constant(v._ub): + raise ValueError(f'The upper bound on {str(v)} is not constant. All variable bounds must be constant.') + + m.unc_param_vars_set = pe.Set() + m.unc_param_vars = pe.Var(m.unc_param_vars_set) + sub_map = dict() + for p in uncertain_params: + key = _VarIndex(p, None) + m.unc_param_vars_set.add(key) + sub_map[id(p)] = m.unc_param_vars[key] + m.unc_param_vars[key].setlb(param_bounds[p][0]) + m.unc_param_vars[key].setub(param_bounds[p][1]) + m.unc_param_vars[key].value = param_nominal_values[p] + + m.unc_cons_set = pe.Set() + m.unc_cons = pe.Constraint(m.unc_cons_set) + for c in list(m.component_data_objects(pe.Constraint, descend_into=True, active=True)): + new_body = replace_expressions(c.body, substitution_map=sub_map) + new_lower = replace_expressions(c.lower, substitution_map=sub_map) + new_upper = replace_expressions(c.upper, substitution_map=sub_map) + if c.equality: + key = _ConIndex(c, None) + m.unc_cons_set.add(key) + m.unc_cons[key] = new_body == new_lower + else: + if c.lower is not None: + key = _ConIndex(c, 'lb') + m.unc_cons_set.add(key) + m.unc_cons[key] = new_lower <= new_body + if c.upper is not None: + key = _ConIndex(c, 'ub') + m.unc_cons_set.add(key) + m.unc_cons[key] = new_body <= new_upper + c.deactivate() + + return m diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py new file mode 100644 index 0000000000..2ff5f14e43 --- /dev/null +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -0,0 +1,57 @@ +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData +from pyomo.common.collections import ComponentSet +from pyomo.core.expr.visitor import identify_variables +from coramin.utils import get_objective +from typing import Mapping, Sequence +from pyomo.core.base.var import _GeneralVarData + + +def get_all_unfixed_variables(m: _BlockData): + return ComponentSet(v for v in m.component_data_objects(pe.Var, descend_into=True, active=True) if not v.is_fixed()) + + +def get_used_unfixed_variables(m: _BlockData): + res = ComponentSet() + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): + res.update(v for v in identify_variables(c.body, include_fixed=False)) + obj = get_objective(m) + if obj is not None: + res.update(identify_variables(obj.expr, include_fixed=False)) + return res + + +class BoundsManager(object): + def __init__(self, m: _BlockData): + self._vars = ComponentSet(m.component_data_objects(pe.Var, descend_into=True)) + self._saved_bounds = list() + + def save_bounds(self): + bnds = pe.ComponentMap() + for v in self._vars: + bnds[v] = (v.lb, v.ub) + self._saved_bounds.append(bnds) + + def pop_bounds(self, ndx=-1): + bnds = self._saved_bounds.pop(ndx) + for v, _bnds in bnds.items(): + lb, ub = _bnds + v.setlb(lb) + v.setub(ub) + + +def _remove_var_bounds(m: _BlockData): + for v in get_all_unfixed_variables(m): + v.setlb(None) + v.setub(None) + if v.is_integer(): + raise ValueError('Unwilling to remove domain from integer variable') + v.domain = pe.Reals + + +def _apply_var_bounds(bounds: Mapping[_GeneralVarData, Sequence[float]]): + for v, (lb, ub) in bounds.items(): + if v.lb is None or v.lb < lb: + v.setlb(lb) + if v.ub is None or v.ub > ub: + v.setub(ub) From 63d2e820c92f4b7c37fa24c4a436f76cba80b78c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 08:31:03 -0700 Subject: [PATCH 02/60] reformatting files in flexibility analysis --- idaes/apps/flexibility_analysis/__init__.py | 12 +- .../decision_rules/dr_config.py | 18 +- .../decision_rules/linear_dr.py | 38 +- .../decision_rules/relu_dr.py | 103 +++- .../decision_rules/tests/test_linear_dr.py | 18 +- .../examples/idaes_hx_network.py | 181 +++--- .../examples/linear_hx_network.py | 63 +- .../examples/nonlin_hx_network.py | 50 +- idaes/apps/flexibility_analysis/flex_index.py | 234 ++++++-- idaes/apps/flexibility_analysis/flextest.py | 548 +++++++++++------- idaes/apps/flexibility_analysis/indices.py | 2 - .../flexibility_analysis/inner_problem.py | 105 +++- idaes/apps/flexibility_analysis/kkt.py | 53 +- idaes/apps/flexibility_analysis/sampling.py | 149 +++-- .../tests/test_flextest.py | 67 ++- .../tests/test_indices.py | 24 +- .../tests/test_sampling.py | 121 ++-- .../tests/test_uncertain_params.py | 54 +- .../tests/test_var_utils.py | 11 +- .../flexibility_analysis/uncertain_params.py | 26 +- idaes/apps/flexibility_analysis/var_utils.py | 8 +- 21 files changed, 1258 insertions(+), 627 deletions(-) diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index cf072f7a8e..8202cf7833 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -1,5 +1,13 @@ -from .flextest import (solve_flextest, SamplingStrategy, FlexTestConfig, FlexTestMethod, - FlexTestTermination, FlexTestResults, SamplingConfig, FlexTest) +from .flextest import ( + solve_flextest, + SamplingStrategy, + FlexTestConfig, + FlexTestMethod, + FlexTestTermination, + FlexTestResults, + SamplingConfig, + FlexTest, +) from .decision_rules.dr_enum import DecisionRuleTypes from .decision_rules.linear_dr import LinearDRConfig from .decision_rules.relu_dr import ReluDRConfig diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py index 266f2b72d0..6e1c1403bf 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py @@ -2,6 +2,18 @@ class DRConfig(ConfigDict): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): - super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, - visibility=visibility) + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index ca664f380e..27e2bc3921 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -8,15 +8,31 @@ class LinearDRConfig(DRConfig): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): - super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, - visibility=visibility) - self.solver = self.declare('solver', ConfigValue(default=pe.SolverFactory('appsi_gurobi'))) + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.solver = self.declare( + "solver", ConfigValue(default=pe.SolverFactory("appsi_gurobi")) + ) -def construct_linear_decision_rule(input_vals: MutableMapping[_GeneralVarData, Sequence[float]], - output_vals: MutableMapping[_GeneralVarData, Sequence[float]], - config: LinearDRConfig) -> _BlockData: +def construct_linear_decision_rule( + input_vals: MutableMapping[_GeneralVarData, Sequence[float]], + output_vals: MutableMapping[_GeneralVarData, Sequence[float]], + config: LinearDRConfig, +) -> _BlockData: n_inputs = len(input_vals) n_outputs = len(output_vals) @@ -35,7 +51,9 @@ def construct_linear_decision_rule(input_vals: MutableMapping[_GeneralVarData, S trainer.coefs = pe.Var(trainer.input_set) trainer.out_est = pe.Var(trainer.sample_set) - obj_expr = sum((trainer.out_est[i] - out_samples[i])**2 for i in trainer.sample_set) + obj_expr = sum( + (trainer.out_est[i] - out_samples[i]) ** 2 for i in trainer.sample_set + ) trainer.objective = pe.Objective(expr=obj_expr) trainer.est_cons = pe.Constraint(trainer.sample_set) @@ -56,7 +74,9 @@ def construct_linear_decision_rule(input_vals: MutableMapping[_GeneralVarData, S lin_vars = [v for v in input_vals.keys()] lin_coefs.append(-1) lin_vars.append(output_var) - dr_expr = LinearExpression(constant=trainer.const.value, linear_coefs=lin_coefs, linear_vars=lin_vars) + dr_expr = LinearExpression( + constant=trainer.const.value, linear_coefs=lin_coefs, linear_vars=lin_vars + ) res.decision_rule[out_ndx] = (dr_expr, 0) return res diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index e6b441ee64..dfdf8e5b8e 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -14,22 +14,50 @@ class ReluDRConfig(DRConfig): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): - super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, - visibility=visibility) - self.n_layers: int = self.declare('n_layers', ConfigValue(domain=int, default=4)) - self.n_nodes_per_layer: int = self.declare('n_nodes_per_layer', ConfigValue(domain=int, default=4)) - self.tensorflow_seed: int = self.declare('tensorflow_seed', ConfigValue(domain=int, default=0)) - self.scale_inputs: bool = self.declare('scale_inputs', ConfigValue(domain=bool, default=True)) - self.scale_outputs: bool = self.declare('scale_outputs', ConfigValue(domain=bool, default=True)) - self.epochs: int = self.declare('epochs', ConfigValue(domain=int, default=2000)) - self.batch_size: int = self.declare('batch_size', ConfigValue(domain=int, default=20)) - self.plot_history: bool = self.declare('plot_history', ConfigValue(domain=bool, default=False)) + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.n_layers: int = self.declare( + "n_layers", ConfigValue(domain=int, default=4) + ) + self.n_nodes_per_layer: int = self.declare( + "n_nodes_per_layer", ConfigValue(domain=int, default=4) + ) + self.tensorflow_seed: int = self.declare( + "tensorflow_seed", ConfigValue(domain=int, default=0) + ) + self.scale_inputs: bool = self.declare( + "scale_inputs", ConfigValue(domain=bool, default=True) + ) + self.scale_outputs: bool = self.declare( + "scale_outputs", ConfigValue(domain=bool, default=True) + ) + self.epochs: int = self.declare("epochs", ConfigValue(domain=int, default=2000)) + self.batch_size: int = self.declare( + "batch_size", ConfigValue(domain=int, default=20) + ) + self.plot_history: bool = self.declare( + "plot_history", ConfigValue(domain=bool, default=False) + ) -def construct_relu_decision_rule(input_vals: MutableMapping[_GeneralVarData, Sequence[float]], - output_vals: MutableMapping[_GeneralVarData, Sequence[float]], - config: ReluDRConfig) -> _BlockData: +def construct_relu_decision_rule( + input_vals: MutableMapping[_GeneralVarData, Sequence[float]], + output_vals: MutableMapping[_GeneralVarData, Sequence[float]], + config: ReluDRConfig, +) -> _BlockData: tf.random.set_seed(config.tensorflow_seed) inputs = list(input_vals.keys()) outputs = list(output_vals.keys()) @@ -41,7 +69,7 @@ def construct_relu_decision_rule(input_vals: MutableMapping[_GeneralVarData, Seq training_input = np.empty((n_samples, len(inputs))) for ndx, inp in enumerate(inputs): - training_input[:,ndx] = np.array(input_vals[inp], dtype=np.float64) + training_input[:, ndx] = np.array(input_vals[inp], dtype=np.float64) training_output = np.empty((n_samples, len(outputs))) for ndx, outp in enumerate(outputs): @@ -51,7 +79,9 @@ def construct_relu_decision_rule(input_vals: MutableMapping[_GeneralVarData, Seq input_mean = training_input.mean(axis=0) input_std = training_input.std(axis=0) for ndx in range(len(inputs)): - training_input[:, ndx] = (training_input[:, ndx] - input_mean[ndx]) / input_std[ndx] + training_input[:, ndx] = ( + training_input[:, ndx] - input_mean[ndx] + ) / input_std[ndx] else: input_mean = [0] * len(inputs) input_std = [1] * len(inputs) @@ -60,35 +90,50 @@ def construct_relu_decision_rule(input_vals: MutableMapping[_GeneralVarData, Seq output_mean = training_output.mean(axis=0) output_std = training_output.std(axis=0) for ndx in range(len(outputs)): - training_output[:, ndx] = (training_output[:, ndx] - output_mean[ndx]) / output_std[ndx] + training_output[:, ndx] = ( + training_output[:, ndx] - output_mean[ndx] + ) / output_std[ndx] else: output_mean = [0] * len(outputs) output_std = [1] * len(outputs) nn = keras.Sequential() - nn.add(layers.Dense(units=config.n_nodes_per_layer, input_dim=len(inputs), activation='relu')) + nn.add( + layers.Dense( + units=config.n_nodes_per_layer, input_dim=len(inputs), activation="relu" + ) + ) for layer_ndx in range(config.n_layers - 1): - nn.add(layers.Dense(config.n_nodes_per_layer, activation='relu')) + nn.add(layers.Dense(config.n_nodes_per_layer, activation="relu")) nn.add(layers.Dense(len(outputs))) - nn.compile(optimizer=keras.optimizers.Adam(), loss='mse') - history = nn.fit(training_input, training_output, batch_size=config.batch_size, epochs=config.epochs, verbose=0) + nn.compile(optimizer=keras.optimizers.Adam(), loss="mse") + history = nn.fit( + training_input, + training_output, + batch_size=config.batch_size, + epochs=config.epochs, + verbose=0, + ) if config.plot_history: import matplotlib.pyplot as plt - plt.scatter(history.epoch, history.history['loss']) - plt.xlabel('Epoch') - plt.ylabel('Loss') - plt.yscale('log') + + plt.scatter(history.epoch, history.history["loss"]) + plt.xlabel("Epoch") + plt.ylabel("Loss") + plt.yscale("log") plt.show() plt.close() res = pe.Block(concrete=True) res.nn = OmltBlock() - scaler = OffsetScaling(offset_inputs=[float(i) for i in input_mean], - factor_inputs=[float(i) for i in input_std], - offset_outputs=[float(i) for i in output_mean], - factor_outputs=[float(i) for i in output_std]) + scaler = OffsetScaling( + offset_inputs=[float(i) for i in input_mean], + factor_inputs=[float(i) for i in input_std], + offset_outputs=[float(i) for i in output_mean], + factor_outputs=[float(i) for i in output_std], + ) input_bounds = [(v.lb, v.ub) for v in inputs] net = load_keras_sequential(nn, scaler, input_bounds) formulation = ReluBigMFormulation(net) diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index aa3a149718..7246337cfd 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -7,11 +7,11 @@ def y1_func(x1, x2): - return 3*x1 - 2*x2 + 5 + return 3 * x1 - 2 * x2 + 5 def y2_func(x1, x2): - return -x1 + 0.5*x2 + return -x1 + 0.5 * x2 class TestLinearDecisionRule(unittest.TestCase): @@ -41,12 +41,20 @@ def test_construct_linear_dr(self): output_vals[m.y2] = [float(i) for i in y2_samples] opt = Gurobi() - m.dr = construct_linear_decision_rule(input_vals=input_vals, output_vals=output_vals, solver=opt) + m.dr = construct_linear_decision_rule( + input_vals=input_vals, output_vals=output_vals, solver=opt + ) self.assertEqual(pe.value(m.dr.decision_rule[0].lower), 0) self.assertEqual(pe.value(m.dr.decision_rule[0].lower), 0) - self.assertAlmostEqual(pe.value(m.dr.decision_rule[0].body), y1_func(m.x1.value, m.x2.value) - m.y1.value) + self.assertAlmostEqual( + pe.value(m.dr.decision_rule[0].body), + y1_func(m.x1.value, m.x2.value) - m.y1.value, + ) self.assertEqual(pe.value(m.dr.decision_rule[1].lower), 0) self.assertEqual(pe.value(m.dr.decision_rule[1].lower), 0) - self.assertAlmostEqual(pe.value(m.dr.decision_rule[1].body), y2_func(m.x1.value, m.x2.value) - m.y2.value) + self.assertAlmostEqual( + pe.value(m.dr.decision_rule[1].body), + y2_func(m.x1.value, m.x2.value) - m.y2.value, + ) diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 283416ca79..9e9227bd11 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -1,5 +1,7 @@ import pyomo.environ as pe -from idaes.generic_models.properties.activity_coeff_models.BTX_activity_coeff_VLE import BTXParameterBlock +from idaes.generic_models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( + BTXParameterBlock, +) from idaes.core import FlowsheetBlock from idaes.generic_models.unit_models.heater import Heater import coramin @@ -23,18 +25,22 @@ def create_model(): m = pe.ConcreteModel() - m.fs = FlowsheetBlock(default={'dynamic': False}) - m.fs.properties = BTXParameterBlock(default={'valid_phase': 'Vap', - 'activity_coeff_model': 'Ideal', - 'state_vars': 'FTPz'}) - - m.fs.heater1 = Heater(default={'property_package': m.fs.properties}) - m.fs.cooler1 = Heater(default={'property_package': m.fs.properties}) - m.fs.heater2 = Heater(default={'property_package': m.fs.properties}) - m.fs.cooler2 = Heater(default={'property_package': m.fs.properties}) - m.fs.heater3 = Heater(default={'property_package': m.fs.properties}) - m.fs.cooler3 = Heater(default={'property_package': m.fs.properties}) - m.fs.cooler4 = Heater(default={'property_package': m.fs.properties}) + m.fs = FlowsheetBlock(default={"dynamic": False}) + m.fs.properties = BTXParameterBlock( + default={ + "valid_phase": "Vap", + "activity_coeff_model": "Ideal", + "state_vars": "FTPz", + } + ) + + m.fs.heater1 = Heater(default={"property_package": m.fs.properties}) + m.fs.cooler1 = Heater(default={"property_package": m.fs.properties}) + m.fs.heater2 = Heater(default={"property_package": m.fs.properties}) + m.fs.cooler2 = Heater(default={"property_package": m.fs.properties}) + m.fs.heater3 = Heater(default={"property_package": m.fs.properties}) + m.fs.cooler3 = Heater(default={"property_package": m.fs.properties}) + m.fs.cooler4 = Heater(default={"property_package": m.fs.properties}) m.fs.s1 = Arc(source=m.fs.heater1.outlet, destination=m.fs.heater2.inlet) m.fs.s2 = Arc(source=m.fs.cooler1.outlet, destination=m.fs.cooler4.inlet) @@ -51,12 +57,30 @@ def create_model(): approach_limit = 5 m.fs.temp_approach = pe.ConstraintList() - m.fs.temp_approach.add(m.fs.cooler1.inlet.temperature[0] >= m.fs.heater1.outlet.temperature[0] + approach_limit) - m.fs.temp_approach.add(m.fs.cooler1.outlet.temperature[0] >= m.fs.heater1.inlet.temperature[0] + approach_limit) - m.fs.temp_approach.add(m.fs.cooler2.inlet.temperature[0] >= m.fs.heater2.outlet.temperature[0] + approach_limit) - m.fs.temp_approach.add(m.fs.cooler2.outlet.temperature[0] >= m.fs.heater2.inlet.temperature[0] + approach_limit) - m.fs.temp_approach.add(m.fs.cooler3.inlet.temperature[0] >= m.fs.heater3.outlet.temperature[0] + approach_limit) - m.fs.temp_approach.add(m.fs.cooler3.outlet.temperature[0] >= m.fs.heater3.inlet.temperature[0] + approach_limit) + m.fs.temp_approach.add( + m.fs.cooler1.inlet.temperature[0] + >= m.fs.heater1.outlet.temperature[0] + approach_limit + ) + m.fs.temp_approach.add( + m.fs.cooler1.outlet.temperature[0] + >= m.fs.heater1.inlet.temperature[0] + approach_limit + ) + m.fs.temp_approach.add( + m.fs.cooler2.inlet.temperature[0] + >= m.fs.heater2.outlet.temperature[0] + approach_limit + ) + m.fs.temp_approach.add( + m.fs.cooler2.outlet.temperature[0] + >= m.fs.heater2.inlet.temperature[0] + approach_limit + ) + m.fs.temp_approach.add( + m.fs.cooler3.inlet.temperature[0] + >= m.fs.heater3.outlet.temperature[0] + approach_limit + ) + m.fs.temp_approach.add( + m.fs.cooler3.outlet.temperature[0] + >= m.fs.heater3.inlet.temperature[0] + approach_limit + ) # specs m.fs.heater1.inlet.temperature[0].fix(488) @@ -66,7 +90,9 @@ def create_model(): m.fs.heater3.inlet.temperature[0].fix(400) m.fs.heater3.outlet.temperature[0].fix(550) m.fs.cooler4.outlet.temperature[0].fix(450) - m.fs.cooler3_out_temp_spec = pe.Constraint(expr=m.fs.cooler3.outlet.temperature[0] <= 423) + m.fs.cooler3_out_temp_spec = pe.Constraint( + expr=m.fs.cooler3.outlet.temperature[0] <= 423 + ) m.fs.heater1.inlet.flow_mol[0].fix(1) m.fs.cooler1.inlet.flow_mol[0].fix(1) @@ -80,19 +106,19 @@ def create_model(): m.fs.cooler2.inlet.pressure[0].fix(101325) m.fs.heater3.inlet.pressure[0].fix(101325) - m.fs.heater1.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) - m.fs.cooler1.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) - m.fs.cooler2.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) - m.fs.heater3.inlet.mole_frac_comp[0, 'benzene'].fix(0.5) + m.fs.heater1.inlet.mole_frac_comp[0, "benzene"].fix(0.5) + m.fs.cooler1.inlet.mole_frac_comp[0, "benzene"].fix(0.5) + m.fs.cooler2.inlet.mole_frac_comp[0, "benzene"].fix(0.5) + m.fs.heater3.inlet.mole_frac_comp[0, "benzene"].fix(0.5) - m.fs.heater1.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) - m.fs.cooler1.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) - m.fs.cooler2.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) - m.fs.heater3.inlet.mole_frac_comp[0, 'toluene'].fix(0.5) + m.fs.heater1.inlet.mole_frac_comp[0, "toluene"].fix(0.5) + m.fs.cooler1.inlet.mole_frac_comp[0, "toluene"].fix(0.5) + m.fs.cooler2.inlet.mole_frac_comp[0, "toluene"].fix(0.5) + m.fs.heater3.inlet.mole_frac_comp[0, "toluene"].fix(0.5) m.obj = pe.Objective(expr=-m.fs.cooler4.heat_duty[0]) - pe.TransformationFactory('network.expand_arcs').apply_to(m) + pe.TransformationFactory("network.expand_arcs").apply_to(m) scale_model(m) nominal_values = pe.ComponentMap() @@ -126,7 +152,7 @@ def create_model(): for p in nominal_values.keys(): assert p.is_fixed() p.unfix() - + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): body_degree = polynomial_degree(c.body) lower, body, upper = c.lower, c.body, c.upper @@ -135,7 +161,7 @@ def create_model(): se = se.simplify() body = sympy2pyomo_expression(se, om) c.set_value((c.lower, body, c.upper)) - print('simplified: ', c.body) + print("simplified: ", c.body) for p in nominal_values.keys(): assert not p.is_fixed() @@ -194,26 +220,26 @@ def get_var_bounds(m: _BlockData, param_bounds): b.properties_in[0].flow_mol.setub(5) b.properties_out[0].flow_mol.setlb(0) b.properties_out[0].flow_mol.setub(5) - b.properties_in[0].flow_mol_phase['Vap'].setlb(0) - b.properties_in[0].flow_mol_phase['Vap'].setub(5) - b.properties_out[0].flow_mol_phase['Vap'].setlb(0) - b.properties_out[0].flow_mol_phase['Vap'].setub(5) - b.properties_in[0].mole_frac_comp['benzene'].setlb(0) - b.properties_in[0].mole_frac_comp['benzene'].setub(1) - b.properties_in[0].mole_frac_comp['toluene'].setlb(0) - b.properties_in[0].mole_frac_comp['toluene'].setub(1) - b.properties_out[0].mole_frac_comp['benzene'].setlb(0) - b.properties_out[0].mole_frac_comp['benzene'].setub(1) - b.properties_out[0].mole_frac_comp['toluene'].setlb(0) - b.properties_out[0].mole_frac_comp['toluene'].setub(1) - b.properties_in[0].mole_frac_phase_comp['Vap','benzene'].setlb(0) - b.properties_in[0].mole_frac_phase_comp['Vap','benzene'].setub(1) - b.properties_in[0].mole_frac_phase_comp['Vap','toluene'].setlb(0) - b.properties_in[0].mole_frac_phase_comp['Vap','toluene'].setub(1) - b.properties_out[0].mole_frac_phase_comp['Vap','benzene'].setlb(0) - b.properties_out[0].mole_frac_phase_comp['Vap','benzene'].setub(1) - b.properties_out[0].mole_frac_phase_comp['Vap','toluene'].setlb(0) - b.properties_out[0].mole_frac_phase_comp['Vap','toluene'].setub(1) + b.properties_in[0].flow_mol_phase["Vap"].setlb(0) + b.properties_in[0].flow_mol_phase["Vap"].setub(5) + b.properties_out[0].flow_mol_phase["Vap"].setlb(0) + b.properties_out[0].flow_mol_phase["Vap"].setub(5) + b.properties_in[0].mole_frac_comp["benzene"].setlb(0) + b.properties_in[0].mole_frac_comp["benzene"].setub(1) + b.properties_in[0].mole_frac_comp["toluene"].setlb(0) + b.properties_in[0].mole_frac_comp["toluene"].setub(1) + b.properties_out[0].mole_frac_comp["benzene"].setlb(0) + b.properties_out[0].mole_frac_comp["benzene"].setub(1) + b.properties_out[0].mole_frac_comp["toluene"].setlb(0) + b.properties_out[0].mole_frac_comp["toluene"].setub(1) + b.properties_in[0].mole_frac_phase_comp["Vap", "benzene"].setlb(0) + b.properties_in[0].mole_frac_phase_comp["Vap", "benzene"].setub(1) + b.properties_in[0].mole_frac_phase_comp["Vap", "toluene"].setlb(0) + b.properties_in[0].mole_frac_phase_comp["Vap", "toluene"].setub(1) + b.properties_out[0].mole_frac_phase_comp["Vap", "benzene"].setlb(0) + b.properties_out[0].mole_frac_phase_comp["Vap", "benzene"].setub(1) + b.properties_out[0].mole_frac_phase_comp["Vap", "toluene"].setlb(0) + b.properties_out[0].mole_frac_phase_comp["Vap", "toluene"].setub(1) b.properties_in[0].pressure.setlb(1.01325) b.properties_in[0].pressure.setub(1.01325) b.properties_out[0].pressure.setlb(1.01325) @@ -252,12 +278,20 @@ def scale_model(m): m.scaling_factor[b.properties_out[0].pressure] = 1e-5 m.scaling_factor[b.properties_in[0].temperature] = 1e-2 m.scaling_factor[b.properties_out[0].temperature] = 1e-2 - m.scaling_factor[b.properties_in[0].enth_mol_phase['Vap']] = 1e-4 - m.scaling_factor[b.properties_out[0].enth_mol_phase['Vap']] = 1e-4 - m.scaling_factor[b.properties_in[0].enth_mol_phase_comp['Vap', 'benzene']] = 1e-4 - m.scaling_factor[b.properties_out[0].enth_mol_phase_comp['Vap', 'benzene']] = 1e-4 - m.scaling_factor[b.properties_in[0].enth_mol_phase_comp['Vap', 'toluene']] = 1e-4 - m.scaling_factor[b.properties_out[0].enth_mol_phase_comp['Vap', 'toluene']] = 1e-4 + m.scaling_factor[b.properties_in[0].enth_mol_phase["Vap"]] = 1e-4 + m.scaling_factor[b.properties_out[0].enth_mol_phase["Vap"]] = 1e-4 + m.scaling_factor[ + b.properties_in[0].enth_mol_phase_comp["Vap", "benzene"] + ] = 1e-4 + m.scaling_factor[ + b.properties_out[0].enth_mol_phase_comp["Vap", "benzene"] + ] = 1e-4 + m.scaling_factor[ + b.properties_in[0].enth_mol_phase_comp["Vap", "toluene"] + ] = 1e-4 + m.scaling_factor[ + b.properties_out[0].enth_mol_phase_comp["Vap", "toluene"] + ] = 1e-4 for c in b.enthalpy_balances.values(): m.scaling_factor[c] = 1e-4 for c in b.pressure_balance.values(): @@ -277,7 +311,7 @@ def scale_model(m): m.scaling_factor[m.fs.s3_expanded.pressure_equality[0]] = 1e-4 m.scaling_factor[coramin.utils.get_objective(m)] = 1e-4 - pe.TransformationFactory('core.scale_model').apply_to(m, rename=False) + pe.TransformationFactory("core.scale_model").apply_to(m, rename=False) def nominal_optimization(): @@ -287,7 +321,7 @@ def nominal_optimization(): log_infeasible_constraints(m, log_variables=False) log_infeasible_bounds(m) - opt = pe.SolverFactory('ipopt') + opt = pe.SolverFactory("ipopt") res = opt.solve(m, tee=True) pe.assert_optimal_termination(res) @@ -311,14 +345,14 @@ def main(method): config.feasibility_tol = 1e-6 config.terminate_early = False config.method = method - config.minlp_solver = pe.SolverFactory('scip') - config.minlp_solver.options['limits/time'] = 300 - config.sampling_config.solver = pe.SolverFactory('appsi_ipopt') + config.minlp_solver = pe.SolverFactory("scip") + config.minlp_solver.options["limits/time"] = 300 + config.sampling_config.solver = pe.SolverFactory("appsi_ipopt") config.sampling_config.strategy = flexibility.SamplingStrategy.lhs config.sampling_config.num_points = 100 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() - config.decision_rule_config.solver = pe.SolverFactory('appsi_gurobi') + config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 @@ -327,11 +361,18 @@ def main(method): config.decision_rule_config.batch_size = 50 config.decision_rule_config.scale_inputs = True config.decision_rule_config.scale_outputs = True - results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, param_bounds=param_bounds, - controls=[m.fs.cooler4.control_volume.heat[0], - m.fs.heater3.control_volume.properties_in[0].flow_mol], - valid_var_bounds=var_bounds, config=config) + results = flexibility.solve_flextest( + m=m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[ + m.fs.cooler4.control_volume.heat[0], + m.fs.heater3.control_volume.properties_in[0].flow_mol, + ], + valid_var_bounds=var_bounds, + config=config, + ) print(results) # results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), # param_nominal_values=nominal_values, param_bounds=param_bounds, @@ -339,8 +380,8 @@ def main(method): # print(results) -if __name__ == '__main__': - print('\n\n********************Active Constraint**************************') +if __name__ == "__main__": + print("\n\n********************Active Constraint**************************") main(flexibility.FlexTestMethod.active_constraint) # print('\n\n********************Linear Decision Rule**************************') # main(flexibility.FlexTestMethod.linear_decision_rule) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 9108e4f57c..4bc633dd80 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -14,18 +14,19 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: 11(6), 675-693. """ - print("""This example is based off of \n\n + print( + """This example is based off of \n\n Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for flexibility analysis in chemical processes. Computers & Chemical Engineering, - 11(6), 675-693.\n\n""") + 11(6), 675-693.\n\n""" + ) m = pe.ConcreteModel() m.uncertain_temps_set = pe.Set(initialize=[1, 3, 5, 8]) - m.uncertain_temps = pe.Param(m.uncertain_temps_set, mutable=True, initialize={1: 620, - 3: 388, - 5: 583, - 8: 313}) + m.uncertain_temps = pe.Param( + m.uncertain_temps_set, mutable=True, initialize={1: 620, 3: 388, 5: 583, 8: 313} + ) nominal_values = pe.ComponentMap() for p in m.uncertain_temps.values(): nominal_values[p] = p.value @@ -39,9 +40,15 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: m.qc = pe.Var() m.balances = pe.Constraint([1, 2, 3, 4]) - m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * (m.variable_temps[4] - m.uncertain_temps[3]) - m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * (563 - m.variable_temps[4]) - m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * (393 - m.uncertain_temps[8]) + m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * ( + m.variable_temps[4] - m.uncertain_temps[3] + ) + m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * ( + 563 - m.variable_temps[4] + ) + m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * ( + 393 - m.uncertain_temps[8] + ) m.balances[4] = m.qc == 1.5 * (m.variable_temps[2] - 350) m.temp_approaches = pe.Constraint([1, 2, 3, 4]) @@ -52,11 +59,11 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: m.performance = pe.Constraint(expr=m.variable_temps[7] <= 323) - #m.ineq1 = pe.Constraint(expr=-0.67*m.qc + m.uncertain_temps[3] - 350 <= 0) - #m.ineq2 = pe.Constraint(expr=-m.uncertain_temps[5] - 0.75*m.uncertain_temps[1] + 0.5*m.qc - m.uncertain_temps[3] + 1388.5 <= 0) - #m.ineq3 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] + 2044 <= 0) - #m.ineq4 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] - 2*m.uncertain_temps[8] + 2830 <= 0) - #m.ineq5 = pe.Constraint(expr=m.uncertain_temps[5] + 1.5*m.uncertain_temps[1] - m.qc + 2*m.uncertain_temps[3] + 3*m.uncertain_temps[8] - 3153 <= 0) + # m.ineq1 = pe.Constraint(expr=-0.67*m.qc + m.uncertain_temps[3] - 350 <= 0) + # m.ineq2 = pe.Constraint(expr=-m.uncertain_temps[5] - 0.75*m.uncertain_temps[1] + 0.5*m.qc - m.uncertain_temps[3] + 1388.5 <= 0) + # m.ineq3 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] + 2044 <= 0) + # m.ineq4 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] - 2*m.uncertain_temps[8] + 2830 <= 0) + # m.ineq5 = pe.Constraint(expr=m.uncertain_temps[5] + 1.5*m.uncertain_temps[1] - m.qc + 2*m.uncertain_temps[3] + 3*m.uncertain_temps[8] - 3153 <= 0) return m, nominal_values, param_bounds @@ -76,13 +83,13 @@ def main(method): config.feasibility_tol = 1e-6 config.terminate_early = False config.method = method - config.minlp_solver = pe.SolverFactory('gurobi_direct') - config.sampling_config.solver = pe.SolverFactory('appsi_gurobi') + config.minlp_solver = pe.SolverFactory("gurobi_direct") + config.sampling_config.solver = pe.SolverFactory("appsi_gurobi") config.sampling_config.strategy = flexibility.SamplingStrategy.lhs config.sampling_config.num_points = 200 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() - config.decision_rule_config.solver = pe.SolverFactory('appsi_gurobi') + config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 @@ -96,18 +103,24 @@ def main(method): # param_nominal_values=nominal_values, param_bounds=param_bounds, # controls=[m.qc], valid_var_bounds=var_bounds, config=config) # print(results) - results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, param_bounds=param_bounds, - controls=[m.qc], valid_var_bounds=var_bounds, config=config) + results = flexibility.solve_flex_index( + m=m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + valid_var_bounds=var_bounds, + config=config, + ) print(results) -if __name__ == '__main__': - print('\n\n********************Active Constraint**************************') +if __name__ == "__main__": + print("\n\n********************Active Constraint**************************") main(flexibility.FlexTestMethod.active_constraint) - print('\n\n********************Linear Decision Rule**************************') + print("\n\n********************Linear Decision Rule**************************") main(flexibility.FlexTestMethod.linear_decision_rule) - print('\n\n********************Vertex Enumeration**************************') + print("\n\n********************Vertex Enumeration**************************") main(flexibility.FlexTestMethod.vertex_enumeration) - print('\n\n********************ReLU Decision rule**************************') + print("\n\n********************ReLU Decision rule**************************") main(flexibility.FlexTestMethod.relu_decision_rule) diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index 9644c9c456..07d278001a 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -6,9 +6,11 @@ from typing import Tuple, MutableMapping, Union -def create_model() -> Tuple[_BlockData, - MutableMapping[_ParamData, float], - MutableMapping[_ParamData, Tuple[float, float]]]: +def create_model() -> Tuple[ + _BlockData, + MutableMapping[_ParamData, float], + MutableMapping[_ParamData, Tuple[float, float]], +]: """ This example is from @@ -17,20 +19,22 @@ def create_model() -> Tuple[_BlockData, 11(6), 675-693. """ - print("""This example is based off of \n\n + print( + """This example is based off of \n\n Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for flexibility analysis in chemical processes. Computers & Chemical Engineering, - 11(6), 675-693.\n\n""") + 11(6), 675-693.\n\n""" + ) m = pe.ConcreteModel() m.qc = pe.Var() m.fh1 = pe.Param(mutable=True, initialize=1.4) - m.f1 = pe.Constraint(expr=-25 + m.qc*((1/m.fh1) - 0.5) + 10/m.fh1 <= 0) - m.f2 = pe.Constraint(expr=-190 + 10/m.fh1 + m.qc/m.fh1 <= 0) - m.f3 = pe.Constraint(expr=-270 + 250/m.fh1 + m.qc/m.fh1 <= 0) - m.f4 = pe.Constraint(expr=260 - 250/m.fh1 - m.qc/m.fh1 <= 0) + m.f1 = pe.Constraint(expr=-25 + m.qc * ((1 / m.fh1) - 0.5) + 10 / m.fh1 <= 0) + m.f2 = pe.Constraint(expr=-190 + 10 / m.fh1 + m.qc / m.fh1 <= 0) + m.f3 = pe.Constraint(expr=-270 + 250 / m.fh1 + m.qc / m.fh1 <= 0) + m.f4 = pe.Constraint(expr=260 - 250 / m.fh1 - m.qc / m.fh1 <= 0) nominal_values = pe.ComponentMap() nominal_values[m.fh1] = 1 @@ -54,13 +58,13 @@ def main(method): config.feasibility_tol = 1e-6 config.terminate_early = False config.method = method - config.minlp_solver = pe.SolverFactory('scip') - config.sampling_config.solver = pe.SolverFactory('gurobi_direct') + config.minlp_solver = pe.SolverFactory("scip") + config.sampling_config.solver = pe.SolverFactory("gurobi_direct") config.sampling_config.strategy = flexibility.SamplingStrategy.lhs config.sampling_config.num_points = 100 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() - config.decision_rule_config.solver = pe.SolverFactory('appsi_gurobi') + config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 @@ -73,18 +77,24 @@ def main(method): # param_nominal_values=nominal_values, param_bounds=param_bounds, # controls=[m.qc], valid_var_bounds=var_bounds, config=config) # print(results) - results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, param_bounds=param_bounds, - controls=[m.qc], valid_var_bounds=var_bounds, config=config) + results = flexibility.solve_flex_index( + m=m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + valid_var_bounds=var_bounds, + config=config, + ) print(results) -if __name__ == '__main__': - print('\n\n********************Active Constraint**************************') +if __name__ == "__main__": + print("\n\n********************Active Constraint**************************") main(flexibility.FlexTestMethod.active_constraint) - print('\n\n********************Linear Decision Rule**************************') + print("\n\n********************Linear Decision Rule**************************") main(flexibility.FlexTestMethod.linear_decision_rule) - print('\n\n********************Vertex Enumeration**************************') + print("\n\n********************Vertex Enumeration**************************") main(flexibility.FlexTestMethod.vertex_enumeration) - print('\n\n********************ReLU Decision rule**************************') + print("\n\n********************ReLU Decision rule**************************") main(flexibility.FlexTestMethod.relu_decision_rule) diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index 13f7fe8df1..d2a1684ce8 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -4,7 +4,13 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from flexibility.sampling import SamplingStrategy -from .flextest import FlexTestConfig, solve_flextest, FlexTestMethod, FlexTestTermination, FlexTest +from .flextest import ( + FlexTestConfig, + solve_flextest, + FlexTestMethod, + FlexTestTermination, + FlexTest, +) import math import logging @@ -16,41 +22,56 @@ def _get_param_bounds(orig_param_bounds, nominal_values, flex_index): tmp_param_bounds = pe.ComponentMap() for p, (p_lb, p_ub) in orig_param_bounds.items(): p_nom = nominal_values[p] - tmp_param_bounds[p] = (p_nom - (p_nom - p_lb) * flex_index, p_nom + (p_ub - p_nom) * flex_index) + tmp_param_bounds[p] = ( + p_nom - (p_nom - p_lb) * flex_index, + p_nom + (p_ub - p_nom) * flex_index, + ) return tmp_param_bounds -def _add_table_row(log_level, outer_iter, flex_index_lower, flex_index_upper, fi_lb_max_viol, fi_ub_max_viol): +def _add_table_row( + log_level, + outer_iter, + flex_index_lower, + flex_index_upper, + fi_lb_max_viol, + fi_ub_max_viol, +): if flex_index_lower is None: lb_str = str(flex_index_lower) else: - lb_str = f'{flex_index_lower:12.3e}' + lb_str = f"{flex_index_lower:12.3e}" if flex_index_upper is None: ub_str = str(flex_index_upper) else: - ub_str = f'{flex_index_upper:12.3e}' + ub_str = f"{flex_index_upper:12.3e}" if fi_lb_max_viol is None: lb_mv_str = str(fi_lb_max_viol) else: - lb_mv_str = f'{fi_lb_max_viol:12.3e}' + lb_mv_str = f"{fi_lb_max_viol:12.3e}" if fi_ub_max_viol is None: ub_mv_str = str(fi_ub_max_viol) else: - ub_mv_str = f'{fi_ub_max_viol:12.3e}' - logger.log(log_level, f"{outer_iter:<12}{lb_str:<12}{ub_str:<12}{lb_mv_str:<15}{ub_mv_str:<15}") - - -def solve_flex_index(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], - in_place: bool = False, - cap_index_at_1: bool = True, - reconstruct_decision_rule: Optional[bool] = None, - config: Optional[FlexTestConfig] = None, - log_level: int = logging.INFO) -> float: + ub_mv_str = f"{fi_ub_max_viol:12.3e}" + logger.log( + log_level, + f"{outer_iter:<12}{lb_str:<12}{ub_str:<12}{lb_mv_str:<15}{ub_mv_str:<15}", + ) + + +def solve_flex_index( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], + in_place: bool = False, + cap_index_at_1: bool = True, + reconstruct_decision_rule: Optional[bool] = None, + config: Optional[FlexTestConfig] = None, + log_level: int = logging.INFO, +) -> float: original_uncertain_params = uncertain_params original_param_nominal_values = param_nominal_values original_param_bounds = param_bounds @@ -59,19 +80,36 @@ def solve_flex_index(m: _BlockData, if not in_place: m = m.clone() uncertain_params = [m.find_component(i) for i in original_uncertain_params] - param_nominal_values = pe.ComponentMap((p, original_param_nominal_values[orig_p]) for orig_p, p in zip(original_uncertain_params, uncertain_params)) - param_bounds = pe.ComponentMap((p, original_param_bounds[orig_p]) for orig_p, p in zip(original_uncertain_params, uncertain_params)) + param_nominal_values = pe.ComponentMap( + (p, original_param_nominal_values[orig_p]) + for orig_p, p in zip(original_uncertain_params, uncertain_params) + ) + param_bounds = pe.ComponentMap( + (p, original_param_bounds[orig_p]) + for orig_p, p in zip(original_uncertain_params, uncertain_params) + ) controls = [m.find_component(i) for i in original_controls] - valid_var_bounds = pe.ComponentMap((m.find_component(v), bnds) for v, bnds in original_valid_var_bounds.items()) + valid_var_bounds = pe.ComponentMap( + (m.find_component(v), bnds) for v, bnds in original_valid_var_bounds.items() + ) - if config.method == FlexTestMethod.linear_decision_rule and reconstruct_decision_rule is None: + if ( + config.method == FlexTestMethod.linear_decision_rule + and reconstruct_decision_rule is None + ): reconstruct_decision_rule = True - elif config.method == FlexTestMethod.relu_decision_rule and reconstruct_decision_rule is None: + elif ( + config.method == FlexTestMethod.relu_decision_rule + and reconstruct_decision_rule is None + ): reconstruct_decision_rule = False elif reconstruct_decision_rule is None: reconstruct_decision_rule = False - logger.log(log_level, f"{'Iter':<12}{'FI LB':<12}{'FI UB':<12}{'FI LB Max Viol':<15}{'FI UB Max Viol':<15}") + logger.log( + log_level, + f"{'Iter':<12}{'FI LB':<12}{'FI UB':<12}{'FI LB Max Viol':<15}{'FI UB Max Viol':<15}", + ) fi_lb_max_viol = -math.inf fi_ub_max_viol = math.inf @@ -90,15 +128,24 @@ def solve_flex_index(m: _BlockData, nominal_config.sampling_config.strategy = SamplingStrategy.grid nominal_config.sampling_config.num_points = 1 nominal_config.sampling_config.enable_progress_bar = False - nominal_res = solve_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, - param_bounds=nominal_bounds, controls=controls, valid_var_bounds=valid_var_bounds, - in_place=False, config=nominal_config) + nominal_res = solve_flextest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=nominal_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + in_place=False, + config=nominal_config, + ) if nominal_res.termination != FlexTestTermination.proven_feasible: - raise RuntimeError('Nominal point is infeasible') + raise RuntimeError("Nominal point is infeasible") outer_iter += 1 fi_lb_max_viol = nominal_res.max_constraint_violation - _add_table_row(log_level, outer_iter, flex_index_lower, None, fi_lb_max_viol, fi_ub_max_viol) + _add_table_row( + log_level, outer_iter, flex_index_lower, None, fi_lb_max_viol, fi_ub_max_viol + ) flextest_config: FlexTestConfig = config() flextest_config.terminate_early = True @@ -109,41 +156,75 @@ def solve_flex_index(m: _BlockData, # Find an upper bound on the flexibility index (i.e., a point where the flextest fails) found_infeasible_point = False for _iter in range(10): - tmp_param_bounds = _get_param_bounds(param_bounds, param_nominal_values, flex_index_upper) - upper_res = solve_flextest(m=m, uncertain_params=uncertain_params, - param_nominal_values=param_nominal_values, - param_bounds=tmp_param_bounds, controls=controls, - valid_var_bounds=valid_var_bounds, - in_place=False, config=flextest_config) + tmp_param_bounds = _get_param_bounds( + param_bounds, param_nominal_values, flex_index_upper + ) + upper_res = solve_flextest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=tmp_param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + in_place=False, + config=flextest_config, + ) outer_iter += 1 if upper_res.termination == FlexTestTermination.found_infeasible_point: fi_ub_max_viol = upper_res.max_constraint_violation - _add_table_row(log_level, outer_iter, - flex_index_lower, flex_index_upper, - fi_lb_max_viol, fi_ub_max_viol) + _add_table_row( + log_level, + outer_iter, + flex_index_lower, + flex_index_upper, + fi_lb_max_viol, + fi_ub_max_viol, + ) found_infeasible_point = True break elif upper_res.termination == FlexTestTermination.proven_feasible: flex_index_lower = flex_index_upper flex_index_upper *= 2 fi_lb_max_viol = upper_res.max_constraint_violation - _add_table_row(log_level, outer_iter, flex_index_lower, None, fi_lb_max_viol, fi_ub_max_viol) + _add_table_row( + log_level, + outer_iter, + flex_index_lower, + None, + fi_lb_max_viol, + fi_ub_max_viol, + ) else: - raise RuntimeError('Unexpected termination from flexibility test') + raise RuntimeError("Unexpected termination from flexibility test") if not found_infeasible_point: - raise RuntimeError('Could not find an upper bound on the flexibility index') + raise RuntimeError("Could not find an upper bound on the flexibility index") - max_param_bounds = _get_param_bounds(param_bounds, param_nominal_values, flex_index_upper) + max_param_bounds = _get_param_bounds( + param_bounds, param_nominal_values, flex_index_upper + ) if cap_index_at_1: if reconstruct_decision_rule: - res = solve_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, - param_bounds=max_param_bounds, controls=controls, - valid_var_bounds=valid_var_bounds, in_place=False, config=flextest_config) + res = solve_flextest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=max_param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + in_place=False, + config=flextest_config, + ) else: - ft = FlexTest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, - max_param_bounds=max_param_bounds, controls=controls, valid_var_bounds=valid_var_bounds, - config=flextest_config) + ft = FlexTest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + max_param_bounds=max_param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + config=flextest_config, + ) res = ft.solve(max_param_bounds) outer_iter += 1 if res.termination == FlexTestTermination.proven_feasible: @@ -153,20 +234,42 @@ def solve_flex_index(m: _BlockData, elif res.termination == FlexTestTermination.found_infeasible_point: fi_ub_max_viol = res.max_constraint_violation else: - raise RuntimeError('Unexpected termination from flexibility test') - _add_table_row(log_level, outer_iter, flex_index_lower, flex_index_upper, fi_lb_max_viol, fi_ub_max_viol) + raise RuntimeError("Unexpected termination from flexibility test") + _add_table_row( + log_level, + outer_iter, + flex_index_lower, + flex_index_upper, + fi_lb_max_viol, + fi_ub_max_viol, + ) elif not reconstruct_decision_rule: - ft = FlexTest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, - max_param_bounds=max_param_bounds, controls=controls, valid_var_bounds=valid_var_bounds, - config=flextest_config) + ft = FlexTest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + max_param_bounds=max_param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + config=flextest_config, + ) while (flex_index_upper - flex_index_lower) > 1e-3: midpoint = 0.5 * (flex_index_lower + flex_index_upper) - tmp_param_bounds = _get_param_bounds(param_bounds, param_nominal_values, midpoint) + tmp_param_bounds = _get_param_bounds( + param_bounds, param_nominal_values, midpoint + ) if reconstruct_decision_rule: - res = solve_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, - param_bounds=tmp_param_bounds, controls=controls, - valid_var_bounds=valid_var_bounds, in_place=False, config=flextest_config) + res = solve_flextest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=tmp_param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + in_place=False, + config=flextest_config, + ) else: res = ft.solve(param_bounds=tmp_param_bounds) outer_iter += 1 @@ -177,7 +280,14 @@ def solve_flex_index(m: _BlockData, flex_index_upper = midpoint fi_ub_max_viol = res.max_constraint_violation else: - raise RuntimeError('Unexpected termination from flexibility test') - _add_table_row(log_level, outer_iter, flex_index_lower, flex_index_upper, fi_lb_max_viol, fi_ub_max_viol) + raise RuntimeError("Unexpected termination from flexibility test") + _add_table_row( + log_level, + outer_iter, + flex_index_lower, + flex_index_upper, + fi_lb_max_viol, + fi_ub_max_viol, + ) return flex_index_lower diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index dcd3f3e928..ec984175d1 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -3,7 +3,12 @@ from pyomo.core.base.block import _BlockData from coramin.utils import get_objective import pyomo.environ as pe -from .var_utils import get_used_unfixed_variables, BoundsManager, _remove_var_bounds, _apply_var_bounds +from .var_utils import ( + get_used_unfixed_variables, + BoundsManager, + _remove_var_bounds, + _apply_var_bounds, +) from .indices import _VarIndex, _ConIndex from .uncertain_params import _replace_uncertain_params from .inner_problem import _build_inner_problem @@ -14,9 +19,20 @@ from pyomo.core.base.param import _ParamData from flexibility.decision_rules.linear_dr import construct_linear_decision_rule from flexibility.decision_rules.relu_dr import construct_relu_decision_rule -from flexibility.sampling import SamplingStrategy, perform_sampling, SamplingConfig, _perform_sampling +from flexibility.sampling import ( + SamplingStrategy, + perform_sampling, + SamplingConfig, + _perform_sampling, +) import enum -from pyomo.common.config import ConfigDict, ConfigValue, PositiveFloat, InEnum, MarkImmutable +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + PositiveFloat, + InEnum, + MarkImmutable, +) from flexibility.scaling_check import _get_longest_name @@ -33,16 +49,42 @@ class FlexTestMethod(enum.Enum): class FlexTestConfig(ConfigDict): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): - super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, - visibility=visibility) - self.feasibility_tol: float = self.declare('feasibility_tol', ConfigValue(domain=PositiveFloat, default=1e-6)) - self.terminate_early: bool = self.declare('terminate_early', ConfigValue(domain=bool, default=False)) - self.method: FlexTestMethod = self.declare('method', ConfigValue(domain=InEnum(FlexTestMethod), - default=FlexTestMethod.active_constraint)) - self.minlp_solver = self.declare('minlp_solver', ConfigValue(default=pe.SolverFactory('scip'))) - self.sampling_config: SamplingConfig = self.declare('sampling_config', SamplingConfig()) - self.decision_rule_config = self.declare('decision_rule_config', ConfigValue(default=None)) + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.feasibility_tol: float = self.declare( + "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-6) + ) + self.terminate_early: bool = self.declare( + "terminate_early", ConfigValue(domain=bool, default=False) + ) + self.method: FlexTestMethod = self.declare( + "method", + ConfigValue( + domain=InEnum(FlexTestMethod), default=FlexTestMethod.active_constraint + ), + ) + self.minlp_solver = self.declare( + "minlp_solver", ConfigValue(default=pe.SolverFactory("scip")) + ) + self.sampling_config: SamplingConfig = self.declare( + "sampling_config", SamplingConfig() + ) + self.decision_rule_config = self.declare( + "decision_rule_config", ConfigValue(default=None) + ) class FlexTestTermination(enum.Enum): @@ -55,50 +97,67 @@ class FlexTestResults(object): def __init__(self): self.termination = FlexTestTermination.uncertain self.max_constraint_violation: Optional[float] = None - self.unc_param_values_at_max_violation: \ - Optional[MutableMapping[Union[_GeneralVarData, _ParamData], float]] = None + self.unc_param_values_at_max_violation: Optional[ + MutableMapping[Union[_GeneralVarData, _ParamData], float] + ] = None def __str__(self): - s = f'Termination: {self.termination}\n' - s += f'Maximum constraint violation: {self.max_constraint_violation}\n' + s = f"Termination: {self.termination}\n" + s += f"Maximum constraint violation: {self.max_constraint_violation}\n" if self.unc_param_values_at_max_violation is not None: - s += f'Uncertain parameter values at maximum constraint violation: \n' - longest_param_name = _get_longest_name(self.unc_param_values_at_max_violation.keys()) + s += f"Uncertain parameter values at maximum constraint violation: \n" + longest_param_name = _get_longest_name( + self.unc_param_values_at_max_violation.keys() + ) s += f'{"Param":<{longest_param_name + 5}}{"Value":>12}\n' for k, v in self.unc_param_values_at_max_violation.items(): - s += f'{str(k):<{longest_param_name + 5}}{v:>12.2e}\n' + s += f"{str(k):<{longest_param_name + 5}}{v:>12.2e}\n" return s def _get_dof(m: _BlockData): - n_cons = len(set(i for i in m.component_data_objects(pe.Constraint, active=True, descend_into=True) if i.equality)) + n_cons = len( + set( + i + for i in m.component_data_objects( + pe.Constraint, active=True, descend_into=True + ) + if i.equality + ) + ) n_vars = len(get_used_unfixed_variables(m)) return n_vars - n_cons dr_construction_map = dict() -dr_construction_map[FlexTestMethod.linear_decision_rule] = construct_linear_decision_rule +dr_construction_map[ + FlexTestMethod.linear_decision_rule +] = construct_linear_decision_rule dr_construction_map[FlexTestMethod.relu_decision_rule] = construct_relu_decision_rule -def build_flextest_with_dr(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - config: FlexTestConfig): +def build_flextest_with_dr( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: FlexTestConfig, +): # enforce_equalities must be true for this method, or the resulting # problem will be unbounded; the key is degrees of freedom - + # perform sampling - tmp = perform_sampling(m=m, - uncertain_params=uncertain_params, - param_nominal_values=param_nominal_values, - param_bounds=param_bounds, - controls=controls, - in_place=False, - config=config.sampling_config) + tmp = perform_sampling( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + in_place=False, + config=config.sampling_config, + ) _sample_points, max_violation_values, control_values = tmp @@ -109,7 +168,9 @@ def build_flextest_with_dr(m: _BlockData, v.fix() # these should be fixed before we check the degrees of freedom if _get_dof(m) != len(controls): - raise ValueError('The number of controls must match the number of degrees of freedom') + raise ValueError( + "The number of controls must match the number of degrees of freedom" + ) # check the scaling of the model # this has to be done with valid_var_bounds (original bounds removed) to ensure we have @@ -131,26 +192,37 @@ def build_flextest_with_dr(m: _BlockData, p_var = m.unc_param_vars[ndx] sample_points[p_var] = _sample_points[p] - dr = dr_construction_map[config.method](input_vals=sample_points, output_vals=control_values, - config=config.decision_rule_config) - - _build_inner_problem(m=m, enforce_equalities=True, unique_constraint_violations=True, - valid_var_bounds=valid_var_bounds) + dr = dr_construction_map[config.method]( + input_vals=sample_points, + output_vals=control_values, + config=config.decision_rule_config, + ) + + _build_inner_problem( + m=m, + enforce_equalities=True, + unique_constraint_violations=True, + valid_var_bounds=valid_var_bounds, + ) _apply_var_bounds(valid_var_bounds) m.decision_rule = dr obj = get_objective(m) obj.deactivate() - - m.max_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation, sense=pe.maximize) + + m.max_constraint_violation_obj = pe.Objective( + expr=m.max_constraint_violation, sense=pe.maximize + ) -def build_active_constraint_flextest(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - default_M=None): +def build_active_constraint_flextest( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + default_M=None, +): enforce_equalities = False _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) for v in m.unc_param_vars.values(): @@ -163,10 +235,18 @@ def build_active_constraint_flextest(m: _BlockData, check_bounds_and_scaling(m) bounds_manager.pop_bounds() - orig_equality_cons = [c for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True) if c.equality] - - _build_inner_problem(m=m, enforce_equalities=enforce_equalities, unique_constraint_violations=False, - valid_var_bounds=valid_var_bounds) + orig_equality_cons = [ + c + for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True) + if c.equality + ] + + _build_inner_problem( + m=m, + enforce_equalities=enforce_equalities, + unique_constraint_violations=False, + valid_var_bounds=valid_var_bounds, + ) for v in m.unc_param_vars.values(): v.fix() @@ -174,37 +254,48 @@ def build_active_constraint_flextest(m: _BlockData, for v in m.unc_param_vars.values(): v.unfix() - add_kkt_with_milp_complementarity_conditions(m=m, - uncertain_params=list(m.unc_param_vars.values()), - valid_var_bounds=valid_var_bounds, - default_M=default_M) + add_kkt_with_milp_complementarity_conditions( + m=m, + uncertain_params=list(m.unc_param_vars.values()), + valid_var_bounds=valid_var_bounds, + default_M=default_M, + ) m.equality_cuts = pe.ConstraintList() max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] for c in orig_equality_cons: - key1 = _ConIndex(c, 'lb') - key2 = _ConIndex(m.ineq_violation_cons[key1], 'ub') + key1 = _ConIndex(c, "lb") + key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") y1 = m.active_indicator[key2] - key1 = _ConIndex(c, 'ub') - key2 = _ConIndex(m.ineq_violation_cons[key1], 'ub') + key1 = _ConIndex(c, "ub") + key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") y2 = m.active_indicator[key2] - m.equality_cuts.add(m.max_constraint_violation <= (1 - y1*y2) * max_viol_ub) - m.equality_cuts.add(m.max_constraint_violation >= (1 - y1*y2) * max_viol_lb) + m.equality_cuts.add(m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub) + m.equality_cuts.add(m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb) m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) - m.max_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation, sense=pe.maximize) - - -def _solve_flextest_active_constraint(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - config: Optional[FlexTestConfig] = None) -> FlexTestResults: - build_active_constraint_flextest(m=m, uncertain_params=uncertain_params, param_nominal_values=param_nominal_values, - param_bounds=param_bounds, valid_var_bounds=valid_var_bounds) + m.max_constraint_violation_obj = pe.Objective( + expr=m.max_constraint_violation, sense=pe.maximize + ) + + +def _solve_flextest_active_constraint( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None, +) -> FlexTestResults: + build_active_constraint_flextest( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + valid_var_bounds=valid_var_bounds, + ) opt = config.minlp_solver res = opt.solve(m) pe.assert_optimal_termination(res) @@ -221,20 +312,24 @@ def _solve_flextest_active_constraint(m: _BlockData, return results -def _solve_flextest_decision_rule(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - config: Optional[FlexTestConfig] = None) -> FlexTestResults: - build_flextest_with_dr(m=m, - uncertain_params=uncertain_params, - param_nominal_values=param_nominal_values, - param_bounds=param_bounds, - controls=controls, - valid_var_bounds=valid_var_bounds, - config=config) +def _solve_flextest_decision_rule( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None, +) -> FlexTestResults: + build_flextest_with_dr( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + config=config, + ) opt = config.minlp_solver res = opt.solve(m) pe.assert_optimal_termination(res) @@ -251,20 +346,24 @@ def _solve_flextest_decision_rule(m: _BlockData, return results -def _solve_flextest_sampling(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - config: Optional[FlexTestConfig] = None) -> FlexTestResults: - tmp = perform_sampling(m=m, - uncertain_params=uncertain_params, - param_nominal_values=param_nominal_values, - param_bounds=param_bounds, - controls=controls, - in_place=True, - config=config.sampling_config) +def _solve_flextest_sampling( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None, +) -> FlexTestResults: + tmp = perform_sampling( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + in_place=True, + config=config.sampling_config, + ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = int(np.argmax(max_violation_values)) @@ -280,23 +379,27 @@ def _solve_flextest_sampling(m: _BlockData, return results -def _solve_flextest_vertex_enumeration(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - config: Optional[FlexTestConfig] = None) -> FlexTestResults: +def _solve_flextest_vertex_enumeration( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + config: Optional[FlexTestConfig] = None, +) -> FlexTestResults: config: FlexTestConfig = config() config.sampling_config.num_points = 2 config.sampling_config.strategy = SamplingStrategy.grid - tmp = perform_sampling(m=m, - uncertain_params=uncertain_params, - param_nominal_values=param_nominal_values, - param_bounds=param_bounds, - controls=controls, - in_place=True, - config=config.sampling_config) + tmp = perform_sampling( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + in_place=True, + config=config.sampling_config, + ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = int(np.argmax(max_violation_values)) @@ -320,14 +423,16 @@ def _solve_flextest_vertex_enumeration(m: _BlockData, _flextest_map[FlexTestMethod.relu_decision_rule] = _solve_flextest_decision_rule -def solve_flextest(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - in_place: bool = False, - config: Optional[FlexTestConfig] = None) -> FlexTestResults: +def solve_flextest( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + in_place: bool = False, + config: Optional[FlexTestConfig] = None, +) -> FlexTestResults: if config is None: config = FlexTestConfig() @@ -340,19 +445,27 @@ def solve_flextest(m: _BlockData, if not in_place: m = m.clone() uncertain_params = [m.find_component(i) for i in original_uncertain_params] - param_nominal_values = pe.ComponentMap((p, original_param_nominal_values[orig_p]) - for orig_p, p in zip(original_uncertain_params, uncertain_params)) - param_bounds = pe.ComponentMap((p, original_param_bounds[orig_p]) - for orig_p, p in zip(original_uncertain_params, uncertain_params)) + param_nominal_values = pe.ComponentMap( + (p, original_param_nominal_values[orig_p]) + for orig_p, p in zip(original_uncertain_params, uncertain_params) + ) + param_bounds = pe.ComponentMap( + (p, original_param_bounds[orig_p]) + for orig_p, p in zip(original_uncertain_params, uncertain_params) + ) controls = [m.find_component(i) for i in original_controls] - valid_var_bounds = pe.ComponentMap((m.find_component(v), bnds) for v, bnds in original_valid_var_bounds.items()) - results = _flextest_map[config.method](m=m, - uncertain_params=uncertain_params, - param_nominal_values=param_nominal_values, - param_bounds=param_bounds, - controls=controls, - valid_var_bounds=valid_var_bounds, - config=config) + valid_var_bounds = pe.ComponentMap( + (m.find_component(v), bnds) for v, bnds in original_valid_var_bounds.items() + ) + results = _flextest_map[config.method]( + m=m, + uncertain_params=uncertain_params, + param_nominal_values=param_nominal_values, + param_bounds=param_bounds, + controls=controls, + valid_var_bounds=valid_var_bounds, + config=config, + ) if not in_place: unc_param_values = pe.ComponentMap() for v, val in results.unc_param_values_at_max_violation.items(): @@ -362,40 +475,50 @@ def solve_flextest(m: _BlockData, class FlexTest(object): - def __init__(self, - m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - max_param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], - controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], - config: Optional[FlexTestConfig] = None): + def __init__( + self, + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + max_param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + controls: Sequence[_GeneralVarData], + valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], + config: Optional[FlexTestConfig] = None, + ): if config is None: self.config: FlexTestConfig = FlexTestConfig() else: self.config: FlexTestConfig = config() - MarkImmutable(self.config.get('method')) + MarkImmutable(self.config.get("method")) if self.config.method == FlexTestMethod.vertex_enumeration: self.config.sampling_config.strategy = SamplingStrategy.grid self.config.sampling_config.num_points = 2 - MarkImmutable(self.config.sampling_config.get('strategy')) - MarkImmutable(self.config.sampling_config.get('num_points')) + MarkImmutable(self.config.sampling_config.get("strategy")) + MarkImmutable(self.config.sampling_config.get("num_points")) self._original_model = m self._model = m.clone() m = self._model self._uncertain_params = [m.find_component(i) for i in uncertain_params] - self._param_nominal_values = pe.ComponentMap((p, param_nominal_values[orig_p]) - for orig_p, p in zip(uncertain_params, self._uncertain_params)) - self._max_param_bounds = pe.ComponentMap((p, max_param_bounds[orig_p]) - for orig_p, p in zip(uncertain_params, self._uncertain_params)) + self._param_nominal_values = pe.ComponentMap( + (p, param_nominal_values[orig_p]) + for orig_p, p in zip(uncertain_params, self._uncertain_params) + ) + self._max_param_bounds = pe.ComponentMap( + (p, max_param_bounds[orig_p]) + for orig_p, p in zip(uncertain_params, self._uncertain_params) + ) self._controls = [m.find_component(i) for i in controls] - self._valid_var_bounds = pe.ComponentMap((m.find_component(v), bnds) for v, bnds in valid_var_bounds.items()) + self._valid_var_bounds = pe.ComponentMap( + (m.find_component(v), bnds) for v, bnds in valid_var_bounds.items() + ) - self._orig_param_clone_param_map = pe.ComponentMap((i, j) for i, j in zip(uncertain_params, - self._uncertain_params)) - self._clone_param_orig_param_map = pe.ComponentMap((i, j) for i, j in zip(self._uncertain_params, - uncertain_params)) + self._orig_param_clone_param_map = pe.ComponentMap( + (i, j) for i, j in zip(uncertain_params, self._uncertain_params) + ) + self._clone_param_orig_param_map = pe.ComponentMap( + (i, j) for i, j in zip(self._uncertain_params, uncertain_params) + ) assert self.config.method in FlexTestMethod if self.config.method == FlexTestMethod.active_constraint: @@ -404,12 +527,17 @@ def __init__(self, self._build_flextest_with_dr() elif self.config.method == FlexTestMethod.relu_decision_rule: self._build_flextest_with_dr() - elif self.config.method in {FlexTestMethod.sampling, FlexTestMethod.vertex_enumeration}: + elif self.config.method in { + FlexTestMethod.sampling, + FlexTestMethod.vertex_enumeration, + }: self._build_sampling() else: - raise ValueError(f'Unrecognized method: {self.config.method}') + raise ValueError(f"Unrecognized method: {self.config.method}") - def _set_param_bounds(self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]]): + def _set_param_bounds( + self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]] + ): for orig_p, clone_p in self._orig_param_clone_param_map.items(): p_lb, p_ub = param_bounds[orig_p] ndx = _VarIndex(clone_p, None) @@ -418,33 +546,42 @@ def _set_param_bounds(self, param_bounds: Mapping[Union[_GeneralVarData, _ParamD p_var.setub(p_ub) def _build_active_constraint_model(self): - build_active_constraint_flextest(m=self._model, - uncertain_params=self._uncertain_params, - param_nominal_values=self._param_nominal_values, - param_bounds=self._max_param_bounds, - valid_var_bounds=self._valid_var_bounds) + build_active_constraint_flextest( + m=self._model, + uncertain_params=self._uncertain_params, + param_nominal_values=self._param_nominal_values, + param_bounds=self._max_param_bounds, + valid_var_bounds=self._valid_var_bounds, + ) def _build_flextest_with_dr(self): - build_flextest_with_dr(m=self._model, - uncertain_params=self._uncertain_params, - param_nominal_values=self._param_nominal_values, - param_bounds=self._max_param_bounds, - controls=self._controls, - valid_var_bounds=self._valid_var_bounds, - config=self.config) + build_flextest_with_dr( + m=self._model, + uncertain_params=self._uncertain_params, + param_nominal_values=self._param_nominal_values, + param_bounds=self._max_param_bounds, + controls=self._controls, + valid_var_bounds=self._valid_var_bounds, + config=self.config, + ) def _build_sampling(self): - _replace_uncertain_params(m=self._model, - uncertain_params=self._uncertain_params, - param_nominal_values=self._param_nominal_values, - param_bounds=self._max_param_bounds) - _build_inner_problem(m=self._model, - enforce_equalities=True, - unique_constraint_violations=False, - valid_var_bounds=None) - - def _solve_maximization(self, param_bounds: Mapping[Union[_GeneralVarData, - _ParamData], Sequence[float]]) -> FlexTestResults: + _replace_uncertain_params( + m=self._model, + uncertain_params=self._uncertain_params, + param_nominal_values=self._param_nominal_values, + param_bounds=self._max_param_bounds, + ) + _build_inner_problem( + m=self._model, + enforce_equalities=True, + unique_constraint_violations=False, + valid_var_bounds=None, + ) + + def _solve_maximization( + self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]] + ) -> FlexTestResults: self._set_param_bounds(param_bounds=param_bounds) opt = self.config.minlp_solver @@ -459,17 +596,26 @@ def _solve_maximization(self, param_bounds: Mapping[Union[_GeneralVarData, results.termination = FlexTestTermination.proven_feasible results.unc_param_values_at_max_violation = pe.ComponentMap() for key, v in self._model.unc_param_vars.items(): - results.unc_param_values_at_max_violation[self._clone_param_orig_param_map[key.var]] = v.value + results.unc_param_values_at_max_violation[ + self._clone_param_orig_param_map[key.var] + ] = v.value return results - def _solve_sampling(self, param_bounds: Mapping[Union[_GeneralVarData, - _ParamData], Sequence[float]]) -> FlexTestResults: + def _solve_sampling( + self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]] + ) -> FlexTestResults: self._set_param_bounds(param_bounds=param_bounds) - tmp = _perform_sampling(m=self._model, uncertain_params=self._uncertain_params, - controls=self._controls, config=self.config.sampling_config) + tmp = _perform_sampling( + m=self._model, + uncertain_params=self._uncertain_params, + controls=self._controls, + config=self.config.sampling_config, + ) sample_points, max_violation_values, control_values = tmp - sample_points = pe.ComponentMap((self._clone_param_orig_param_map[p], vals) - for p, vals in sample_points.items()) + sample_points = pe.ComponentMap( + (self._clone_param_orig_param_map[p], vals) + for p, vals in sample_points.items() + ) results = FlexTestResults() max_viol_ndx = int(np.argmax(max_violation_values)) @@ -483,11 +629,19 @@ def _solve_sampling(self, param_bounds: Mapping[Union[_GeneralVarData, results.unc_param_values_at_max_violation[key] = vals[max_viol_ndx] return results - def solve(self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]]) -> FlexTestResults: - if self.config.method in {FlexTestMethod.active_constraint, FlexTestMethod.linear_decision_rule, - FlexTestMethod.relu_decision_rule}: + def solve( + self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]] + ) -> FlexTestResults: + if self.config.method in { + FlexTestMethod.active_constraint, + FlexTestMethod.linear_decision_rule, + FlexTestMethod.relu_decision_rule, + }: return self._solve_maximization(param_bounds) - elif self.config.method in {FlexTestMethod.sampling, FlexTestMethod.vertex_enumeration}: + elif self.config.method in { + FlexTestMethod.sampling, + FlexTestMethod.vertex_enumeration, + }: return self._solve_sampling(param_bounds=param_bounds) else: - raise ValueError(f'Unrecognized method: {self.config.method}') + raise ValueError(f"Unrecognized method: {self.config.method}") diff --git a/idaes/apps/flexibility_analysis/indices.py b/idaes/apps/flexibility_analysis/indices.py index d359ca24cc..79e8639961 100644 --- a/idaes/apps/flexibility_analysis/indices.py +++ b/idaes/apps/flexibility_analysis/indices.py @@ -1,5 +1,3 @@ - - class _ConIndex(object): def __init__(self, con, bound): self._con = con diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index 6603b18039..75d5cfaa00 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -10,7 +10,9 @@ from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -def _get_bounds_on_max_constraint_violation(m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]]): +def _get_bounds_on_max_constraint_violation( + m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]] +): bounds_manager = BoundsManager(m) bounds_manager.save_bounds() @@ -38,7 +40,9 @@ def _get_bounds_on_max_constraint_violation(m: _BlockData, valid_var_bounds: Map return min_constraint_violation, max_constraint_violation -def _get_constraint_violation_bounds(m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]]) -> MutableMapping[_GeneralVarData, Tuple[float, float]]: +def _get_constraint_violation_bounds( + m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]] +) -> MutableMapping[_GeneralVarData, Tuple[float, float]]: bounds_manager = BoundsManager(m) bounds_manager.save_bounds() @@ -63,10 +67,12 @@ def _get_constraint_violation_bounds(m: _BlockData, valid_var_bounds: Mapping[_G return constraint_violation_bounds -def _build_inner_problem(m: _BlockData, - enforce_equalities: bool, - unique_constraint_violations: bool, - valid_var_bounds: Optional[MutableMapping[_GeneralVarData, Tuple[float, float]]]): +def _build_inner_problem( + m: _BlockData, + enforce_equalities: bool, + unique_constraint_violations: bool, + valid_var_bounds: Optional[MutableMapping[_GeneralVarData, Tuple[float, float]]], +): """ If enfoce equalities is True and unique_constraint_violations is False, then this function converts @@ -75,7 +81,7 @@ def _build_inner_problem(m: _BlockData, c(x) = 0 g(x) <= 0 - to + to min u s.t. @@ -89,7 +95,7 @@ def _build_inner_problem(m: _BlockData, c(x) = 0 g(x) <= 0 - to + to min u s.t. @@ -104,7 +110,7 @@ def _build_inner_problem(m: _BlockData, c(x) = 0 g(x) <= 0 - to + to min u s.t. @@ -135,41 +141,65 @@ def _build_inner_problem(m: _BlockData, if unique_constraint_violations: m.constraint_violation = pe.Var(m.ineq_violation_set) - for c in list(m.component_data_objects(pe.Constraint, descend_into=True, active=True)): + for c in list( + m.component_data_objects(pe.Constraint, descend_into=True, active=True) + ): if c.equality and enforce_equalities: continue if c.lower is not None: - key = _ConIndex(c, 'lb') + key = _ConIndex(c, "lb") m.ineq_violation_set.add(key) if unique_constraint_violations: - m.ineq_violation_cons[key] = (c.lower - c.body - m.constraint_violation[key], 0) + m.ineq_violation_cons[key] = ( + c.lower - c.body - m.constraint_violation[key], + 0, + ) else: - m.ineq_violation_cons[key] = (None, c.lower - c.body - m.max_constraint_violation, 0) + m.ineq_violation_cons[key] = ( + None, + c.lower - c.body - m.max_constraint_violation, + 0, + ) if c.upper is not None: - key = _ConIndex(c, 'ub') + key = _ConIndex(c, "ub") m.ineq_violation_set.add(key) if unique_constraint_violations: - m.ineq_violation_cons[key] = (c.body - c.upper - m.constraint_violation[key], 0) + m.ineq_violation_cons[key] = ( + c.body - c.upper - m.constraint_violation[key], + 0, + ) else: - m.ineq_violation_cons[key] = (None, c.body - c.upper - m.max_constraint_violation, 0) + m.ineq_violation_cons[key] = ( + None, + c.body - c.upper - m.max_constraint_violation, + 0, + ) for v in original_vars: if v.is_integer(): - raise ValueError('Original problem must be continuous') + raise ValueError("Original problem must be continuous") if v.lb is not None: - key = _VarIndex(v, 'lb') + key = _VarIndex(v, "lb") m.ineq_violation_set.add(key) if unique_constraint_violations: m.ineq_violation_cons[key] = (v.lb - v - m.constraint_violation[key], 0) else: - m.ineq_violation_cons[key] = (None, v.lb - v - m.max_constraint_violation, 0) + m.ineq_violation_cons[key] = ( + None, + v.lb - v - m.max_constraint_violation, + 0, + ) if v.ub is not None: - key = _VarIndex(v, 'ub') + key = _VarIndex(v, "ub") m.ineq_violation_set.add(key) if unique_constraint_violations: m.ineq_violation_cons[key] = (v - v.ub - m.constraint_violation[key], 0) else: - m.ineq_violation_cons[key] = (None, v - v.ub - m.max_constraint_violation, 0) + m.ineq_violation_cons[key] = ( + None, + v - v.ub - m.max_constraint_violation, + 0, + ) for key in m.ineq_violation_set: if isinstance(key, _ConIndex): @@ -187,11 +217,19 @@ def _build_inner_problem(m: _BlockData, # u_hat[i] = constraint_violation[i] * y[i] # and use mccormick for the last constraint - m.max_violation_selector = pe.Var(m.ineq_violation_set, domain=pe.Binary) # y[i] - m.one_max_violation = pe.Constraint(expr=sum(m.max_violation_selector.values()) == 1) + m.max_violation_selector = pe.Var( + m.ineq_violation_set, domain=pe.Binary + ) # y[i] + m.one_max_violation = pe.Constraint( + expr=sum(m.max_violation_selector.values()) == 1 + ) m.u_hat = pe.Var(m.ineq_violation_set) - m.max_violation_sum = pe.Constraint(expr=m.max_constraint_violation == sum(m.u_hat.values())) - constraint_violation_bounds = _get_constraint_violation_bounds(m, valid_var_bounds) + m.max_violation_sum = pe.Constraint( + expr=m.max_constraint_violation == sum(m.u_hat.values()) + ) + constraint_violation_bounds = _get_constraint_violation_bounds( + m, valid_var_bounds + ) m.u_hat_cons = pe.ConstraintList() for key in m.ineq_violation_set: violation_var = m.constraint_violation[key] @@ -202,11 +240,20 @@ def _build_inner_problem(m: _BlockData, m.u_hat_cons.add(m.u_hat[key] <= violation_var + viol_lb * y_i - viol_lb) m.u_hat_cons.add(m.u_hat[key] >= viol_ub * y_i + violation_var - viol_ub) valid_var_bounds.update(constraint_violation_bounds) - valid_var_bounds[m.max_constraint_violation] = (min(i[0] for i in constraint_violation_bounds.values()), max(i[1] for i in constraint_violation_bounds.values())) + valid_var_bounds[m.max_constraint_violation] = ( + min(i[0] for i in constraint_violation_bounds.values()), + max(i[1] for i in constraint_violation_bounds.values()), + ) for key in m.ineq_violation_set: valid_var_bounds[m.max_violation_selector[key]] = (0, 1) - valid_var_bounds[m.u_hat[key]] = (min(0.0, constraint_violation_bounds[m.constraint_violation[key]][0]), - max(0.0, constraint_violation_bounds[m.constraint_violation[key]][1])) + valid_var_bounds[m.u_hat[key]] = ( + min(0.0, constraint_violation_bounds[m.constraint_violation[key]][0]), + max(0.0, constraint_violation_bounds[m.constraint_violation[key]][1]), + ) else: if valid_var_bounds is not None: - valid_var_bounds[m.max_constraint_violation] = _get_bounds_on_max_constraint_violation(m=m, valid_var_bounds=valid_var_bounds) + valid_var_bounds[ + m.max_constraint_violation + ] = _get_bounds_on_max_constraint_violation( + m=m, valid_var_bounds=valid_var_bounds + ) diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index a3e7dc7467..a867fe66c9 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -3,7 +3,12 @@ from coramin.utils import get_objective from pyomo.core.base.block import _BlockData from pyomo.contrib.fbbt.fbbt import fbbt -from .var_utils import get_used_unfixed_variables, _remove_var_bounds, _apply_var_bounds, BoundsManager +from .var_utils import ( + get_used_unfixed_variables, + _remove_var_bounds, + _apply_var_bounds, + BoundsManager, +) from typing import Sequence, Mapping, Optional from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression @@ -35,27 +40,27 @@ def _add_grad_lag_constraints(m: _BlockData) -> _BlockData: for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): if c.equality: - key = _ConIndex(c, 'eq') + key = _ConIndex(c, "eq") m.duals_eq_set.add(key) lagrangian += m.duals_eq[key] * (c.body - c.upper) else: if c.upper is not None: - key = _ConIndex(c, 'ub') + key = _ConIndex(c, "ub") m.duals_ineq_set.add(key) lagrangian += m.duals_ineq[key] * (c.body - c.upper) if c.lower is not None: - key = _ConIndex(c, 'lb') + key = _ConIndex(c, "lb") m.duals_ineq_set.add(key) lagrangian += m.duals_ineq[key] * (c.lower - c.body) for v in primal_vars: assert v.is_continuous() if v.ub is not None: - key = _VarIndex(v, 'ub') + key = _VarIndex(v, "ub") m.duals_ineq_set.add(key) lagrangian += m.duals_ineq[key] * (v - v.ub) if v.lb is not None: - key = _VarIndex(v, 'lb') + key = _VarIndex(v, "lb") m.duals_ineq_set.add(key) lagrangian += m.duals_ineq[key] * (v.lb - v) @@ -90,10 +95,10 @@ def _introduce_inequality_slacks(m) -> _BlockData: lb = e.lb ub = e.ub - if bnd == 'ub': + if bnd == "ub": m.ineq_cons_with_slacks[key] = s + e - ub == 0 else: - assert bnd == 'lb' + assert bnd == "lb" m.ineq_cons_with_slacks[key] = s + lb - e == 0 for key in m.duals_ineq_set: @@ -115,15 +120,19 @@ def _do_fbbt(m, uncertain_params): fbbt(m) for p in uncertain_params: if p.lb > p_bounds[p][0] + 1e-6 or p.ub < p_bounds[p][1] - 1e-6: - raise RuntimeError('The bounds provided in valid_var_bounds were proven to ' - 'be invalid for some values of the uncertain parameters.') + raise RuntimeError( + "The bounds provided in valid_var_bounds were proven to " + "be invalid for some values of the uncertain parameters." + ) p.fix() -def add_kkt_with_milp_complementarity_conditions(m: _BlockData, - uncertain_params: Sequence[_GeneralVarData], - valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]], - default_M=None) -> _BlockData: +def add_kkt_with_milp_complementarity_conditions( + m: _BlockData, + uncertain_params: Sequence[_GeneralVarData], + valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]], + default_M=None, +) -> _BlockData: for v in uncertain_params: v.fix() @@ -146,22 +155,30 @@ def add_kkt_with_milp_complementarity_conditions(m: _BlockData, for key in m.duals_ineq_set: if m.duals_ineq[key].ub is None: if default_M is None: - raise RuntimeError(f'could not compute upper bound on multiplier for inequality {key}.') + raise RuntimeError( + f"could not compute upper bound on multiplier for inequality {key}." + ) else: dual_M = default_M else: dual_M = m.duals_ineq[key].ub if m.slacks[key].ub is None: if default_M is None: - raise RuntimeError(f'could not compute upper bound on slack for inequality {key}') + raise RuntimeError( + f"could not compute upper bound on slack for inequality {key}" + ) else: slack_M = default_M else: slack_M = m.slacks[key].ub m.dual_M[key].value = dual_M m.slack_M[key].value = slack_M - m.dual_ineq_0_if_not_active[key] = m.duals_ineq[key] <= m.active_indicator[key] * m.dual_M[key] - m.slack_0_if_active[key] = m.slacks[key] <= (1 - m.active_indicator[key]) * m.slack_M[key] + m.dual_ineq_0_if_not_active[key] = ( + m.duals_ineq[key] <= m.active_indicator[key] * m.dual_M[key] + ) + m.slack_0_if_active[key] = ( + m.slacks[key] <= (1 - m.active_indicator[key]) * m.slack_M[key] + ) for v in uncertain_params: v.unfix() diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 06edabbc62..a1fc1c2906 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -21,10 +21,14 @@ class SamplingStrategy(enum.Enum): lhs = enum.auto() -def _grid_sampling(uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int): +def _grid_sampling( + uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int +): uncertain_params_values = pe.ComponentMap() for p in uncertain_params: - uncertain_params_values[p] = list(set([float(i) for i in np.linspace(p.lb, p.ub, num_points)])) + uncertain_params_values[p] = list( + set([float(i) for i in np.linspace(p.lb, p.ub, num_points)]) + ) uncertain_params_values[p].sort() sample_points = pe.ComponentMap() @@ -40,13 +44,17 @@ def _grid_sampling(uncertain_params: Sequence[_GeneralVarData], num_points: int, return n_samples, sample_points -def _lhs_sampling(uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int): +def _lhs_sampling( + uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int +): lb_list = list() ub_list = list() for p in uncertain_params: lb_list.append(p.lb) ub_list.append(p.ub) - sampler = LatinHypercubeSampling([lb_list, ub_list], number_of_samples=num_points, sampling_type='creation') + sampler = LatinHypercubeSampling( + [lb_list, ub_list], number_of_samples=num_points, sampling_type="creation" + ) np.random.seed(seed) sample_array = sampler.sample_points() @@ -64,24 +72,49 @@ def _lhs_sampling(uncertain_params: Sequence[_GeneralVarData], num_points: int, class SamplingConfig(ConfigDict): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): - super().__init__(description=description, doc=doc, implicit=implicit, implicit_domain=implicit_domain, - visibility=visibility) - self.strategy: SamplingStrategy = self.declare('strategy', ConfigValue(domain=InEnum(SamplingStrategy), - default=SamplingStrategy.lhs)) - self.lhs_seed: int = self.declare('lhs_seed', ConfigValue(domain=int, default=0)) - self.solver = self.declare('solver', ConfigValue(default=pe.SolverFactory('appsi_ipopt'))) - self.num_points: int = self.declare('num_points', ConfigValue(domain=int, default=100)) - self.enable_progress_bar: bool = self.declare('enable_progress_bar', ConfigValue(domain=bool, default=True)) - - -def _perform_sampling(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - controls: Optional[Sequence[_GeneralVarData]], - config: SamplingConfig) -> Tuple[MutableMapping[Union[_GeneralVarData, _ParamData], - Sequence[float]], - Sequence[float], - MutableMapping[_GeneralVarData, Sequence[float]]]: + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.strategy: SamplingStrategy = self.declare( + "strategy", + ConfigValue(domain=InEnum(SamplingStrategy), default=SamplingStrategy.lhs), + ) + self.lhs_seed: int = self.declare( + "lhs_seed", ConfigValue(domain=int, default=0) + ) + self.solver = self.declare( + "solver", ConfigValue(default=pe.SolverFactory("appsi_ipopt")) + ) + self.num_points: int = self.declare( + "num_points", ConfigValue(domain=int, default=100) + ) + self.enable_progress_bar: bool = self.declare( + "enable_progress_bar", ConfigValue(domain=bool, default=True) + ) + + +def _perform_sampling( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + controls: Optional[Sequence[_GeneralVarData]], + config: SamplingConfig, +) -> Tuple[ + MutableMapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + Sequence[float], + MutableMapping[_GeneralVarData, Sequence[float]], +]: if isinstance(config.solver, PersistentSolver): using_persistent = True else: @@ -92,7 +125,9 @@ def _perform_sampling(m: _BlockData, ndx = _VarIndex(p, None) p_var = m.unc_param_vars[ndx] unc_param_vars.append(p_var) - n_samples, sample_points = _sample_strategy_map[config.strategy](unc_param_vars, config.num_points, config.lhs_seed) + n_samples, sample_points = _sample_strategy_map[config.strategy]( + unc_param_vars, config.num_points, config.lhs_seed + ) if using_persistent: config.solver.set_instance(m) @@ -111,7 +146,12 @@ def _perform_sampling(m: _BlockData, for v in controls: control_values[v] = list() - for sample_ndx in tqdm(list(range(n_samples)), ncols=100, desc='Sampling', disable=not config.enable_progress_bar): + for sample_ndx in tqdm( + list(range(n_samples)), + ncols=100, + desc="Sampling", + disable=not config.enable_progress_bar, + ): for p, p_vals in sample_points.items(): p.fix(p_vals[sample_ndx]) @@ -128,36 +168,57 @@ def _perform_sampling(m: _BlockData, if using_persistent: config.solver.update_config.set_value(original_update_config) - unc_param_var_to_unc_param_map = pe.ComponentMap(zip(unc_param_vars, uncertain_params)) - sample_points = pe.ComponentMap((unc_param_var_to_unc_param_map[p], vals) for p, vals in sample_points.items()) + unc_param_var_to_unc_param_map = pe.ComponentMap( + zip(unc_param_vars, uncertain_params) + ) + sample_points = pe.ComponentMap( + (unc_param_var_to_unc_param_map[p], vals) for p, vals in sample_points.items() + ) return sample_points, max_violation_values, control_values -def perform_sampling(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], - controls: Optional[Sequence[_GeneralVarData]], - in_place: bool, - config: SamplingConfig) -> Tuple[MutableMapping[Union[_GeneralVarData, _ParamData], - Sequence[float]], - Sequence[float], - MutableMapping[_GeneralVarData, Sequence[float]]]: +def perform_sampling( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + controls: Optional[Sequence[_GeneralVarData]], + in_place: bool, + config: SamplingConfig, +) -> Tuple[ + MutableMapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + Sequence[float], + MutableMapping[_GeneralVarData, Sequence[float]], +]: original_model = m if not in_place: m = m.clone() uncertain_params = [m.find_component(p) for p in uncertain_params] - param_nominal_values = pe.ComponentMap((m.find_component(p), val) for p, val in param_nominal_values.items()) - param_bounds = pe.ComponentMap((m.find_component(p), bnds) for p, bnds in param_bounds.items()) + param_nominal_values = pe.ComponentMap( + (m.find_component(p), val) for p, val in param_nominal_values.items() + ) + param_bounds = pe.ComponentMap( + (m.find_component(p), bnds) for p, bnds in param_bounds.items() + ) controls = [m.find_component(v) for v in controls] _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) - _build_inner_problem(m=m, enforce_equalities=True, unique_constraint_violations=False, valid_var_bounds=None) - sample_points, max_violation_values, control_values = _perform_sampling(m=m, uncertain_params=uncertain_params, - controls=controls, config=config) - - sample_points = pe.ComponentMap((original_model.find_component(p), vals) for p, vals in sample_points.items()) - control_values = pe.ComponentMap((original_model.find_component(v), vals) for v, vals in control_values.items()) + _build_inner_problem( + m=m, + enforce_equalities=True, + unique_constraint_violations=False, + valid_var_bounds=None, + ) + sample_points, max_violation_values, control_values = _perform_sampling( + m=m, uncertain_params=uncertain_params, controls=controls, config=config + ) + + sample_points = pe.ComponentMap( + (original_model.find_component(p), vals) for p, vals in sample_points.items() + ) + control_values = pe.ComponentMap( + (original_model.find_component(v), vals) for v, vals in control_values.items() + ) return sample_points, max_violation_values, control_values diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 57bbae478a..6ae5be9a58 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -12,10 +12,22 @@ def create_poly_model(): offset = 1.5 - m.obj = pe.Objective(expr=m.z**2) - m.c1 = pe.Constraint(expr=0.01*(m.z-offset)**4 - 0.05*(m.z-offset)**3 - (m.z-offset)**2 - (m.z-offset) - 10 + m.theta <= 0) - m.c2 = pe.Constraint(expr=(-0.02*m.theta - 14)*m.z + (1.66*m.theta - 100) <= 0) - m.c3 = pe.Constraint(expr=30*m.z - 50 - 4*m.theta + pe.exp(-0.2*m.theta + 1) <= 0) + m.obj = pe.Objective(expr=m.z ** 2) + m.c1 = pe.Constraint( + expr=0.01 * (m.z - offset) ** 4 + - 0.05 * (m.z - offset) ** 3 + - (m.z - offset) ** 2 + - (m.z - offset) + - 10 + + m.theta + <= 0 + ) + m.c2 = pe.Constraint( + expr=(-0.02 * m.theta - 14) * m.z + (1.66 * m.theta - 100) <= 0 + ) + m.c3 = pe.Constraint( + expr=30 * m.z - 50 - 4 * m.theta + pe.exp(-0.2 * m.theta + 1) <= 0 + ) nominal_values = pe.ComponentMap() nominal_values[m.theta] = 22.5 @@ -36,10 +48,9 @@ def create_hx_network_model(): m = pe.ConcreteModel() m.uncertain_temps_set = pe.Set(initialize=[1, 3, 5, 8]) - m.uncertain_temps = pe.Param(m.uncertain_temps_set, mutable=True, initialize={1: 620, - 3: 388, - 5: 583, - 8: 313}) + m.uncertain_temps = pe.Param( + m.uncertain_temps_set, mutable=True, initialize={1: 620, 3: 388, 5: 583, 8: 313} + ) nominal_values = pe.ComponentMap() for p in m.uncertain_temps.values(): nominal_values[p] = p.value @@ -53,9 +64,15 @@ def create_hx_network_model(): m.qc = pe.Var() m.balances = pe.Constraint([1, 2, 3, 4]) - m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * (m.variable_temps[4] - m.uncertain_temps[3]) - m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * (563 - m.variable_temps[4]) - m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * (393 - m.uncertain_temps[8]) + m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * ( + m.variable_temps[4] - m.uncertain_temps[3] + ) + m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * ( + 563 - m.variable_temps[4] + ) + m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * ( + 393 - m.uncertain_temps[8] + ) m.balances[4] = m.qc == 1.5 * (m.variable_temps[2] - 350) m.temp_approaches = pe.Constraint([1, 2, 3, 4]) @@ -74,12 +91,14 @@ def test_poly(self): m, nominal_values, param_bounds = create_poly_model() var_bounds = pe.ComponentMap() var_bounds[m.z] = (-20, 20) - build_active_constraint_flextest(m, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - valid_var_bounds=var_bounds) - opt = pe.SolverFactory('scip') + build_active_constraint_flextest( + m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + valid_var_bounds=var_bounds, + ) + opt = pe.SolverFactory("scip") res = opt.solve(m, tee=False) pe.assert_optimal_termination(res) self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) @@ -93,12 +112,14 @@ def test_hx_network(self): for v in m.variable_temps.values(): var_bounds[v] = (100, 1000) var_bounds[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) - build_active_constraint_flextest(m, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - valid_var_bounds=var_bounds) - opt = pe.SolverFactory('gurobi_direct') + build_active_constraint_flextest( + m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + valid_var_bounds=var_bounds, + ) + opt = pe.SolverFactory("gurobi_direct") res = opt.solve(m, tee=False) pe.assert_optimal_termination(res) self.assertAlmostEqual(m.max_constraint_violation.value, 4, 4) diff --git a/idaes/apps/flexibility_analysis/tests/test_indices.py b/idaes/apps/flexibility_analysis/tests/test_indices.py index 3b48a8f1f6..f1fc59f2c0 100644 --- a/idaes/apps/flexibility_analysis/tests/test_indices.py +++ b/idaes/apps/flexibility_analysis/tests/test_indices.py @@ -9,19 +9,19 @@ def test_var_index(self): m.x = pe.Var() m.y = pe.Var() - vi1 = _VarIndex(m.x, 'lb') + vi1 = _VarIndex(m.x, "lb") self.assertIs(vi1.var, m.x) - self.assertEqual(vi1.bound, 'lb') + self.assertEqual(vi1.bound, "lb") - vi2 = _VarIndex(m.x, 'lb') + vi2 = _VarIndex(m.x, "lb") self.assertEqual(vi1, vi2) self.assertEqual(hash(vi1), hash(vi2)) - vi2 = _VarIndex(m.x, 'ub') + vi2 = _VarIndex(m.x, "ub") self.assertNotEqual(vi1, vi2) - vi2 = _VarIndex(m.y, 'lb') + vi2 = _VarIndex(m.y, "lb") self.assertNotEqual(vi1, vi2) self.assertEqual(str(vi1), "('x', 'lb')") @@ -32,26 +32,26 @@ def test_con_index(self): m.x = pe.Var() m.y = pe.Var() m.c1 = pe.Constraint(expr=m.x == m.y) - m.c2 = pe.Constraint(expr=m.x == 2*m.y) + m.c2 = pe.Constraint(expr=m.x == 2 * m.y) - ci1 = _ConIndex(m.c1, 'lb') + ci1 = _ConIndex(m.c1, "lb") self.assertIs(ci1.con, m.c1) - self.assertEqual(ci1.bound, 'lb') + self.assertEqual(ci1.bound, "lb") - ci2 = _ConIndex(m.c1, 'lb') + ci2 = _ConIndex(m.c1, "lb") self.assertEqual(ci1, ci2) self.assertEqual(hash(ci1), hash(ci2)) - ci2 = _ConIndex(m.c1, 'ub') + ci2 = _ConIndex(m.c1, "ub") self.assertNotEqual(ci1, ci2) - ci2 = _ConIndex(m.c2, 'lb') + ci2 = _ConIndex(m.c2, "lb") self.assertNotEqual(ci1, ci2) self.assertEqual(str(ci1), "('c1', 'lb')") self.assertEqual(repr(ci1), "('c1', 'lb')") - vi1 = _VarIndex(m.x, 'lb') + vi1 = _VarIndex(m.x, "lb") self.assertNotEqual(ci1, vi1) self.assertNotEqual(vi1, ci1) diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index bb9c71cd1b..3865ddc654 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -12,10 +12,22 @@ def create_poly_model(): offset = 1.5 - m.obj = pe.Objective(expr=m.z**2) - m.c1 = pe.Constraint(expr=0.01*(m.z-offset)**4 - 0.05*(m.z-offset)**3 - (m.z-offset)**2 - (m.z-offset) - 10 + m.theta <= 0) - m.c2 = pe.Constraint(expr=(-0.02*m.theta - 14)*m.z + (1.66*m.theta - 100) <= 0) - m.c3 = pe.Constraint(expr=30*m.z - 50 - 4*m.theta + pe.exp(-0.2*m.theta + 1) <= 0) + m.obj = pe.Objective(expr=m.z ** 2) + m.c1 = pe.Constraint( + expr=0.01 * (m.z - offset) ** 4 + - 0.05 * (m.z - offset) ** 3 + - (m.z - offset) ** 2 + - (m.z - offset) + - 10 + + m.theta + <= 0 + ) + m.c2 = pe.Constraint( + expr=(-0.02 * m.theta - 14) * m.z + (1.66 * m.theta - 100) <= 0 + ) + m.c3 = pe.Constraint( + expr=30 * m.z - 50 - 4 * m.theta + pe.exp(-0.2 * m.theta + 1) <= 0 + ) nominal_values = pe.ComponentMap() nominal_values[m.theta] = 22.5 @@ -36,10 +48,9 @@ def create_hx_network_model(): m = pe.ConcreteModel() m.uncertain_temps_set = pe.Set(initialize=[1, 3, 5, 8]) - m.uncertain_temps = pe.Param(m.uncertain_temps_set, mutable=True, initialize={1: 620, - 3: 388, - 5: 583, - 8: 313}) + m.uncertain_temps = pe.Param( + m.uncertain_temps_set, mutable=True, initialize={1: 620, 3: 388, 5: 583, 8: 313} + ) nominal_values = pe.ComponentMap() for p in m.uncertain_temps.values(): nominal_values[p] = p.value @@ -53,9 +64,15 @@ def create_hx_network_model(): m.qc = pe.Var() m.balances = pe.Constraint([1, 2, 3, 4]) - m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * (m.variable_temps[4] - m.uncertain_temps[3]) - m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * (563 - m.variable_temps[4]) - m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * (393 - m.uncertain_temps[8]) + m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * ( + m.variable_temps[4] - m.uncertain_temps[3] + ) + m.balances[2] = m.uncertain_temps[5] - m.variable_temps[6] == 2 * ( + 563 - m.variable_temps[4] + ) + m.balances[3] = m.variable_temps[6] - m.variable_temps[7] == 3 * ( + 393 - m.uncertain_temps[8] + ) m.balances[4] = m.qc == 1.5 * (m.variable_temps[2] - 350) m.temp_approaches = pe.Constraint([1, 2, 3, 4]) @@ -72,15 +89,17 @@ def create_hx_network_model(): class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() - opt = pe.SolverFactory('scip') - tmp = perform_sampling(m, - num_points=5, - solver=opt, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - controls=[m.z], - in_place=True) + opt = pe.SolverFactory("scip") + tmp = perform_sampling( + m, + num_points=5, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.z], + in_place=True, + ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) self.assertAlmostEqual(max_violation_values[max_viol_ndx], -1.0142, 4) @@ -89,46 +108,52 @@ def test_poly(self): def test_hx_network(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory('gurobi_direct') - tmp = perform_sampling(m, - num_points=2, - solver=opt, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - controls=[m.qc], - in_place=True) + opt = pe.SolverFactory("gurobi_direct") + tmp = perform_sampling( + m, + num_points=2, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + in_place=True, + ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) def test_hx_network2(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory('gurobi_direct') - tmp = perform_sampling(m, - num_points=2, - solver=opt, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - enforce_equalities=False, - controls=[m.qc], - in_place=True) + opt = pe.SolverFactory("gurobi_direct") + tmp = perform_sampling( + m, + num_points=2, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + enforce_equalities=False, + controls=[m.qc], + in_place=True, + ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) self.assertAlmostEqual(max_violation_values[max_viol_ndx], 4, 4) def test_hx_network3(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory('appsi_gurobi') - tmp = perform_sampling(m, - num_points=2, - solver=opt, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - controls=[m.qc], - in_place=True) + opt = pe.SolverFactory("appsi_gurobi") + tmp = perform_sampling( + m, + num_points=2, + solver=opt, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + in_place=True, + ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) diff --git a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py index 647f13801a..914390cf43 100644 --- a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py +++ b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py @@ -21,8 +21,12 @@ def test_replace_mutable_parameter(self): param_bounds = pe.ComponentMap() param_bounds[m.p] = (1, 4) - _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, - param_bounds=param_bounds) + _replace_uncertain_params( + m=m, + uncertain_params=[m.p], + param_nominal_values=nominal_values, + param_bounds=param_bounds, + ) self.assertEqual(len(m.unc_cons), 4) @@ -36,24 +40,30 @@ def test_replace_mutable_parameter(self): c_ndx = _ConIndex(m.c1, None) self.assertEqual(m.unc_cons[c_ndx].lower, 0) self.assertEqual(m.unc_cons[c_ndx].upper, 0) - self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.x + m.unc_param_vars[v_ndx])) + self.assertTrue( + compare_expressions(m.unc_cons[c_ndx].body, m.x + m.unc_param_vars[v_ndx]) + ) self.assertFalse(m.c2.active) - c_ndx = _ConIndex(m.c2, 'ub') + c_ndx = _ConIndex(m.c2, "ub") self.assertEqual(m.unc_cons[c_ndx].lower, None) self.assertEqual(m.unc_cons[c_ndx].upper, 0) self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.y - m.x)) self.assertFalse(m.c3.active) - c_ndx = _ConIndex(m.c3, 'lb') + c_ndx = _ConIndex(m.c3, "lb") self.assertEqual(m.unc_cons[c_ndx].lower, -1) self.assertEqual(m.unc_cons[c_ndx].upper, None) - self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.y + m.unc_param_vars[v_ndx])) + self.assertTrue( + compare_expressions(m.unc_cons[c_ndx].body, m.y + m.unc_param_vars[v_ndx]) + ) - c_ndx = _ConIndex(m.c3, 'ub') + c_ndx = _ConIndex(m.c3, "ub") self.assertEqual(m.unc_cons[c_ndx].lower, None) self.assertEqual(m.unc_cons[c_ndx].upper, 1) - self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.y + m.unc_param_vars[v_ndx])) + self.assertTrue( + compare_expressions(m.unc_cons[c_ndx].body, m.y + m.unc_param_vars[v_ndx]) + ) def test_replace_var(self): m = pe.ConcreteModel() @@ -67,15 +77,21 @@ def test_replace_var(self): param_bounds = pe.ComponentMap() param_bounds[m.p] = (1, 4) - _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, - param_bounds=param_bounds) + _replace_uncertain_params( + m=m, + uncertain_params=[m.p], + param_nominal_values=nominal_values, + param_bounds=param_bounds, + ) self.assertFalse(m.c1.active) c_ndx = _ConIndex(m.c1, None) v_ndx = _VarIndex(m.p, None) self.assertEqual(m.unc_cons[c_ndx].lower, 0) self.assertEqual(m.unc_cons[c_ndx].upper, 0) - self.assertTrue(compare_expressions(m.unc_cons[c_ndx].body, m.x + m.unc_param_vars[v_ndx])) + self.assertTrue( + compare_expressions(m.unc_cons[c_ndx].body, m.x + m.unc_param_vars[v_ndx]) + ) self.assertEqual(m.unc_param_vars[v_ndx].lb, 1) self.assertEqual(m.unc_param_vars[v_ndx].ub, 4) self.assertEqual(m.unc_param_vars[v_ndx].value, 2.3) @@ -93,8 +109,12 @@ def test_non_constant_var(self): param_bounds[m.p] = (1, 4) with self.assertRaises(ValueError): - _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, - param_bounds=param_bounds) + _replace_uncertain_params( + m=m, + uncertain_params=[m.p], + param_nominal_values=nominal_values, + param_bounds=param_bounds, + ) def test_non_constant_var2(self): m = pe.ConcreteModel() @@ -109,5 +129,9 @@ def test_non_constant_var2(self): param_bounds[m.p] = (1, 4) with self.assertRaises(ValueError): - _replace_uncertain_params(m=m, uncertain_params=[m.p], param_nominal_values=nominal_values, - param_bounds=param_bounds) + _replace_uncertain_params( + m=m, + uncertain_params=[m.p], + param_nominal_values=nominal_values, + param_bounds=param_bounds, + ) diff --git a/idaes/apps/flexibility_analysis/tests/test_var_utils.py b/idaes/apps/flexibility_analysis/tests/test_var_utils.py index e54e07225d..2a0776801a 100644 --- a/idaes/apps/flexibility_analysis/tests/test_var_utils.py +++ b/idaes/apps/flexibility_analysis/tests/test_var_utils.py @@ -1,7 +1,12 @@ import pyomo.environ as pe import unittest -from flexibility.var_utils import (get_all_unfixed_variables, get_used_unfixed_variables, BoundsManager, - _remove_var_bounds, _apply_var_bounds) +from flexibility.var_utils import ( + get_all_unfixed_variables, + get_used_unfixed_variables, + BoundsManager, + _remove_var_bounds, + _apply_var_bounds, +) class TestGetVariables(unittest.TestCase): @@ -19,7 +24,7 @@ def test_get_all_unfixed_variables(self): def test_get_used_unfixed_variables(self): m = pe.ConcreteModel() - m.x = pe.Var([1,2,3,4,5,6]) + m.x = pe.Var([1, 2, 3, 4, 5, 6]) m.x[3].fix(1) m.x[4].fix(1) m.x[6].fix(1) diff --git a/idaes/apps/flexibility_analysis/uncertain_params.py b/idaes/apps/flexibility_analysis/uncertain_params.py index fabc936d89..f4e31c8573 100644 --- a/idaes/apps/flexibility_analysis/uncertain_params.py +++ b/idaes/apps/flexibility_analysis/uncertain_params.py @@ -8,15 +8,21 @@ from .var_utils import get_all_unfixed_variables -def _replace_uncertain_params(m: _BlockData, - uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], - param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]]) -> _BlockData: +def _replace_uncertain_params( + m: _BlockData, + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], +) -> _BlockData: for v in get_all_unfixed_variables(m): if not pe.is_constant(v._lb): - raise ValueError(f'The lower bound on {str(v)} is not constant. All variable bounds must be constant.') + raise ValueError( + f"The lower bound on {str(v)} is not constant. All variable bounds must be constant." + ) if not pe.is_constant(v._ub): - raise ValueError(f'The upper bound on {str(v)} is not constant. All variable bounds must be constant.') + raise ValueError( + f"The upper bound on {str(v)} is not constant. All variable bounds must be constant." + ) m.unc_param_vars_set = pe.Set() m.unc_param_vars = pe.Var(m.unc_param_vars_set) @@ -31,7 +37,9 @@ def _replace_uncertain_params(m: _BlockData, m.unc_cons_set = pe.Set() m.unc_cons = pe.Constraint(m.unc_cons_set) - for c in list(m.component_data_objects(pe.Constraint, descend_into=True, active=True)): + for c in list( + m.component_data_objects(pe.Constraint, descend_into=True, active=True) + ): new_body = replace_expressions(c.body, substitution_map=sub_map) new_lower = replace_expressions(c.lower, substitution_map=sub_map) new_upper = replace_expressions(c.upper, substitution_map=sub_map) @@ -41,11 +49,11 @@ def _replace_uncertain_params(m: _BlockData, m.unc_cons[key] = new_body == new_lower else: if c.lower is not None: - key = _ConIndex(c, 'lb') + key = _ConIndex(c, "lb") m.unc_cons_set.add(key) m.unc_cons[key] = new_lower <= new_body if c.upper is not None: - key = _ConIndex(c, 'ub') + key = _ConIndex(c, "ub") m.unc_cons_set.add(key) m.unc_cons[key] = new_body <= new_upper c.deactivate() diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 2ff5f14e43..7e52eac0ca 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -8,7 +8,11 @@ def get_all_unfixed_variables(m: _BlockData): - return ComponentSet(v for v in m.component_data_objects(pe.Var, descend_into=True, active=True) if not v.is_fixed()) + return ComponentSet( + v + for v in m.component_data_objects(pe.Var, descend_into=True, active=True) + if not v.is_fixed() + ) def get_used_unfixed_variables(m: _BlockData): @@ -45,7 +49,7 @@ def _remove_var_bounds(m: _BlockData): v.setlb(None) v.setub(None) if v.is_integer(): - raise ValueError('Unwilling to remove domain from integer variable') + raise ValueError("Unwilling to remove domain from integer variable") v.domain = pe.Reals From 051db2ba4afd9086f52ac167dd898eb209c03337 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 09:00:18 -0700 Subject: [PATCH 03/60] flexibility: updating imports --- .../decision_rules/tests/test_linear_dr.py | 5 +-- .../examples/idaes_hx_network.py | 4 +-- .../examples/linear_hx_network.py | 2 +- .../examples/nonlin_hx_network.py | 2 +- idaes/apps/flexibility_analysis/flex_index.py | 2 +- idaes/apps/flexibility_analysis/flextest.py | 31 +++++++++++++++---- idaes/apps/flexibility_analysis/kkt.py | 4 +-- idaes/apps/flexibility_analysis/sampling.py | 1 - .../tests/test_flextest.py | 4 +-- .../tests/test_indices.py | 2 +- .../tests/test_sampling.py | 4 +-- .../tests/test_uncertain_params.py | 4 +-- .../tests/test_var_utils.py | 2 +- 13 files changed, 42 insertions(+), 25 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index 7246337cfd..b8f887ec41 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -1,9 +1,10 @@ import unittest import pyomo.environ as pe -from flexibility.decision_rules.linear_dr import construct_linear_decision_rule +from idaes.apps.flexibility_analysis.decision_rules.linear_dr import ( + construct_linear_decision_rule, +) from pyomo.contrib.appsi.solvers import Gurobi import numpy as np -from pyomo.core.expr.compare import compare_expressions def y1_func(x1, x2): diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 9e9227bd11..99d512a05a 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -13,8 +13,8 @@ from idaes.core.control_volume_base import ControlVolumeBlockData from pyomo.core.base.block import _BlockData import numpy as np -import flexibility -from flexibility.var_utils import BoundsManager +import idaes.apps.flexibility_analysis as flexibility +from idaes.apps.flexibility_analysis.var_utils import BoundsManager from pyomo.core.expr.numvalue import polynomial_degree from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.repn.standard_repn import generate_standard_repn diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 4bc633dd80..b46b3df7fe 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -1,6 +1,6 @@ import pyomo.environ as pe from pyomo.core.base.block import _BlockData -import flexibility +import idaes.apps.flexibility_analysis as flexibility from typing import Tuple, Mapping from pyomo.contrib.fbbt import interval diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index 07d278001a..893a8aa0c6 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -2,7 +2,7 @@ from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData -import flexibility +import idaes.apps.flexibility_analysis as flexibility from typing import Tuple, MutableMapping, Union diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index d2a1684ce8..ab4b60982c 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -3,7 +3,7 @@ from typing import Sequence, Union, Mapping, MutableMapping, Optional from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from flexibility.sampling import SamplingStrategy +from .sampling import SamplingStrategy from .flextest import ( FlexTestConfig, solve_flextest, diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index ec984175d1..0eb5692266 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -12,14 +12,14 @@ from .indices import _VarIndex, _ConIndex from .uncertain_params import _replace_uncertain_params from .inner_problem import _build_inner_problem -from .scaling_check import check_bounds_and_scaling +from pyomo.util.report_scaling import report_scaling import logging from typing import Sequence, Union, Mapping, MutableMapping, Optional, Tuple from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from flexibility.decision_rules.linear_dr import construct_linear_decision_rule -from flexibility.decision_rules.relu_dr import construct_relu_decision_rule -from flexibility.sampling import ( +from .decision_rules.linear_dr import construct_linear_decision_rule +from .decision_rules.relu_dr import construct_relu_decision_rule +from .sampling import ( SamplingStrategy, perform_sampling, SamplingConfig, @@ -33,12 +33,27 @@ InEnum, MarkImmutable, ) -from flexibility.scaling_check import _get_longest_name logger = logging.getLogger(__name__) +def _get_longest_name(comps): + longest_name = 0 + + for i in comps: + i_len = len(str(i)) + if i_len > longest_name: + longest_name = i_len + + if longest_name > 195: + longest_name = 195 + if longest_name < 12: + longest_name = 12 + + return longest_name + + class FlexTestMethod(enum.Enum): active_constraint = enum.auto() linear_decision_rule = enum.auto() @@ -181,7 +196,11 @@ def build_flextest_with_dr( bounds_manager.save_bounds() _remove_var_bounds(m) _apply_var_bounds(valid_var_bounds) - check_bounds_and_scaling(m) + passed = report_scaling(m) + if not passed: + raise ValueError( + "Please scale the model. If a scaling report was not shown, set the logging level to INFO." + ) bounds_manager.pop_bounds() # construct the decision rule diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index a867fe66c9..34a086752c 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -5,11 +5,9 @@ from pyomo.contrib.fbbt.fbbt import fbbt from .var_utils import ( get_used_unfixed_variables, - _remove_var_bounds, _apply_var_bounds, - BoundsManager, ) -from typing import Sequence, Mapping, Optional +from typing import Sequence, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression from .indices import _VarIndex, _ConIndex diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index a1fc1c2906..f46fd06e42 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -1,6 +1,5 @@ from pyomo.core.base.block import _BlockData import pyomo.environ as pe -import math import numpy as np import itertools from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 6ae5be9a58..45dc39fd91 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -1,7 +1,7 @@ import pyomo.environ as pe -from flexibility.flextest import build_active_constraint_flextest +from idaes.apps.flexibility_analysis.flextest import build_active_constraint_flextest import unittest -from flexibility.indices import _VarIndex +from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval diff --git a/idaes/apps/flexibility_analysis/tests/test_indices.py b/idaes/apps/flexibility_analysis/tests/test_indices.py index f1fc59f2c0..75c097d2cb 100644 --- a/idaes/apps/flexibility_analysis/tests/test_indices.py +++ b/idaes/apps/flexibility_analysis/tests/test_indices.py @@ -1,6 +1,6 @@ import unittest import pyomo.environ as pe -from flexibility.indices import _VarIndex, _ConIndex +from idaes.apps.flexibility_analysis.indices import _VarIndex, _ConIndex class TestIndices(unittest.TestCase): diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 3865ddc654..b2af8509f1 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -1,7 +1,7 @@ import pyomo.environ as pe -from flexibility.sampling import perform_sampling +from idaes.apps.flexibility_analysis.sampling import perform_sampling import unittest -from flexibility.indices import _VarIndex +from idaes.apps.flexibility_analysis.indices import _VarIndex import numpy as np diff --git a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py index 914390cf43..ae36bb2cd7 100644 --- a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py +++ b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py @@ -1,7 +1,7 @@ import pyomo.environ as pe import unittest -from flexibility.uncertain_params import _replace_uncertain_params -from flexibility.indices import _ConIndex, _VarIndex +from idaes.apps.flexibility_analysis.uncertain_params import _replace_uncertain_params +from idaes.apps.flexibility_analysis.indices import _ConIndex, _VarIndex from pyomo.core.expr.compare import compare_expressions diff --git a/idaes/apps/flexibility_analysis/tests/test_var_utils.py b/idaes/apps/flexibility_analysis/tests/test_var_utils.py index 2a0776801a..054c0077a8 100644 --- a/idaes/apps/flexibility_analysis/tests/test_var_utils.py +++ b/idaes/apps/flexibility_analysis/tests/test_var_utils.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import unittest -from flexibility.var_utils import ( +from idaes.apps.flexibility_analysis.var_utils import ( get_all_unfixed_variables, get_used_unfixed_variables, BoundsManager, From d27ecde653b8861abd97c15b799c40d27c1555eb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 09:07:31 -0700 Subject: [PATCH 04/60] flexibility analysis cleanup --- idaes/apps/flexibility_analysis/flex_index.py | 8 ++++---- idaes/apps/flexibility_analysis/flextest.py | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index ab4b60982c..f1f35de049 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -40,19 +40,19 @@ def _add_table_row( if flex_index_lower is None: lb_str = str(flex_index_lower) else: - lb_str = f"{flex_index_lower:12.3e}" + lb_str = f"{flex_index_lower:<12.3e}" if flex_index_upper is None: ub_str = str(flex_index_upper) else: - ub_str = f"{flex_index_upper:12.3e}" + ub_str = f"{flex_index_upper:<12.3e}" if fi_lb_max_viol is None: lb_mv_str = str(fi_lb_max_viol) else: - lb_mv_str = f"{fi_lb_max_viol:12.3e}" + lb_mv_str = f"{fi_lb_max_viol:<12.3e}" if fi_ub_max_viol is None: ub_mv_str = str(fi_ub_max_viol) else: - ub_mv_str = f"{fi_ub_max_viol:12.3e}" + ub_mv_str = f"{fi_ub_max_viol:<12.3e}" logger.log( log_level, f"{outer_iter:<12}{lb_str:<12}{ub_str:<12}{lb_mv_str:<15}{ub_mv_str:<15}", diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 0eb5692266..7610f62b79 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -251,7 +251,11 @@ def build_active_constraint_flextest( bounds_manager.save_bounds() _remove_var_bounds(m) _apply_var_bounds(valid_var_bounds) - check_bounds_and_scaling(m) + passed = report_scaling(m) + if not passed: + raise ValueError( + "Please scale the model. If a scaling report was not shown, set the logging level to INFO." + ) bounds_manager.pop_bounds() orig_equality_cons = [ From 73c43268912b424e14b6a7fe842666a5710d2d4f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 10:04:53 -0700 Subject: [PATCH 05/60] flexibility analysis cleanup --- idaes/apps/flexibility_analysis/sampling.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index f46fd06e42..77a8d3a9d0 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -129,15 +129,18 @@ def _perform_sampling( ) if using_persistent: - config.solver.set_instance(m) original_update_config = config.solver.update_config() config.solver.update_config.check_for_new_or_removed_constraints = False config.solver.update_config.check_for_new_or_removed_vars = False config.solver.update_config.check_for_new_or_removed_params = False + config.solver.update_config.check_for_new_objective = False config.solver.update_config.update_constraints = False config.solver.update_config.update_vars = False config.solver.update_config.update_params = False config.solver.update_config.update_named_expressions = False + config.solver.update_config.update_objective = False + config.solver.update_config.treat_fixed_vars_as_params = False + config.solver.set_instance(m) max_violation_values = list() From 392293257b5a69fcf020d89bb220e00d626f9b02 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 10:36:55 -0700 Subject: [PATCH 06/60] flexibility analysis cleanup --- .../decision_rules/relu_dr.py | 25 +++++++++++++++++-- .../examples/linear_hx_network.py | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index dfdf8e5b8e..d7609a8b70 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -11,6 +11,7 @@ from omlt.io import load_keras_sequential from .dr_config import DRConfig from pyomo.common.config import ConfigValue +from idaes.apps.flexibility_analysis.indices import _VarIndex class ReluDRConfig(DRConfig): @@ -134,9 +135,29 @@ def construct_relu_decision_rule( offset_outputs=[float(i) for i in output_mean], factor_outputs=[float(i) for i in output_std], ) - input_bounds = [(v.lb, v.ub) for v in inputs] + input_bounds = { + ndx: ( + (v.lb - input_mean[ndx]) / input_std[ndx], + (v.ub - input_mean[ndx]) / input_std[ndx], + ) + for ndx, v in enumerate(inputs) + } net = load_keras_sequential(nn, scaler, input_bounds) formulation = ReluBigMFormulation(net) - res.nn.build_formulation(formulation, input_vars=inputs, output_vars=outputs) + res.nn.build_formulation(formulation) + + res.input_set = pe.Set() + res.input_links = pe.Constraint(res.input_set) + for ndx, v in enumerate(inputs): + key = _VarIndex(v, None) + res.input_set.add(key) + res.input_links[key] = v == res.nn.inputs[ndx] + + res.output_set = pe.Set() + res.output_links = pe.Constraint(res.output_set) + for ndx, v in enumerate(outputs): + key = _VarIndex(v, None) + res.output_set.add(key) + res.output_links[key] = v == res.nn.outputs[ndx] return res diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index b46b3df7fe..e876851e70 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -111,6 +111,7 @@ def main(method): controls=[m.qc], valid_var_bounds=var_bounds, config=config, + reconstruct_decision_rule=False, ) print(results) From dc77dd647a406c9edcde42d84833f65939ac5092 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 2 Mar 2022 12:51:11 -0700 Subject: [PATCH 07/60] flexibility analysis: working on tests --- .../decision_rules/tests/test_linear_dr.py | 14 +++-- .../examples/idaes_hx_network.py | 24 ++------- .../examples/linear_hx_network.py | 51 +++++++++---------- .../examples/tests/test_examples.py | 37 ++++++++++++++ .../tests/test_sampling.py | 42 ++++++--------- 5 files changed, 91 insertions(+), 77 deletions(-) create mode 100644 idaes/apps/flexibility_analysis/examples/tests/test_examples.py diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index b8f887ec41..e45a4f8224 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -2,9 +2,10 @@ import pyomo.environ as pe from idaes.apps.flexibility_analysis.decision_rules.linear_dr import ( construct_linear_decision_rule, + LinearDRConfig ) -from pyomo.contrib.appsi.solvers import Gurobi import numpy as np +import pytest def y1_func(x1, x2): @@ -15,6 +16,7 @@ def y2_func(x1, x2): return -x1 + 0.5 * x2 +@pytest.mark.solver class TestLinearDecisionRule(unittest.TestCase): def test_construct_linear_dr(self): x1_samples = [float(i) for i in np.linspace(-5, 5, 100)] @@ -41,20 +43,22 @@ def test_construct_linear_dr(self): output_vals[m.y1] = [float(i) for i in y1_samples] output_vals[m.y2] = [float(i) for i in y2_samples] - opt = Gurobi() + opt = pe.SolverFactory('appsi_gurobi') + config = LinearDRConfig() + config.solver = opt m.dr = construct_linear_decision_rule( - input_vals=input_vals, output_vals=output_vals, solver=opt + input_vals=input_vals, output_vals=output_vals, config=config ) self.assertEqual(pe.value(m.dr.decision_rule[0].lower), 0) - self.assertEqual(pe.value(m.dr.decision_rule[0].lower), 0) + self.assertEqual(pe.value(m.dr.decision_rule[0].upper), 0) self.assertAlmostEqual( pe.value(m.dr.decision_rule[0].body), y1_func(m.x1.value, m.x2.value) - m.y1.value, ) self.assertEqual(pe.value(m.dr.decision_rule[1].lower), 0) - self.assertEqual(pe.value(m.dr.decision_rule[1].lower), 0) + self.assertEqual(pe.value(m.dr.decision_rule[1].upper), 0) self.assertAlmostEqual( pe.value(m.dr.decision_rule[1].body), y2_func(m.x1.value, m.x2.value) - m.y2.value, diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 99d512a05a..5fb97b3ded 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -333,9 +333,6 @@ def nominal_optimization(): m.fs.cooler3.report() m.fs.cooler4.report() - # for v in ComponentSet(m.component_data_objects(pe.Var, descend_into=True)): - # print(v, v.value) - def main(method): m, nominal_values, param_bounds = create_model() @@ -348,8 +345,9 @@ def main(method): config.minlp_solver = pe.SolverFactory("scip") config.minlp_solver.options["limits/time"] = 300 config.sampling_config.solver = pe.SolverFactory("appsi_ipopt") - config.sampling_config.strategy = flexibility.SamplingStrategy.lhs - config.sampling_config.num_points = 100 + config.sampling_config.solver.config.log_level = logging.DEBUG + config.sampling_config.strategy = flexibility.SamplingStrategy.grid + config.sampling_config.num_points = 5 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") @@ -374,20 +372,8 @@ def main(method): config=config, ) print(results) - # results = flexibility.solve_flex_index(m=m, uncertain_params=list(nominal_values.keys()), - # param_nominal_values=nominal_values, param_bounds=param_bounds, - # controls=[m.qc], valid_var_bounds=var_bounds, config=config) - # print(results) + return results if __name__ == "__main__": - print("\n\n********************Active Constraint**************************") - main(flexibility.FlexTestMethod.active_constraint) - # print('\n\n********************Linear Decision Rule**************************') - # main(flexibility.FlexTestMethod.linear_decision_rule) - # print('\n\n********************Vertex Enumeration**************************') - # main(flexibility.FlexTestMethod.vertex_enumeration) - # print('\n\n********************Sampling**************************') - # main(flexibility.FlexTestMethod.sampling) - # print('\n\n********************ReLU Decision rule**************************') - # main(flexibility.FlexTestMethod.relu_decision_rule) + main(flexibility.FlexTestMethod.sampling) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index e876851e70..92c7757293 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -59,12 +59,6 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: m.performance = pe.Constraint(expr=m.variable_temps[7] <= 323) - # m.ineq1 = pe.Constraint(expr=-0.67*m.qc + m.uncertain_temps[3] - 350 <= 0) - # m.ineq2 = pe.Constraint(expr=-m.uncertain_temps[5] - 0.75*m.uncertain_temps[1] + 0.5*m.qc - m.uncertain_temps[3] + 1388.5 <= 0) - # m.ineq3 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] + 2044 <= 0) - # m.ineq4 = pe.Constraint(expr=-m.uncertain_temps[5] - 1.5*m.uncertain_temps[1] + m.qc - 2*m.uncertain_temps[3] - 2*m.uncertain_temps[8] + 2830 <= 0) - # m.ineq5 = pe.Constraint(expr=m.uncertain_temps[5] + 1.5*m.uncertain_temps[1] - m.qc + 2*m.uncertain_temps[3] + 3*m.uncertain_temps[8] - 3153 <= 0) - return m, nominal_values, param_bounds @@ -76,7 +70,7 @@ def get_var_bounds(m): return res -def main(method): +def main(flex_index: bool = False, method: flexibility.FlexTestMethod = flexibility.FlexTestMethod.active_constraint, plot_history=True): m, nominal_values, param_bounds = create_model() var_bounds = get_var_bounds(m) config = flexibility.FlexTestConfig() @@ -98,30 +92,33 @@ def main(method): config.decision_rule_config.batch_size = 50 config.decision_rule_config.scale_inputs = True config.decision_rule_config.scale_outputs = True - config.decision_rule_config.plot_history = True - # results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), - # param_nominal_values=nominal_values, param_bounds=param_bounds, - # controls=[m.qc], valid_var_bounds=var_bounds, config=config) - # print(results) - results = flexibility.solve_flex_index( - m=m, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - controls=[m.qc], - valid_var_bounds=var_bounds, - config=config, - reconstruct_decision_rule=False, - ) - print(results) + config.decision_rule_config.plot_history = plot_history + if not flex_index: + results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, param_bounds=param_bounds, + controls=[m.qc], valid_var_bounds=var_bounds, config=config) + print(results) + else: + results = flexibility.solve_flex_index( + m=m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + valid_var_bounds=var_bounds, + config=config, + reconstruct_decision_rule=False, + ) + print(results) + return results if __name__ == "__main__": print("\n\n********************Active Constraint**************************") - main(flexibility.FlexTestMethod.active_constraint) + main(flex_index=True, method=flexibility.FlexTestMethod.active_constraint) print("\n\n********************Linear Decision Rule**************************") - main(flexibility.FlexTestMethod.linear_decision_rule) + main(flex_index=True, method=flexibility.FlexTestMethod.linear_decision_rule) print("\n\n********************Vertex Enumeration**************************") - main(flexibility.FlexTestMethod.vertex_enumeration) + main(flex_index=True, method=flexibility.FlexTestMethod.vertex_enumeration) print("\n\n********************ReLU Decision rule**************************") - main(flexibility.FlexTestMethod.relu_decision_rule) + main(flex_index=True, method=flexibility.FlexTestMethod.relu_decision_rule) diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py new file mode 100644 index 0000000000..f5074389b7 --- /dev/null +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -0,0 +1,37 @@ +import idaes.apps.flexibility_analysis as flex +from idaes.apps.flexibility_analysis.examples import linear_hx_network, nonlin_hx_network, idaes_hx_network +import unittest + + +class TestExamples(unittest.TestCase): + def test_linear_hx_network(self): + res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.active_constraint, plot_history=False) + self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + self.assertAlmostEqual(res.max_constraint_violation, 4, 5) + + res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.vertex_enumeration, plot_history=False) + self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) + + res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.linear_decision_rule, plot_history=False) + self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + + res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.relu_decision_rule, plot_history=False) + self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + + res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.active_constraint, plot_history=False) + self.assertAlmostEqual(res, 0.5, 5) + + res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.vertex_enumeration, plot_history=False) + self.assertAlmostEqual(res, 0.5, 5) + + res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.linear_decision_rule, plot_history=False) + self.assertLessEqual(res, 0.5) + + res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.relu_decision_rule, plot_history=False) + self.assertAlmostEqual(res, 0.5, 2) + + def test_idaes_hx_network(self): + res = idaes_hx_network.main(flex.FlexTestMethod.sampling) + self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + self.assertAlmostEqual(res.max_constraint_violation, 0.375170890924453) # regression diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index b2af8509f1..7cb0ddd09b 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -1,7 +1,6 @@ import pyomo.environ as pe -from idaes.apps.flexibility_analysis.sampling import perform_sampling +from idaes.apps.flexibility_analysis.sampling import perform_sampling, SamplingConfig, SamplingStrategy import unittest -from idaes.apps.flexibility_analysis.indices import _VarIndex import numpy as np @@ -90,15 +89,18 @@ class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() opt = pe.SolverFactory("scip") + config = SamplingConfig() + config.solver = opt + config.num_points = 5 + config.strategy = SamplingStrategy.grid tmp = perform_sampling( m, - num_points=5, - solver=opt, uncertain_params=list(nominal_values.keys()), param_nominal_values=nominal_values, param_bounds=param_bounds, controls=[m.z], in_place=True, + config=config ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) @@ -109,50 +111,38 @@ def test_poly(self): def test_hx_network(self): m, nominal_values, param_bounds = create_hx_network_model() opt = pe.SolverFactory("gurobi_direct") + config = SamplingConfig() + config.solver = opt + config.num_points = 2 + config.strategy = SamplingStrategy.grid tmp = perform_sampling( m, - num_points=2, - solver=opt, uncertain_params=list(nominal_values.keys()), param_nominal_values=nominal_values, param_bounds=param_bounds, controls=[m.qc], in_place=True, + config=config ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) - def test_hx_network2(self): - m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory("gurobi_direct") - tmp = perform_sampling( - m, - num_points=2, - solver=opt, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - enforce_equalities=False, - controls=[m.qc], - in_place=True, - ) - sample_points, max_violation_values, control_values = tmp - max_viol_ndx = np.argmax(max_violation_values) - self.assertAlmostEqual(max_violation_values[max_viol_ndx], 4, 4) - def test_hx_network3(self): m, nominal_values, param_bounds = create_hx_network_model() opt = pe.SolverFactory("appsi_gurobi") + config = SamplingConfig() + config.solver = opt + config.num_points = 2 + config.strategy = SamplingStrategy.grid tmp = perform_sampling( m, - num_points=2, - solver=opt, uncertain_params=list(nominal_values.keys()), param_nominal_values=nominal_values, param_bounds=param_bounds, controls=[m.qc], in_place=True, + config=config ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) From dae913f64eeb65a6fbf59fe2cee1d59d35b2dff5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 4 Mar 2022 13:39:53 -0700 Subject: [PATCH 08/60] flexibility analysis: updating imports --- idaes/apps/flexibility_analysis/__init__.py | 2 +- .../_check_dependencies.py | 6 +++ .../_check_relu_dependencies.py | 6 +++ .../decision_rules/relu_dr.py | 43 +------------------ .../decision_rules/relu_dr_config.py | 42 ++++++++++++++++++ .../decision_rules/tests/test_linear_dr.py | 3 +- .../examples/idaes_hx_network.py | 5 ++- .../examples/linear_hx_network.py | 6 +-- .../examples/tests/test_examples.py | 3 ++ idaes/apps/flexibility_analysis/flextest.py | 40 +++++++++++++---- .../flexibility_analysis/inner_problem.py | 5 ++- idaes/apps/flexibility_analysis/kkt.py | 7 +-- idaes/apps/flexibility_analysis/sampling.py | 10 +++-- .../tests/test_flextest.py | 3 ++ .../tests/test_indices.py | 3 ++ .../tests/test_sampling.py | 3 ++ .../tests/test_uncertain_params.py | 3 ++ .../tests/test_var_utils.py | 4 ++ idaes/apps/flexibility_analysis/var_utils.py | 6 ++- 19 files changed, 132 insertions(+), 68 deletions(-) create mode 100644 idaes/apps/flexibility_analysis/_check_dependencies.py create mode 100644 idaes/apps/flexibility_analysis/_check_relu_dependencies.py create mode 100644 idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index 8202cf7833..b4b76557c5 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -10,5 +10,5 @@ ) from .decision_rules.dr_enum import DecisionRuleTypes from .decision_rules.linear_dr import LinearDRConfig -from .decision_rules.relu_dr import ReluDRConfig +from .decision_rules.relu_dr_config import ReluDRConfig from .flex_index import solve_flex_index diff --git a/idaes/apps/flexibility_analysis/_check_dependencies.py b/idaes/apps/flexibility_analysis/_check_dependencies.py new file mode 100644 index 0000000000..7c3487806f --- /dev/null +++ b/idaes/apps/flexibility_analysis/_check_dependencies.py @@ -0,0 +1,6 @@ +from pyomo.common.dependencies import attempt_import +import unittest +coramin, coramin_available = attempt_import('coramin') +np, nump_available = attempt_import('numpy') +if not coramin_available or not nump_available: + raise unittest.SkipTest('flexibility_analysis tests require coramin and numpy') diff --git a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py new file mode 100644 index 0000000000..ba5db2630a --- /dev/null +++ b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py @@ -0,0 +1,6 @@ +from pyomo.common.dependencies import attempt_import +import unittest +tensorflow, tensorflow_available = attempt_import('tensorflow') +omlt, nump_available = attempt_import('omlt') +if not tensorflow_available or not nump_available: + raise unittest.SkipTest('flexibility_analysis tests require tensorflow and omlt') diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index d7609a8b70..cd56d6f0bc 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -9,49 +9,8 @@ from omlt import OmltBlock, OffsetScaling from omlt.neuralnet import ReluBigMFormulation from omlt.io import load_keras_sequential -from .dr_config import DRConfig -from pyomo.common.config import ConfigValue from idaes.apps.flexibility_analysis.indices import _VarIndex - - -class ReluDRConfig(DRConfig): - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - self.n_layers: int = self.declare( - "n_layers", ConfigValue(domain=int, default=4) - ) - self.n_nodes_per_layer: int = self.declare( - "n_nodes_per_layer", ConfigValue(domain=int, default=4) - ) - self.tensorflow_seed: int = self.declare( - "tensorflow_seed", ConfigValue(domain=int, default=0) - ) - self.scale_inputs: bool = self.declare( - "scale_inputs", ConfigValue(domain=bool, default=True) - ) - self.scale_outputs: bool = self.declare( - "scale_outputs", ConfigValue(domain=bool, default=True) - ) - self.epochs: int = self.declare("epochs", ConfigValue(domain=int, default=2000)) - self.batch_size: int = self.declare( - "batch_size", ConfigValue(domain=int, default=20) - ) - self.plot_history: bool = self.declare( - "plot_history", ConfigValue(domain=bool, default=False) - ) +from .relu_dr_config import ReluDRConfig def construct_relu_decision_rule( diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py new file mode 100644 index 0000000000..a5f3a2e41d --- /dev/null +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py @@ -0,0 +1,42 @@ +from .dr_config import DRConfig +from pyomo.common.config import ConfigValue + + +class ReluDRConfig(DRConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.n_layers: int = self.declare( + "n_layers", ConfigValue(domain=int, default=4) + ) + self.n_nodes_per_layer: int = self.declare( + "n_nodes_per_layer", ConfigValue(domain=int, default=4) + ) + self.tensorflow_seed: int = self.declare( + "tensorflow_seed", ConfigValue(domain=int, default=0) + ) + self.scale_inputs: bool = self.declare( + "scale_inputs", ConfigValue(domain=bool, default=True) + ) + self.scale_outputs: bool = self.declare( + "scale_outputs", ConfigValue(domain=bool, default=True) + ) + self.epochs: int = self.declare("epochs", ConfigValue(domain=int, default=2000)) + self.batch_size: int = self.declare( + "batch_size", ConfigValue(domain=int, default=20) + ) + self.plot_history: bool = self.declare( + "plot_history", ConfigValue(domain=bool, default=False) + ) diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index e45a4f8224..26cf7fd020 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -1,3 +1,4 @@ +from idaes.apps.flexibility_analysis import _check_dependencies import unittest import pyomo.environ as pe from idaes.apps.flexibility_analysis.decision_rules.linear_dr import ( @@ -16,8 +17,8 @@ def y2_func(x1, x2): return -x1 + 0.5 * x2 -@pytest.mark.solver class TestLinearDecisionRule(unittest.TestCase): + @pytest.mark.unit def test_construct_linear_dr(self): x1_samples = [float(i) for i in np.linspace(-5, 5, 100)] x2_samples = [float(i) for i in np.linspace(-5, 5, 100)] diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 5fb97b3ded..137c79a744 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -4,7 +4,8 @@ ) from idaes.core import FlowsheetBlock from idaes.generic_models.unit_models.heater import Heater -import coramin +from pyomo.common.dependencies import attempt_import +coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') import logging from pyomo.contrib.fbbt.fbbt import fbbt from pyomo.network import Arc @@ -347,7 +348,7 @@ def main(method): config.sampling_config.solver = pe.SolverFactory("appsi_ipopt") config.sampling_config.solver.config.log_level = logging.DEBUG config.sampling_config.strategy = flexibility.SamplingStrategy.grid - config.sampling_config.num_points = 5 + config.sampling_config.num_points = 3 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 92c7757293..4d1cec041e 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -75,11 +75,11 @@ def main(flex_index: bool = False, method: flexibility.FlexTestMethod = flexibil var_bounds = get_var_bounds(m) config = flexibility.FlexTestConfig() config.feasibility_tol = 1e-6 - config.terminate_early = False + config.terminate_early = False # TODO: this does not do anything yet config.method = method - config.minlp_solver = pe.SolverFactory("gurobi_direct") + config.minlp_solver = pe.SolverFactory("gurobi_direct") # TODO: rename minlp_solver to describe what it is solving config.sampling_config.solver = pe.SolverFactory("appsi_gurobi") - config.sampling_config.strategy = flexibility.SamplingStrategy.lhs + config.sampling_config.strategy = 'lhs' config.sampling_config.num_points = 200 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index f5074389b7..9afd8b5e80 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -1,8 +1,11 @@ +from idaes.apps.flexibility_analysis import _check_dependencies, _check_relu_dependencies import idaes.apps.flexibility_analysis as flex from idaes.apps.flexibility_analysis.examples import linear_hx_network, nonlin_hx_network, idaes_hx_network import unittest +import pytest +@pytest.mark.unit class TestExamples(unittest.TestCase): def test_linear_hx_network(self): res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.active_constraint, plot_history=False) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 7610f62b79..45f01cfdab 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -1,7 +1,8 @@ import numpy as np from .kkt import add_kkt_with_milp_complementarity_conditions from pyomo.core.base.block import _BlockData -from coramin.utils import get_objective +from pyomo.common.dependencies import attempt_import +coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') import pyomo.environ as pe from .var_utils import ( get_used_unfixed_variables, @@ -18,7 +19,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from .decision_rules.linear_dr import construct_linear_decision_rule -from .decision_rules.relu_dr import construct_relu_decision_rule +from pyomo.common.dependencies import attempt_import from .sampling import ( SamplingStrategy, perform_sampling, @@ -33,6 +34,8 @@ InEnum, MarkImmutable, ) +relu_dr, relu_dr_available = attempt_import('idaes.apps.flexibility_analysis.decision_rules.relu_dr', + 'The ReLU decision rule requires Tensorflow and OMLT') logger = logging.getLogger(__name__) @@ -92,7 +95,7 @@ def __init__( ), ) self.minlp_solver = self.declare( - "minlp_solver", ConfigValue(default=pe.SolverFactory("scip")) + "minlp_solver", ConfigValue() ) self.sampling_config: SamplingConfig = self.declare( "sampling_config", SamplingConfig() @@ -148,7 +151,6 @@ def _get_dof(m: _BlockData): dr_construction_map[ FlexTestMethod.linear_decision_rule ] = construct_linear_decision_rule -dr_construction_map[FlexTestMethod.relu_decision_rule] = construct_relu_decision_rule def build_flextest_with_dr( @@ -160,6 +162,10 @@ def build_flextest_with_dr( valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], config: FlexTestConfig, ): + # this has to be here in case tensorflow or omlt are not installed + dr_construction_map[ + FlexTestMethod.relu_decision_rule] = relu_dr.construct_relu_decision_rule + # enforce_equalities must be true for this method, or the resulting # problem will be unbounded; the key is degrees of freedom @@ -199,12 +205,14 @@ def build_flextest_with_dr( passed = report_scaling(m) if not passed: raise ValueError( - "Please scale the model. If a scaling report was not shown, set the logging level to INFO." + "Please scale the model. If a scaling report was not shown, " + "set the logging level to INFO." ) bounds_manager.pop_bounds() # construct the decision rule - # the keys of sample_points need to be the new variables that replaced the uncertain parameters + # the keys of sample_points need to be the new variables + # that replaced the uncertain parameters sample_points: MutableMapping[_GeneralVarData, Sequence[float]] = pe.ComponentMap() for p in uncertain_params: ndx = _VarIndex(p, None) @@ -226,7 +234,7 @@ def build_flextest_with_dr( _apply_var_bounds(valid_var_bounds) m.decision_rule = dr - obj = get_objective(m) + obj = coramin.utils.get_objective(m) obj.deactivate() m.max_constraint_violation_obj = pe.Objective( @@ -245,8 +253,9 @@ def build_active_constraint_flextest( enforce_equalities = False _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) for v in m.unc_param_vars.values(): - valid_var_bounds[v] = (v.lb, v.ub) + valid_var_bounds[v] = v.bounds + # TODO: make this a context manager or try-finally bounds_manager = BoundsManager(m) bounds_manager.save_bounds() _remove_var_bounds(m) @@ -254,10 +263,12 @@ def build_active_constraint_flextest( passed = report_scaling(m) if not passed: raise ValueError( - "Please scale the model. If a scaling report was not shown, set the logging level to INFO." + "Please scale the model. If a scaling report was not " + "shown, set the logging level to INFO." ) bounds_manager.pop_bounds() + # TODO: constraint.equality does not check for range constraints with equal bounds orig_equality_cons = [ c for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True) @@ -284,6 +295,9 @@ def build_active_constraint_flextest( default_M=default_M, ) + # TODO: to control the namespace and reduce cloning: + # take the users model and stick it on a new block as a sub-block + m.equality_cuts = pe.ConstraintList() max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] for c in orig_equality_cons: @@ -466,6 +480,14 @@ def solve_flextest( original_controls = controls original_valid_var_bounds = valid_var_bounds if not in_place: + # TODO: + # tmp_name = pyomo.common.modeling.unique_component_name(m, 'tmp_data') + # setattr(m, tmp_name, uncertain_params) + # new_m = m.clone() + # old_to_new_params = ComponentMap(zip(getattr(m, tmp_name), + # getattr(new_m, tmp_name))) + # delattr(m, tmp_name) + # m = new_m m = m.clone() uncertain_params = [m.find_component(i) for i in original_uncertain_params] param_nominal_values = pe.ComponentMap( diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index 75d5cfaa00..7abcbb9aa9 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -1,7 +1,8 @@ import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData, ScalarVar -from coramin.utils import get_objective +from pyomo.common.dependencies import attempt_import +coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') from .var_utils import get_all_unfixed_variables, BoundsManager, _apply_var_bounds from .indices import _ConIndex, _VarIndex from typing import MutableMapping, Tuple, Optional, Mapping, Sequence @@ -122,7 +123,7 @@ def _build_inner_problem( This function will also modify valid_var_bounds to include any new variables """ - obj = get_objective(m) + obj = coramin.utils.get_objective(m) if obj is not None: obj.deactivate() diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index 34a086752c..78f7fcd05a 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -1,6 +1,7 @@ import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -from coramin.utils import get_objective +from pyomo.common.dependencies import attempt_import +coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') from pyomo.core.base.block import _BlockData from pyomo.contrib.fbbt.fbbt import fbbt from .var_utils import ( @@ -29,7 +30,7 @@ def _add_grad_lag_constraints(m: _BlockData) -> _BlockData: m.duals_ineq_set = pe.Set() m.duals_ineq = pe.Var(m.duals_ineq_set, bounds=(0, None)) - obj = get_objective(m) + obj = coramin.utils.get_objective(m) assert obj.sense == pe.minimize if obj is None: lagrangian = 0 @@ -135,7 +136,7 @@ def add_kkt_with_milp_complementarity_conditions( v.fix() _add_grad_lag_constraints(m) - obj = get_objective(m) + obj = coramin.utils.get_objective(m) obj.deactivate() _apply_var_bounds(valid_var_bounds) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 77a8d3a9d0..132a350540 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -6,18 +6,22 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.contrib.appsi.base import PersistentSolver -from tqdm import tqdm from .uncertain_params import _replace_uncertain_params from .inner_problem import _build_inner_problem import enum from idaes.surrogate.pysmo.sampling import LatinHypercubeSampling from .indices import _VarIndex from pyomo.common.config import ConfigDict, ConfigValue, InEnum +try: + from tqdm import tqdm +except ImportError: + def tqdm(items, ncols, desc, disable): + return items class SamplingStrategy(enum.Enum): - grid = enum.auto() - lhs = enum.auto() + grid = 'grid' + lhs = 'lhs' def _grid_sampling( diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 45dc39fd91..4b2cb83649 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -1,8 +1,10 @@ +from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe from idaes.apps.flexibility_analysis.flextest import build_active_constraint_flextest import unittest from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval +import pytest def create_poly_model(): @@ -86,6 +88,7 @@ def create_hx_network_model(): return m, nominal_values, param_bounds +@pytest.mark.unit class TestFlexTest(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() diff --git a/idaes/apps/flexibility_analysis/tests/test_indices.py b/idaes/apps/flexibility_analysis/tests/test_indices.py index 75c097d2cb..f0fe2c7f62 100644 --- a/idaes/apps/flexibility_analysis/tests/test_indices.py +++ b/idaes/apps/flexibility_analysis/tests/test_indices.py @@ -1,8 +1,11 @@ +from idaes.apps.flexibility_analysis import _check_dependencies import unittest import pyomo.environ as pe from idaes.apps.flexibility_analysis.indices import _VarIndex, _ConIndex +import pytest +@pytest.mark.unit class TestIndices(unittest.TestCase): def test_var_index(self): m = pe.ConcreteModel() diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 7cb0ddd09b..a08ce15239 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -1,7 +1,9 @@ +from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe from idaes.apps.flexibility_analysis.sampling import perform_sampling, SamplingConfig, SamplingStrategy import unittest import numpy as np +import pytest def create_poly_model(): @@ -85,6 +87,7 @@ def create_hx_network_model(): return m, nominal_values, param_bounds +@pytest.mark.unit class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() diff --git a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py index ae36bb2cd7..d91f059cc0 100644 --- a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py +++ b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py @@ -1,10 +1,13 @@ +from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe import unittest from idaes.apps.flexibility_analysis.uncertain_params import _replace_uncertain_params from idaes.apps.flexibility_analysis.indices import _ConIndex, _VarIndex from pyomo.core.expr.compare import compare_expressions +import pytest +@pytest.mark.unit class TestReplaceUncertainParams(unittest.TestCase): def test_replace_mutable_parameter(self): m = pe.ConcreteModel() diff --git a/idaes/apps/flexibility_analysis/tests/test_var_utils.py b/idaes/apps/flexibility_analysis/tests/test_var_utils.py index 054c0077a8..e09f82f60b 100644 --- a/idaes/apps/flexibility_analysis/tests/test_var_utils.py +++ b/idaes/apps/flexibility_analysis/tests/test_var_utils.py @@ -1,3 +1,4 @@ +from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe import unittest from idaes.apps.flexibility_analysis.var_utils import ( @@ -7,8 +8,10 @@ _remove_var_bounds, _apply_var_bounds, ) +import pytest +@pytest.mark.unit class TestGetVariables(unittest.TestCase): def test_get_all_unfixed_variables(self): m = pe.ConcreteModel() @@ -36,6 +39,7 @@ def test_get_used_unfixed_variables(self): self.assertIn(m.x[5], uuf_vars) +@pytest.mark.unit class TestBounds(unittest.TestCase): def test_bounds_manager1(self): m = pe.ConcreteModel() diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 7e52eac0ca..dfc8a0eca5 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -2,7 +2,8 @@ from pyomo.core.base.block import _BlockData from pyomo.common.collections import ComponentSet from pyomo.core.expr.visitor import identify_variables -from coramin.utils import get_objective +from pyomo.common.dependencies import attempt_import +coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') from typing import Mapping, Sequence from pyomo.core.base.var import _GeneralVarData @@ -19,7 +20,7 @@ def get_used_unfixed_variables(m: _BlockData): res = ComponentSet() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): res.update(v for v in identify_variables(c.body, include_fixed=False)) - obj = get_objective(m) + obj = coramin.utils.get_objective(m) if obj is not None: res.update(identify_variables(obj.expr, include_fixed=False)) return res @@ -27,6 +28,7 @@ def get_used_unfixed_variables(m: _BlockData): class BoundsManager(object): def __init__(self, m: _BlockData): + # TODO: maybe use get_used_unfixed_variables here? self._vars = ComponentSet(m.component_data_objects(pe.Var, descend_into=True)) self._saved_bounds = list() From 94cee4a196036217a68ff0f317849524dd14a874 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 16 Mar 2022 09:17:48 -0600 Subject: [PATCH 09/60] flexibility analysis: minor updates --- idaes/apps/flexibility_analysis/flextest.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 45f01cfdab..6044f1ca59 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -298,17 +298,18 @@ def build_active_constraint_flextest( # TODO: to control the namespace and reduce cloning: # take the users model and stick it on a new block as a sub-block - m.equality_cuts = pe.ConstraintList() - max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] - for c in orig_equality_cons: - key1 = _ConIndex(c, "lb") - key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") - y1 = m.active_indicator[key2] - key1 = _ConIndex(c, "ub") - key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") - y2 = m.active_indicator[key2] - m.equality_cuts.add(m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub) - m.equality_cuts.add(m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb) + if not enforce_equalities: + m.equality_cuts = pe.ConstraintList() + max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] + for c in orig_equality_cons: + key1 = _ConIndex(c, "lb") + key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") + y1 = m.active_indicator[key2] + key1 = _ConIndex(c, "ub") + key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") + y2 = m.active_indicator[key2] + m.equality_cuts.add(m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub) + m.equality_cuts.add(m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb) m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) From 95b354fce316ceb50c4ad132b6961a593f0dd7a1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 30 Mar 2022 15:02:20 -0600 Subject: [PATCH 10/60] flexibility analysis: better initialization for sampling --- idaes/apps/flexibility_analysis/sampling.py | 41 ++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 132a350540..8e7ab4e478 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -108,6 +108,38 @@ def __init__( ) +def _deactivate_inequalities(m: _BlockData): + deactivated_cons = list() + for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True): + if not c.equality and c.lb != c.ub: + deactivated_cons.append(c) + c.deactivate() + return deactivated_cons + + +def _init_with_square_problem(m: _BlockData, controls, solver): + for v in controls: + v.fix() + m.max_constraint_violation.fix() + deactivated_cons = _deactivate_inequalities(m) + res = solver.solve(m, tee=True) + for c in deactivated_cons: + c.activate() + for v in controls: + v.unfix() + m.max_constraint_violation.value = 0 + max_viol = 0 + for c in deactivated_cons: + assert c.lb is None + assert c.ub == 0 + body_val = pe.value(c.body) + if body_val > max_viol: + max_viol = body_val + m.max_constraint_violation.value = max_viol + m.max_constraint_violation.unfix() + return max_viol + + def _perform_sampling( m: _BlockData, uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], @@ -152,6 +184,8 @@ def _perform_sampling( for v in controls: control_values[v] = list() + orig_max_constraint_violation_ub = m.max_constraint_violation.ub + for sample_ndx in tqdm( list(range(n_samples)), ncols=100, @@ -160,17 +194,22 @@ def _perform_sampling( ): for p, p_vals in sample_points.items(): p.fix(p_vals[sample_ndx]) + print(p, p.value) if using_persistent: config.solver.update_variables(unc_param_vars) - res = config.solver.solve(m) + max_viol_ub = _init_with_square_problem(m, controls, config.solver) + m.max_constraint_violation.setub(max_viol_ub + 1) + res = config.solver.solve(m, tee=False) pe.assert_optimal_termination(res) max_violation_values.append(m.max_constraint_violation.value) for v in controls: control_values[v].append(v.value) + m.max_constraint_violation.setub(orig_max_constraint_violation_ub) + if using_persistent: config.solver.update_config.set_value(original_update_config) From f8a1c03366205948aba89978d438933365bc9807 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 30 Mar 2022 21:20:44 -0600 Subject: [PATCH 11/60] flexibility analysis: better initialization for sampling --- idaes/apps/flexibility_analysis/sampling.py | 84 ++++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 8e7ab4e478..a3f6431345 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -12,6 +12,7 @@ from idaes.surrogate.pysmo.sampling import LatinHypercubeSampling from .indices import _VarIndex from pyomo.common.config import ConfigDict, ConfigValue, InEnum +import coramin try: from tqdm import tqdm except ImportError: @@ -122,24 +123,60 @@ def _init_with_square_problem(m: _BlockData, controls, solver): v.fix() m.max_constraint_violation.fix() deactivated_cons = _deactivate_inequalities(m) - res = solver.solve(m, tee=True) + try: + res = solver.solve(m, tee=False, load_solutions=False) + except: + res = None for c in deactivated_cons: c.activate() for v in controls: v.unfix() - m.max_constraint_violation.value = 0 - max_viol = 0 - for c in deactivated_cons: - assert c.lb is None - assert c.ub == 0 - body_val = pe.value(c.body) - if body_val > max_viol: - max_viol = body_val - m.max_constraint_violation.value = max_viol + if res is not None and pe.check_optimal_termination(res): + m.max_constraint_violation.value = 0 + max_viol = 0 + for c in deactivated_cons: + assert c.lb is None + assert c.ub == 0 + body_val = pe.value(c.body) + if body_val > max_viol: + max_viol = body_val + m.max_constraint_violation.value = max_viol + else: + max_viol = None m.max_constraint_violation.unfix() return max_viol +def _solve_with_max_viol_fixed(m: _BlockData, controls, solver): + orig_obj = coramin.utils.get_objective(m) + orig_obj.deactivate() + orig_max_viol_value = m.max_constraint_violation.value + m.max_constraint_violation.fix(0) + + m.control_setpoints_obj = pe.Objective(expr=sum((v - v.value)**2 for v in controls)) + + try: + res = solver.solve(m, tee=False, load_solutions=False) + if pe.check_optimal_termination(res): + m.solutions.load_from(res) + feasible = True + control_vals = [v.value for v in controls] + else: + feasible = False + control_vals = None + m.max_constraint_violation.value = orig_max_viol_value + except: + feasible = False + control_vals = None + m.max_constraint_violation.value = orig_max_viol_value + + del m.control_setpoints_obj + m.max_constraint_violation.unfix() + orig_obj.activate() + + return feasible, control_vals + + def _perform_sampling( m: _BlockData, uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], @@ -184,6 +221,7 @@ def _perform_sampling( for v in controls: control_values[v] = list() + m.max_constraint_violation.value = 0 orig_max_constraint_violation_ub = m.max_constraint_violation.ub for sample_ndx in tqdm( @@ -194,21 +232,27 @@ def _perform_sampling( ): for p, p_vals in sample_points.items(): p.fix(p_vals[sample_ndx]) - print(p, p.value) if using_persistent: config.solver.update_variables(unc_param_vars) max_viol_ub = _init_with_square_problem(m, controls, config.solver) - m.max_constraint_violation.setub(max_viol_ub + 1) - res = config.solver.solve(m, tee=False) - pe.assert_optimal_termination(res) - max_violation_values.append(m.max_constraint_violation.value) - - for v in controls: - control_values[v].append(v.value) - - m.max_constraint_violation.setub(orig_max_constraint_violation_ub) + if max_viol_ub is not None: + m.max_constraint_violation.setub(max_viol_ub + 1) + feasible, control_vals = _solve_with_max_viol_fixed(m, controls, config.solver) + if feasible: + max_violation_values.append(0) + for v, val in zip(controls, control_vals): + control_values[v].append(val) + else: + res = config.solver.solve(m, tee=False) + pe.assert_optimal_termination(res) + max_violation_values.append(m.max_constraint_violation.value) + + for v in controls: + control_values[v].append(v.value) + + m.max_constraint_violation.setub(orig_max_constraint_violation_ub) if using_persistent: config.solver.update_config.set_value(original_update_config) From de53a448e32f4a30d880db38d5db27843d7d7e9c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 1 Apr 2022 07:16:22 -0600 Subject: [PATCH 12/60] flexibility analysis: better ordering of sampled points --- idaes/apps/flexibility_analysis/sampling.py | 103 ++++++++++++++++++-- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index a3f6431345..7f7c16e662 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -1,8 +1,9 @@ +from __future__ import annotations from pyomo.core.base.block import _BlockData import pyomo.environ as pe import numpy as np import itertools -from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple +from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple, List from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.contrib.appsi.base import PersistentSolver @@ -25,22 +26,104 @@ class SamplingStrategy(enum.Enum): lhs = 'lhs' +class _GridSamplingState(enum.Enum): + increment = 'increment' + decrement = 'decrement' + + +class _ParamIterator(object): + def __init__( + self, + param: _GeneralVarData, + num_points: int, + next_param: Optional[_ParamIterator], + ): + self.state = _GridSamplingState.increment + self.ndx = 0 + self.pts = list(set([float(i) for i in np.linspace(param.lb, param.ub, num_points)])) + self.pts.sort() + self.next_param = next_param + + def reset(self): + self.state = _GridSamplingState.increment + self.ndx = 0 + + def get_value(self): + res = self.pts[self.ndx] + return res + + def swap_state(self): + if self.state == _GridSamplingState.increment: + self.state = _GridSamplingState.decrement + else: + assert self.state == _GridSamplingState.decrement + self.state = _GridSamplingState.increment + + def step(self) -> bool: + if self.state == _GridSamplingState.increment: + if self.ndx == len(self.pts) - 1: + if self.next_param is None: + done = True + else: + done = self.next_param.step() + self.swap_state() + else: + self.ndx += 1 + done = False + else: + assert self.state == _GridSamplingState.decrement + if self.ndx == 0: + assert self.next_param is not None + done = self.next_param.step() + self.swap_state() + else: + self.ndx -= 1 + done = False + return done + + +class _GridSamplingIterator(object): + def __init__( + self, uncertain_params: Sequence[_GeneralVarData], num_points: int + ): + self.params = list(uncertain_params) + self.param_iterators: List[Optional[_ParamIterator]] = [None] * len(self.params) + self.param_iterators[-1] = _ParamIterator( + param=self.params[-1], num_points=num_points, next_param=None + ) + for ndx in reversed(range(len(self.params) - 1)): + self.param_iterators[ndx] = _ParamIterator( + param=self.params[ndx], num_points=num_points, + next_param=self.param_iterators[ndx + 1] + ) + self.done = False + + def __next__(self): + if self.done: + raise StopIteration + + res = [i.get_value() for i in self.param_iterators] + self.done = self.param_iterators[0].step() + + return res + + def __iter__(self): + [i.reset() for i in self.param_iterators] + self.done = False + return self + + def _grid_sampling( uncertain_params: Sequence[_GeneralVarData], num_points: int, seed: int ): - uncertain_params_values = pe.ComponentMap() - for p in uncertain_params: - uncertain_params_values[p] = list( - set([float(i) for i in np.linspace(p.lb, p.ub, num_points)]) - ) - uncertain_params_values[p].sort() + it = _GridSamplingIterator(uncertain_params=uncertain_params, num_points=num_points) sample_points = pe.ComponentMap() for p in uncertain_params: sample_points[p] = list() n_samples = 0 - for sample in itertools.product(*uncertain_params_values.values()): + for sample in it: for p, v in zip(uncertain_params, sample): sample_points[p].append(float(v)) n_samples += 1 @@ -132,7 +215,7 @@ def _init_with_square_problem(m: _BlockData, controls, solver): for v in controls: v.unfix() if res is not None and pe.check_optimal_termination(res): - m.max_constraint_violation.value = 0 + m.solutions.load_from(res) max_viol = 0 for c in deactivated_cons: assert c.lb is None @@ -238,7 +321,7 @@ def _perform_sampling( max_viol_ub = _init_with_square_problem(m, controls, config.solver) if max_viol_ub is not None: - m.max_constraint_violation.setub(max_viol_ub + 1) + m.max_constraint_violation.setub(max_viol_ub) feasible, control_vals = _solve_with_max_viol_fixed(m, controls, config.solver) if feasible: max_violation_values.append(0) From d778ceb66b557622d789d742e41c4376dc5a9a4d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 7 Apr 2022 15:08:39 -0600 Subject: [PATCH 13/60] flexibility analysis: cleaning up sampling code --- idaes/apps/flexibility_analysis/sampling.py | 71 +++++++++++---------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 7f7c16e662..602be4daa1 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -14,6 +14,7 @@ from .indices import _VarIndex from pyomo.common.config import ConfigDict, ConfigValue, InEnum import coramin +from pyomo.common.errors import ApplicationError try: from tqdm import tqdm except ImportError: @@ -26,6 +27,13 @@ class SamplingStrategy(enum.Enum): lhs = 'lhs' +class SamplingInitStrategy(enum.Enum): + none = 'none' + square = 'square' + min_control_deviation = 'min_control_deviation' + all = 'all' + + class _GridSamplingState(enum.Enum): increment = 'increment' decrement = 'decrement' @@ -190,6 +198,12 @@ def __init__( self.enable_progress_bar: bool = self.declare( "enable_progress_bar", ConfigValue(domain=bool, default=True) ) + self.initialization_strategy: SamplingInitStrategy = self.declare( + "initialization_strategy", + ConfigValue( + domain=InEnum(SamplingInitStrategy), default=SamplingInitStrategy.none + ) + ) def _deactivate_inequalities(m: _BlockData): @@ -203,12 +217,19 @@ def _deactivate_inequalities(m: _BlockData): def _init_with_square_problem(m: _BlockData, controls, solver): for v in controls: + if v.value is None: + raise RuntimeError( + 'Cannot initialize sampling problem with square problem because the ' + 'control values are not initialized.' + ) v.fix() + if m.max_constraint_violation.value is None: + m.max_constraint_violation.value = 0 m.max_constraint_violation.fix() deactivated_cons = _deactivate_inequalities(m) try: res = solver.solve(m, tee=False, load_solutions=False) - except: + except ApplicationError: res = None for c in deactivated_cons: c.activate() @@ -236,7 +257,13 @@ def _solve_with_max_viol_fixed(m: _BlockData, controls, solver): orig_max_viol_value = m.max_constraint_violation.value m.max_constraint_violation.fix(0) - m.control_setpoints_obj = pe.Objective(expr=sum((v - v.value)**2 for v in controls)) + obj_expr = 0 + for v in controls: + if v.value is None: + obj_expr += v**2 + else: + obj_expr += (v - v.value)**2 + m.control_setpoints_obj = pe.Objective(expr=obj_expr) try: res = solver.solve(m, tee=False, load_solutions=False) @@ -248,7 +275,7 @@ def _solve_with_max_viol_fixed(m: _BlockData, controls, solver): feasible = False control_vals = None m.max_constraint_violation.value = orig_max_viol_value - except: + except ApplicationError: feasible = False control_vals = None m.max_constraint_violation.value = orig_max_viol_value @@ -270,11 +297,6 @@ def _perform_sampling( Sequence[float], MutableMapping[_GeneralVarData, Sequence[float]], ]: - if isinstance(config.solver, PersistentSolver): - using_persistent = True - else: - using_persistent = False - unc_param_vars = list() for p in uncertain_params: ndx = _VarIndex(p, None) @@ -284,20 +306,6 @@ def _perform_sampling( unc_param_vars, config.num_points, config.lhs_seed ) - if using_persistent: - original_update_config = config.solver.update_config() - config.solver.update_config.check_for_new_or_removed_constraints = False - config.solver.update_config.check_for_new_or_removed_vars = False - config.solver.update_config.check_for_new_or_removed_params = False - config.solver.update_config.check_for_new_objective = False - config.solver.update_config.update_constraints = False - config.solver.update_config.update_vars = False - config.solver.update_config.update_params = False - config.solver.update_config.update_named_expressions = False - config.solver.update_config.update_objective = False - config.solver.update_config.treat_fixed_vars_as_params = False - config.solver.set_instance(m) - max_violation_values = list() control_values = pe.ComponentMap() @@ -316,13 +324,15 @@ def _perform_sampling( for p, p_vals in sample_points.items(): p.fix(p_vals[sample_ndx]) - if using_persistent: - config.solver.update_variables(unc_param_vars) - - max_viol_ub = _init_with_square_problem(m, controls, config.solver) - if max_viol_ub is not None: - m.max_constraint_violation.setub(max_viol_ub) - feasible, control_vals = _solve_with_max_viol_fixed(m, controls, config.solver) + if config.initialization_strategy in {SamplingInitStrategy.square, SamplingInitStrategy.all}: + max_viol_ub = _init_with_square_problem(m, controls, config.solver) + if max_viol_ub is not None: + m.max_constraint_violation.setub(max_viol_ub) + if config.initialization_strategy in {SamplingInitStrategy.min_control_deviation, SamplingInitStrategy.all}: + feasible, control_vals = _solve_with_max_viol_fixed(m, controls, config.solver) + else: + feasible = False + control_vals = None if feasible: max_violation_values.append(0) for v, val in zip(controls, control_vals): @@ -337,9 +347,6 @@ def _perform_sampling( m.max_constraint_violation.setub(orig_max_constraint_violation_ub) - if using_persistent: - config.solver.update_config.set_value(original_update_config) - unc_param_var_to_unc_param_map = pe.ComponentMap( zip(unc_param_vars, uncertain_params) ) From fb236f215e8530a1395f78deb3eff5ad49be8564 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 8 Apr 2022 08:02:50 -0600 Subject: [PATCH 14/60] flexibility analysis: adding config options for active constraint method --- idaes/apps/flexibility_analysis/flextest.py | 75 ++++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 6044f1ca59..852c0110c9 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -33,6 +33,7 @@ PositiveFloat, InEnum, MarkImmutable, + NonNegativeFloat ) relu_dr, relu_dr_available = attempt_import('idaes.apps.flexibility_analysis.decision_rules.relu_dr', 'The ReLU decision rule requires Tensorflow and OMLT') @@ -66,7 +67,7 @@ class FlexTestMethod(enum.Enum): sampling = enum.auto() -class FlexTestConfig(ConfigDict): +class ActiveConstraintConfig(ConfigDict): def __init__( self, description=None, @@ -74,6 +75,36 @@ def __init__( implicit=False, implicit_domain=None, visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_haar_conditions: bool = self.declare( + 'use_haar_conditions', ConfigValue(domain=bool, default=True) + ) + self.default_BigM: Optional[float] = self.declare( + 'default_BigM', ConfigValue(domain=NonNegativeFloat, default=None) + ) + self.enforce_equalities: bool = self.declare( + 'enforce_equalities', ConfigValue(domain=bool, default=False) + ) + self.skip_scaling_check: bool = self.declare( + 'skip_scaling_check', ConfigValue(domain=bool, default=False) + ) + + +class FlexTestConfig(ConfigDict): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, ): super().__init__( description=description, @@ -103,6 +134,9 @@ def __init__( self.decision_rule_config = self.declare( "decision_rule_config", ConfigValue(default=None) ) + self.active_constraint_config: ActiveConstraintConfig = self.declare( + "active_constraint_config", ActiveConstraintConfig() + ) class FlexTestTermination(enum.Enum): @@ -248,25 +282,28 @@ def build_active_constraint_flextest( param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - default_M=None, + config: Optional[ActiveConstraintConfig] = None, ): - enforce_equalities = False + if config is None: + config = ActiveConstraintConfig() + _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) for v in m.unc_param_vars.values(): valid_var_bounds[v] = v.bounds # TODO: make this a context manager or try-finally - bounds_manager = BoundsManager(m) - bounds_manager.save_bounds() - _remove_var_bounds(m) - _apply_var_bounds(valid_var_bounds) - passed = report_scaling(m) - if not passed: - raise ValueError( - "Please scale the model. If a scaling report was not " - "shown, set the logging level to INFO." - ) - bounds_manager.pop_bounds() + if not config.skip_scaling_check: + bounds_manager = BoundsManager(m) + bounds_manager.save_bounds() + _remove_var_bounds(m) + _apply_var_bounds(valid_var_bounds) + passed = report_scaling(m) + if not passed: + raise ValueError( + "Please scale the model. If a scaling report was not " + "shown, set the logging level to INFO." + ) + bounds_manager.pop_bounds() # TODO: constraint.equality does not check for range constraints with equal bounds orig_equality_cons = [ @@ -277,7 +314,7 @@ def build_active_constraint_flextest( _build_inner_problem( m=m, - enforce_equalities=enforce_equalities, + enforce_equalities=config.enforce_equalities, unique_constraint_violations=False, valid_var_bounds=valid_var_bounds, ) @@ -292,13 +329,13 @@ def build_active_constraint_flextest( m=m, uncertain_params=list(m.unc_param_vars.values()), valid_var_bounds=valid_var_bounds, - default_M=default_M, + default_M=config.default_BigM, ) # TODO: to control the namespace and reduce cloning: # take the users model and stick it on a new block as a sub-block - if not enforce_equalities: + if not config.enforce_equalities: m.equality_cuts = pe.ConstraintList() max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] for c in orig_equality_cons: @@ -311,7 +348,8 @@ def build_active_constraint_flextest( m.equality_cuts.add(m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub) m.equality_cuts.add(m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb) - m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) + if config.use_haar_conditions: + m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) m.max_constraint_violation_obj = pe.Objective( expr=m.max_constraint_violation, sense=pe.maximize @@ -333,6 +371,7 @@ def _solve_flextest_active_constraint( param_nominal_values=param_nominal_values, param_bounds=param_bounds, valid_var_bounds=valid_var_bounds, + config=config.active_constraint_config ) opt = config.minlp_solver res = opt.solve(m) From 2312395ce2226b82f05ca42c3a963c5403e0b515 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 12 Apr 2022 14:19:06 -0600 Subject: [PATCH 15/60] flexibility analysis: minor updates --- idaes/apps/flexibility_analysis/__init__.py | 3 +++ idaes/apps/flexibility_analysis/flextest.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index b4b76557c5..5a216d5b28 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -7,6 +7,9 @@ FlexTestResults, SamplingConfig, FlexTest, + ActiveConstraintConfig, + build_active_constraint_flextest, + build_flextest_with_dr ) from .decision_rules.dr_enum import DecisionRuleTypes from .decision_rules.linear_dr import LinearDRConfig diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 852c0110c9..6b3ca17105 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -221,6 +221,9 @@ def build_flextest_with_dr( for v in m.unc_param_vars.values(): valid_var_bounds[v] = (v.lb, v.ub) v.fix() # these should be fixed before we check the degrees of freedom + for p, p_bnds in param_bounds.items(): + if p.is_variable_type(): + valid_var_bounds[p] = p_bnds if _get_dof(m) != len(controls): raise ValueError( @@ -290,6 +293,9 @@ def build_active_constraint_flextest( _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) for v in m.unc_param_vars.values(): valid_var_bounds[v] = v.bounds + for p, p_bnds in param_bounds.items(): + if p.is_variable_type(): + valid_var_bounds[p] = p_bnds # TODO: make this a context manager or try-finally if not config.skip_scaling_check: From d81c314d54c37129ab4e2f295d832f0205467ccd Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 19 Apr 2022 08:36:39 -0600 Subject: [PATCH 16/60] flexibility analysis: option for total violation instead of max violation --- idaes/apps/flexibility_analysis/__init__.py | 1 + .../decision_rules/relu_dr.py | 8 +- .../decision_rules/relu_dr_config.py | 3 + idaes/apps/flexibility_analysis/flextest.py | 46 ++- .../flexibility_analysis/inner_problem.py | 315 +++++++++++------- idaes/apps/flexibility_analysis/sampling.py | 30 +- 6 files changed, 262 insertions(+), 141 deletions(-) diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index 5a216d5b28..83e8d7c5a6 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -15,3 +15,4 @@ from .decision_rules.linear_dr import LinearDRConfig from .decision_rules.relu_dr_config import ReluDRConfig from .flex_index import solve_flex_index +from .sampling import perform_sampling diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index cd56d6f0bc..009877ce72 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -66,13 +66,17 @@ def construct_relu_decision_rule( for layer_ndx in range(config.n_layers - 1): nn.add(layers.Dense(config.n_nodes_per_layer, activation="relu")) nn.add(layers.Dense(len(outputs))) - nn.compile(optimizer=keras.optimizers.Adam(), loss="mse") + if config.learning_rate is None: + opt = keras.optimizers.Adam() + else: + opt = keras.optimizers.Adam(learning_rate=config.learning_rate) + nn.compile(optimizer=opt, loss="mse") history = nn.fit( training_input, training_output, batch_size=config.batch_size, epochs=config.epochs, - verbose=0, + #verbose=0, ) if config.plot_history: diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py index a5f3a2e41d..2a034f53bb 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py @@ -37,6 +37,9 @@ def __init__( self.batch_size: int = self.declare( "batch_size", ConfigValue(domain=int, default=20) ) + self.learning_rate = self.declare( + "learning_rate", ConfigValue(default=None) + ) self.plot_history: bool = self.declare( "plot_history", ConfigValue(domain=bool, default=False) ) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 6b3ca17105..692794f4c7 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -95,6 +95,9 @@ def __init__( self.skip_scaling_check: bool = self.declare( 'skip_scaling_check', ConfigValue(domain=bool, default=False) ) + self.total_violation: bool = self.declare( + "total_violation", ConfigValue(domain=bool, default=False) + ) class FlexTestConfig(ConfigDict): @@ -137,6 +140,9 @@ def __init__( self.active_constraint_config: ActiveConstraintConfig = self.declare( "active_constraint_config", ActiveConstraintConfig() ) + self.total_violation: bool = self.declare( + "total_violation", ConfigValue(domain=bool, default=False) + ) class FlexTestTermination(enum.Enum): @@ -196,6 +202,8 @@ def build_flextest_with_dr( valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], config: FlexTestConfig, ): + config.sampling_config.total_violation = config.total_violation + # this has to be here in case tensorflow or omlt are not installed dr_construction_map[ FlexTestMethod.relu_decision_rule] = relu_dr.construct_relu_decision_rule @@ -262,11 +270,17 @@ def build_flextest_with_dr( config=config.decision_rule_config, ) + if config.total_violation: + total_violation_disjunctions = True + else: + total_violation_disjunctions = False _build_inner_problem( m=m, enforce_equalities=True, unique_constraint_violations=True, valid_var_bounds=valid_var_bounds, + total_violation=config.total_violation, + total_violation_disjunctions=total_violation_disjunctions ) _apply_var_bounds(valid_var_bounds) m.decision_rule = dr @@ -274,9 +288,14 @@ def build_flextest_with_dr( obj = coramin.utils.get_objective(m) obj.deactivate() - m.max_constraint_violation_obj = pe.Objective( - expr=m.max_constraint_violation, sense=pe.maximize - ) + if config.total_violation: + m.max_total_violation_obj = pe.Objective( + expr=sum(m.constraint_violation.values()), sense=pe.maximize + ) + else: + m.max_constraint_violation_obj = pe.Objective( + expr=m.max_constraint_violation, sense=pe.maximize + ) def build_active_constraint_flextest( @@ -323,6 +342,8 @@ def build_active_constraint_flextest( enforce_equalities=config.enforce_equalities, unique_constraint_violations=False, valid_var_bounds=valid_var_bounds, + total_violation=config.total_violation, + total_violation_disjunctions=False, ) for v in m.unc_param_vars.values(): @@ -341,7 +362,7 @@ def build_active_constraint_flextest( # TODO: to control the namespace and reduce cloning: # take the users model and stick it on a new block as a sub-block - if not config.enforce_equalities: + if not config.enforce_equalities and not config.total_violation: m.equality_cuts = pe.ConstraintList() max_viol_lb, max_viol_ub = valid_var_bounds[m.max_constraint_violation] for c in orig_equality_cons: @@ -354,12 +375,17 @@ def build_active_constraint_flextest( m.equality_cuts.add(m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub) m.equality_cuts.add(m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb) - if config.use_haar_conditions: + if config.use_haar_conditions and not config.total_violation: m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) - m.max_constraint_violation_obj = pe.Objective( - expr=m.max_constraint_violation, sense=pe.maximize - ) + if config.total_violation: + m.max_total_violation_obj = pe.Objective( + expr=sum(m.constraint_violation.values()), sense=pe.maximize + ) + else: + m.max_constraint_violation_obj = pe.Objective( + expr=m.max_constraint_violation, sense=pe.maximize + ) def _solve_flextest_active_constraint( @@ -414,7 +440,7 @@ def _solve_flextest_decision_rule( config=config, ) opt = config.minlp_solver - res = opt.solve(m) + res = opt.solve(m, tee=True) pe.assert_optimal_termination(res) results = FlexTestResults() @@ -438,6 +464,7 @@ def _solve_flextest_sampling( valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], config: Optional[FlexTestConfig] = None, ) -> FlexTestResults: + config.sampling_config.total_violation = config.total_violation tmp = perform_sampling( m=m, uncertain_params=uncertain_params, @@ -474,6 +501,7 @@ def _solve_flextest_vertex_enumeration( config: FlexTestConfig = config() config.sampling_config.num_points = 2 config.sampling_config.strategy = SamplingStrategy.grid + config.sampling_config.total_violation = config.total_violation tmp = perform_sampling( m=m, uncertain_params=uncertain_params, diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index 7abcbb9aa9..e414cc1e94 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -2,70 +2,116 @@ from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData, ScalarVar from pyomo.common.dependencies import attempt_import +from pyomo.core.expr.numeric_expr import ExpressionBase coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') -from .var_utils import get_all_unfixed_variables, BoundsManager, _apply_var_bounds +from .var_utils import get_all_unfixed_variables, BoundsManager, _apply_var_bounds, get_used_unfixed_variables, _remove_var_bounds from .indices import _ConIndex, _VarIndex -from typing import MutableMapping, Tuple, Optional, Mapping, Sequence -import math +from typing import MutableMapping, Tuple, Optional, Mapping, Union from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -def _get_bounds_on_max_constraint_violation( - m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]] -): - bounds_manager = BoundsManager(m) - bounds_manager.save_bounds() - - _apply_var_bounds(valid_var_bounds) +def _get_g_bounds( + m: _BlockData, original_vars, valid_var_bounds: Mapping +) -> MutableMapping[Union[_ConIndex, _VarIndex], Tuple[float, float]]: - min_constraint_violation = math.inf - max_constraint_violation = -math.inf - m.max_constraint_violation.fix(0) - for c in m.ineq_violation_cons.values(): - exprs = list() - assert c.lower is None - assert c.upper is not None - e = c.body - c.upper - ders = reverse_sd(e) - assert ders[m.max_constraint_violation] == -1 - _lb, _ub = compute_bounds_on_expr(e) - if _lb < min_constraint_violation: - min_constraint_violation = _lb - if _ub > max_constraint_violation: - max_constraint_violation = _ub + key_list = list() + g_list = list() - bounds_manager.pop_bounds() - m.max_constraint_violation.unfix() - - return min_constraint_violation, max_constraint_violation + for c in list( + m.component_data_objects(pe.Constraint, descend_into=True, active=True) + ): + if c.lower is not None: + key = _ConIndex(c, "lb") + g = c.lower - c.body + key_list.append(key) + g_list.append(g) + if c.upper is not None: + key = _ConIndex(c, "ub") + g = c.body - c.upper + key_list.append(key) + g_list.append(g) + for v in original_vars: + if v.is_integer(): + raise ValueError("Original problem must be continuous") + if v.lb is not None: + key = _VarIndex(v, "lb") + g = v.lb - v + key_list.append(key) + g_list.append(g) + if v.ub is not None: + key = _VarIndex(v, "ub") + g = v - v.ub + key_list.append(key) + g_list.append(g) -def _get_constraint_violation_bounds( - m: _BlockData, valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]] -) -> MutableMapping[_GeneralVarData, Tuple[float, float]]: bounds_manager = BoundsManager(m) bounds_manager.save_bounds() + _remove_var_bounds(m) _apply_var_bounds(valid_var_bounds) - constraint_violation_bounds = pe.ComponentMap() - for key in m.ineq_violation_set: - v = m.constraint_violation[key] - v.fix(0) - - c = m.ineq_violation_cons[key] - assert c.equality - e = c.body - c.upper - ders = reverse_sd(e) - assert ders[v] == -1 - _lb, _ub = compute_bounds_on_expr(e) - constraint_violation_bounds[v] = (_lb, _ub) - v.unfix() + g_bounds = dict() + for key, g in zip(key_list, g_list): + g_bounds[key] = compute_bounds_on_expr(g) bounds_manager.pop_bounds() - - return constraint_violation_bounds + return g_bounds + + +def _add_total_violation_disjunctions(m: _BlockData, g: ExpressionBase, key, + g_bounds: MutableMapping): + m.zero_violation_cons[key] = ( + None, + (m.constraint_violation[key] - + (1 - m.zero_violation[key]) * m.violation_disjunction_BigM[key]), + 0, + ) + m.nonzero_violation_cons[key] = ( + None, + (m.constraint_violation[key] - g - + (1 - m.nonzero_violation[key]) * m.violation_disjunction_BigM[key]), + 0, + ) + m.violation_disjunction_cons[key] = ( + m.zero_violation[key] + m.nonzero_violation[key], + 1 + ) + lb, ub = g_bounds[key] + m.violation_disjunction_BigM[key].value = max(abs(lb), abs(ub)) + + +def _process_constraint( + m: _BlockData, + g: ExpressionBase, + key: Union[_ConIndex, _VarIndex], + unique_constraint_violations: bool, + total_violation: bool, + total_violation_disjunctions: bool, + g_bounds: MutableMapping, +): + m.ineq_violation_set.add(key) + if total_violation: + m.constraint_violation[key].setlb(0) + if total_violation_disjunctions: + _add_total_violation_disjunctions(m, g, key, g_bounds) + else: + m.ineq_violation_cons[key] = ( + None, + g - m.constraint_violation[key], + 0, + ) + elif unique_constraint_violations: + m.ineq_violation_cons[key] = ( + g - m.constraint_violation[key], + 0, + ) + else: + m.ineq_violation_cons[key] = ( + None, + g - m.max_constraint_violation, + 0, + ) def _build_inner_problem( @@ -73,9 +119,12 @@ def _build_inner_problem( enforce_equalities: bool, unique_constraint_violations: bool, valid_var_bounds: Optional[MutableMapping[_GeneralVarData, Tuple[float, float]]], + total_violation: bool = False, + total_violation_disjunctions: bool = False, ): """ - If enfoce equalities is True and unique_constraint_violations is False, then this function converts + If enfoce equalities is True and unique_constraint_violations is False, then this + function converts min f(x) s.t. @@ -89,7 +138,8 @@ def _build_inner_problem( c(x) = 0 g(x) <= u - If enfoce equalities is False and unique_constraint_violations is False, then this function converts + If enfoce equalities is False and unique_constraint_violations is False, then this + function converts min f(x) s.t. @@ -104,7 +154,8 @@ def _build_inner_problem( -c(x) <= u g(x) <= u - If enfoce equalities is True and unique_constraint_violations is True, then this function converts + If enfoce equalities is True and unique_constraint_violations is True, then this + function converts min f(x) s.t. @@ -121,27 +172,79 @@ def _build_inner_problem( sum(y_i) = 1 Of course, the nonlinear constraint u = sum(u_i * y_i) gets reformulated. + If total_violation is True, then unique_constraint_violations is ignored and + this function converts + + min f(x) + s.t. + c(x) = 0 + g(x) <= 0 + + to (if enforce_equalities is True) + + min sum_{i} u_i + s.t. + c(x) = 0 + g_i(x) <= u_i + u_i >= 0 + + or to (if enforce_equalities is False) + + min sum_{i} u_i + sum_{j} u_j + sum_{k} u_k + s.t. + c_j(x) <= u_j + -c_k(x) <= u_k + g_i(x) <= u_i + u_i >= 0 + u_j >= 0 + u_k >= 0 + + or to (if total_violation_disjunctions is True) + + max sum_{i} u_i + s.t. + c(x) = 0 + u_i >= 0 + [u_i <= 0] v [u_i <= g_i(x)] + This function will also modify valid_var_bounds to include any new variables """ + if total_violation_disjunctions: + assert total_violation + obj = coramin.utils.get_objective(m) if obj is not None: obj.deactivate() for v in m.unc_param_vars.values(): v.fix() - original_vars = list(get_all_unfixed_variables(m)) + original_vars = list(get_used_unfixed_variables(m)) for v in m.unc_param_vars.values(): v.unfix() - m.max_constraint_violation = ScalarVar() - m.min_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation) + if valid_var_bounds is None: + g_bounds = dict() + else: + g_bounds = _get_g_bounds(m, original_vars, valid_var_bounds) m.ineq_violation_set = pe.Set() - m.ineq_violation_cons = pe.Constraint(m.ineq_violation_set) + if not total_violation_disjunctions: + m.ineq_violation_cons = pe.Constraint(m.ineq_violation_set) - if unique_constraint_violations: + if not total_violation: + m.max_constraint_violation = ScalarVar() + + if unique_constraint_violations or total_violation: m.constraint_violation = pe.Var(m.ineq_violation_set) + if total_violation_disjunctions: + m.zero_violation = pe.Var(m.ineq_violation_set, domain=pe.Binary) + m.nonzero_violation = pe.Var(m.ineq_violation_set, domain=pe.Binary) + m.zero_violation_cons = pe.Constraint(m.ineq_violation_set) + m.nonzero_violation_cons = pe.Constraint(m.ineq_violation_set) + m.violation_disjunction_cons = pe.Constraint(m.ineq_violation_set) + m.violation_disjunction_BigM = pe.Param(m.ineq_violation_set, mutable=True) + for c in list( m.component_data_objects(pe.Constraint, descend_into=True, active=True) ): @@ -149,58 +252,39 @@ def _build_inner_problem( continue if c.lower is not None: key = _ConIndex(c, "lb") - m.ineq_violation_set.add(key) - if unique_constraint_violations: - m.ineq_violation_cons[key] = ( - c.lower - c.body - m.constraint_violation[key], - 0, - ) - else: - m.ineq_violation_cons[key] = ( - None, - c.lower - c.body - m.max_constraint_violation, - 0, - ) + g = c.lower - c.body + _process_constraint(m, g, key, unique_constraint_violations, + total_violation, total_violation_disjunctions, + g_bounds) if c.upper is not None: key = _ConIndex(c, "ub") - m.ineq_violation_set.add(key) - if unique_constraint_violations: - m.ineq_violation_cons[key] = ( - c.body - c.upper - m.constraint_violation[key], - 0, - ) - else: - m.ineq_violation_cons[key] = ( - None, - c.body - c.upper - m.max_constraint_violation, - 0, - ) + g = c.body - c.upper + _process_constraint(m, g, key, unique_constraint_violations, + total_violation, total_violation_disjunctions, + g_bounds) for v in original_vars: if v.is_integer(): raise ValueError("Original problem must be continuous") if v.lb is not None: key = _VarIndex(v, "lb") - m.ineq_violation_set.add(key) - if unique_constraint_violations: - m.ineq_violation_cons[key] = (v.lb - v - m.constraint_violation[key], 0) - else: - m.ineq_violation_cons[key] = ( - None, - v.lb - v - m.max_constraint_violation, - 0, - ) + g = v.lb - v + _process_constraint(m, g, key, unique_constraint_violations, + total_violation, total_violation_disjunctions, + g_bounds) if v.ub is not None: key = _VarIndex(v, "ub") - m.ineq_violation_set.add(key) - if unique_constraint_violations: - m.ineq_violation_cons[key] = (v - v.ub - m.constraint_violation[key], 0) - else: - m.ineq_violation_cons[key] = ( - None, - v - v.ub - m.max_constraint_violation, - 0, - ) + g = v - v.ub + _process_constraint(m, g, key, unique_constraint_violations, + total_violation, total_violation_disjunctions, + g_bounds) + + if total_violation: + m.total_constraint_violation_obj = pe.Objective( + expr=sum(m.constraint_violation.values()) + ) + else: + m.min_constraint_violation_obj = pe.Objective(expr=m.max_constraint_violation) for key in m.ineq_violation_set: if isinstance(key, _ConIndex): @@ -210,7 +294,13 @@ def _build_inner_problem( key.var.setub(None) key.var.domain = pe.Reals - if unique_constraint_violations: + if (total_violation or unique_constraint_violations) and valid_var_bounds is not None: + for key in m.ineq_violation_set: + lb, ub = g_bounds[key] + v = m.constraint_violation[key] + valid_var_bounds[v] = (min(lb, 0), max(ub, 0)) + + if unique_constraint_violations and not total_violation: # max_constraint_violation = sum(constraint_violation[i] * y[i]) # sum(y[i]) == 1 # reformulate as @@ -228,33 +318,20 @@ def _build_inner_problem( m.max_violation_sum = pe.Constraint( expr=m.max_constraint_violation == sum(m.u_hat.values()) ) - constraint_violation_bounds = _get_constraint_violation_bounds( - m, valid_var_bounds - ) m.u_hat_cons = pe.ConstraintList() for key in m.ineq_violation_set: violation_var = m.constraint_violation[key] - viol_lb, viol_ub = constraint_violation_bounds[violation_var] + viol_lb, viol_ub = g_bounds[key] + valid_var_bounds[m.u_hat[key]] = (min(viol_lb, 0), max(viol_ub, 0)) + valid_var_bounds[m.max_violation_selector[key]] = (0, 1) y_i = m.max_violation_selector[key] m.u_hat_cons.add(m.u_hat[key] <= viol_ub * y_i) m.u_hat_cons.add(m.u_hat[key] >= viol_lb * y_i) m.u_hat_cons.add(m.u_hat[key] <= violation_var + viol_lb * y_i - viol_lb) m.u_hat_cons.add(m.u_hat[key] >= viol_ub * y_i + violation_var - viol_ub) - valid_var_bounds.update(constraint_violation_bounds) + + if valid_var_bounds is not None and not total_violation: valid_var_bounds[m.max_constraint_violation] = ( - min(i[0] for i in constraint_violation_bounds.values()), - max(i[1] for i in constraint_violation_bounds.values()), + min(i[0] for i in g_bounds.values()), + max(i[1] for i in g_bounds.values()), ) - for key in m.ineq_violation_set: - valid_var_bounds[m.max_violation_selector[key]] = (0, 1) - valid_var_bounds[m.u_hat[key]] = ( - min(0.0, constraint_violation_bounds[m.constraint_violation[key]][0]), - max(0.0, constraint_violation_bounds[m.constraint_violation[key]][1]), - ) - else: - if valid_var_bounds is not None: - valid_var_bounds[ - m.max_constraint_violation - ] = _get_bounds_on_max_constraint_violation( - m=m, valid_var_bounds=valid_var_bounds - ) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 602be4daa1..430eb9172b 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -204,6 +204,9 @@ def __init__( domain=InEnum(SamplingInitStrategy), default=SamplingInitStrategy.none ) ) + self.total_violation: bool = self.declare( + "total_violation", ConfigValue(domain=bool, default=False) + ) def _deactivate_inequalities(m: _BlockData): @@ -306,14 +309,16 @@ def _perform_sampling( unc_param_vars, config.num_points, config.lhs_seed ) - max_violation_values = list() + obj_values = list() + obj = coramin.utils.get_objective(m) control_values = pe.ComponentMap() for v in controls: control_values[v] = list() - m.max_constraint_violation.value = 0 - orig_max_constraint_violation_ub = m.max_constraint_violation.ub + if not config.total_violation: + m.max_constraint_violation.value = 0 + orig_max_constraint_violation_ub = m.max_constraint_violation.ub for sample_ndx in tqdm( list(range(n_samples)), @@ -324,28 +329,29 @@ def _perform_sampling( for p, p_vals in sample_points.items(): p.fix(p_vals[sample_ndx]) - if config.initialization_strategy in {SamplingInitStrategy.square, SamplingInitStrategy.all}: + if not config.total_violation and config.initialization_strategy in {SamplingInitStrategy.square, SamplingInitStrategy.all}: max_viol_ub = _init_with_square_problem(m, controls, config.solver) if max_viol_ub is not None: m.max_constraint_violation.setub(max_viol_ub) - if config.initialization_strategy in {SamplingInitStrategy.min_control_deviation, SamplingInitStrategy.all}: + if not config.total_violation and config.initialization_strategy in {SamplingInitStrategy.min_control_deviation, SamplingInitStrategy.all}: feasible, control_vals = _solve_with_max_viol_fixed(m, controls, config.solver) else: feasible = False control_vals = None if feasible: - max_violation_values.append(0) + obj_values.append(0) for v, val in zip(controls, control_vals): control_values[v].append(val) else: res = config.solver.solve(m, tee=False) pe.assert_optimal_termination(res) - max_violation_values.append(m.max_constraint_violation.value) + obj_values.append(pe.value(obj.expr)) for v in controls: control_values[v].append(v.value) - m.max_constraint_violation.setub(orig_max_constraint_violation_ub) + if not config.total_violation: + m.max_constraint_violation.setub(orig_max_constraint_violation_ub) unc_param_var_to_unc_param_map = pe.ComponentMap( zip(unc_param_vars, uncertain_params) @@ -354,7 +360,7 @@ def _perform_sampling( (unc_param_var_to_unc_param_map[p], vals) for p, vals in sample_points.items() ) - return sample_points, max_violation_values, control_values + return sample_points, obj_values, control_values def perform_sampling( @@ -388,8 +394,10 @@ def perform_sampling( enforce_equalities=True, unique_constraint_violations=False, valid_var_bounds=None, + total_violation=config.total_violation, + total_violation_disjunctions=False, ) - sample_points, max_violation_values, control_values = _perform_sampling( + sample_points, obj_values, control_values = _perform_sampling( m=m, uncertain_params=uncertain_params, controls=controls, config=config ) @@ -400,4 +408,4 @@ def perform_sampling( (original_model.find_component(v), vals) for v, vals in control_values.items() ) - return sample_points, max_violation_values, control_values + return sample_points, obj_values, control_values From 53fc67b61de2fefc59046e86245fe8aa50d22910 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 5 Sep 2023 09:58:42 -0600 Subject: [PATCH 17/60] update tests --- idaes/apps/flexibility_analysis/sampling.py | 10 +++++++--- .../flexibility_analysis/tests/test_flextest.py | 16 ++++++++++++---- .../flexibility_analysis/tests/test_sampling.py | 8 +++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 430eb9172b..70e41e58ec 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -10,11 +10,12 @@ from .uncertain_params import _replace_uncertain_params from .inner_problem import _build_inner_problem import enum -from idaes.surrogate.pysmo.sampling import LatinHypercubeSampling +from idaes.core.surrogate.pysmo.sampling import LatinHypercubeSampling from .indices import _VarIndex from pyomo.common.config import ConfigDict, ConfigValue, InEnum import coramin from pyomo.common.errors import ApplicationError +from pyomo.contrib import appsi try: from tqdm import tqdm except ImportError: @@ -343,8 +344,11 @@ def _perform_sampling( for v, val in zip(controls, control_vals): control_values[v].append(val) else: - res = config.solver.solve(m, tee=False) - pe.assert_optimal_termination(res) + res = config.solver.solve(m) + if isinstance(config.solver, appsi.base.Solver): + assert res.termination_condition == appsi.base.TerminationCondition.optimal + else: + pe.assert_optimal_termination(res) obj_values.append(pe.value(obj.expr)) for v in controls: diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 4b2cb83649..9b23ce481d 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -5,6 +5,8 @@ from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval import pytest +import coramin +from pyomo.contrib import appsi def create_poly_model(): @@ -101,13 +103,19 @@ def test_poly(self): param_bounds=param_bounds, valid_var_bounds=var_bounds, ) - opt = pe.SolverFactory("scip") - res = opt.solve(m, tee=False) - pe.assert_optimal_termination(res) + nlp_solver = appsi.solvers.Ipopt() + mip_solver = appsi.solvers.Gurobi() + opt = coramin.algorithms.multitree.multitree.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) + opt.config.stream_solver = False + opt.config.obbt_at_new_incumbents = True + opt.config.relax_integers_for_obbt = False + opt.config.mip_gap = 1e-4 + res = opt.solve(m) + assert res.termination_condition == appsi.base.TerminationCondition.optimal self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) self.assertAlmostEqual(m.z.value, -2.6513, 4) ndx = _VarIndex(m.theta, None) - self.assertAlmostEqual(m.unc_param_vars[ndx].value, 65) + self.assertAlmostEqual(m.unc_param_vars[ndx].value, 65, 5) def test_hx_network(self): m, nominal_values, param_bounds = create_hx_network_model() diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index a08ce15239..645c71819d 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -4,6 +4,8 @@ import unittest import numpy as np import pytest +import coramin +from pyomo.contrib import appsi def create_poly_model(): @@ -91,7 +93,11 @@ def create_hx_network_model(): class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() - opt = pe.SolverFactory("scip") + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) + opt.config.stream_solver = False + opt.config.convexity_effort = coramin.Effort.none config = SamplingConfig() config.solver = opt config.num_points = 5 From 5fc5458126cd1071735efd52b2b0e247e1037b41 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 14 Sep 2023 22:58:43 -0600 Subject: [PATCH 18/60] update tests --- idaes/apps/flexibility_analysis/tests/test_sampling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 645c71819d..dce89b1b05 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -139,7 +139,7 @@ def test_hx_network(self): def test_hx_network3(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory("appsi_gurobi") + opt = appsi.solvers.Gurobi() config = SamplingConfig() config.solver = opt config.num_points = 2 From d5bf85418de6cf9caa49b4b734ee6a269309779f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 18 Sep 2023 07:29:56 -0600 Subject: [PATCH 19/60] update flexibility example --- .../flexibility_analysis/check_optimal.py | 9 +++++ .../decision_rules/linear_dr.py | 3 +- .../examples/idaes_hx_network.py | 40 ++++++++----------- idaes/apps/flexibility_analysis/sampling.py | 8 ++-- 4 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 idaes/apps/flexibility_analysis/check_optimal.py diff --git a/idaes/apps/flexibility_analysis/check_optimal.py b/idaes/apps/flexibility_analysis/check_optimal.py new file mode 100644 index 0000000000..2c41ee4f97 --- /dev/null +++ b/idaes/apps/flexibility_analysis/check_optimal.py @@ -0,0 +1,9 @@ +import pyomo.environ as pe +from pyomo.contrib import appsi + + +def assert_optimal_termination(results): + if hasattr(results, 'termination_condition'): + assert results.termination_condition == appsi.base.TerminationCondition.optimal + else: + pe.assert_optimal_termination(results) diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index 27e2bc3921..57ab0ab153 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -5,6 +5,7 @@ from pyomo.core.expr.numeric_expr import LinearExpression from .dr_config import DRConfig from pyomo.common.config import ConfigValue +from ..check_optimal import assert_optimal_termination class LinearDRConfig(DRConfig): @@ -68,7 +69,7 @@ def construct_linear_decision_rule( trainer.est_cons[ndx] = (expr, 0) results = config.solver.solve(trainer) - pe.assert_optimal_termination(results) + assert_optimal_termination(results) lin_coefs = [v.value for v in trainer.coefs.values()] lin_vars = [v for v in input_vals.keys()] diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 137c79a744..4887568fcf 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -1,9 +1,9 @@ import pyomo.environ as pe -from idaes.generic_models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( +from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( BTXParameterBlock, ) from idaes.core import FlowsheetBlock -from idaes.generic_models.unit_models.heater import Heater +from idaes.models.unit_models.heater import Heater from pyomo.common.dependencies import attempt_import coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') import logging @@ -11,7 +11,7 @@ from pyomo.network import Arc from idaes.core.util.initialization import propagate_state from pyomo.util.infeasible import log_infeasible_constraints, log_infeasible_bounds -from idaes.core.control_volume_base import ControlVolumeBlockData +from idaes.core.base.control_volume_base import ControlVolumeBlockData from pyomo.core.base.block import _BlockData import numpy as np import idaes.apps.flexibility_analysis as flexibility @@ -19,6 +19,7 @@ from pyomo.core.expr.numvalue import polynomial_degree from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.repn.standard_repn import generate_standard_repn +from coramin.utils.pyomo_utils import simplify_expr logging.basicConfig(level=logging.INFO) @@ -26,22 +27,20 @@ def create_model(): m = pe.ConcreteModel() - m.fs = FlowsheetBlock(default={"dynamic": False}) + m.fs = FlowsheetBlock(dynamic=False) m.fs.properties = BTXParameterBlock( - default={ - "valid_phase": "Vap", - "activity_coeff_model": "Ideal", - "state_vars": "FTPz", - } + valid_phase="Vap", + activity_coeff_model="Ideal", + state_vars="FTPz", ) - m.fs.heater1 = Heater(default={"property_package": m.fs.properties}) - m.fs.cooler1 = Heater(default={"property_package": m.fs.properties}) - m.fs.heater2 = Heater(default={"property_package": m.fs.properties}) - m.fs.cooler2 = Heater(default={"property_package": m.fs.properties}) - m.fs.heater3 = Heater(default={"property_package": m.fs.properties}) - m.fs.cooler3 = Heater(default={"property_package": m.fs.properties}) - m.fs.cooler4 = Heater(default={"property_package": m.fs.properties}) + m.fs.heater1 = Heater(property_package=m.fs.properties) + m.fs.cooler1 = Heater(property_package=m.fs.properties) + m.fs.heater2 = Heater(property_package=m.fs.properties) + m.fs.cooler2 = Heater(property_package=m.fs.properties) + m.fs.heater3 = Heater(property_package=m.fs.properties) + m.fs.cooler3 = Heater(property_package=m.fs.properties) + m.fs.cooler4 = Heater(property_package=m.fs.properties) m.fs.s1 = Arc(source=m.fs.heater1.outlet, destination=m.fs.heater2.inlet) m.fs.s2 = Arc(source=m.fs.cooler1.outlet, destination=m.fs.cooler4.inlet) @@ -155,14 +154,9 @@ def create_model(): p.unfix() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): - body_degree = polynomial_degree(c.body) lower, body, upper = c.lower, c.body, c.upper - if body_degree == 5: - om, se = sympyify_expression(body) - se = se.simplify() - body = sympy2pyomo_expression(se, om) - c.set_value((c.lower, body, c.upper)) - print("simplified: ", c.body) + body = simplify_expr(body) + c.set_value((c.lower, body, c.upper)) for p in nominal_values.keys(): assert not p.is_fixed() diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 70e41e58ec..f59a1aad7a 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -16,6 +16,7 @@ import coramin from pyomo.common.errors import ApplicationError from pyomo.contrib import appsi +from .check_optimal import assert_optimal_termination try: from tqdm import tqdm except ImportError: @@ -344,11 +345,8 @@ def _perform_sampling( for v, val in zip(controls, control_vals): control_values[v].append(val) else: - res = config.solver.solve(m) - if isinstance(config.solver, appsi.base.Solver): - assert res.termination_condition == appsi.base.TerminationCondition.optimal - else: - pe.assert_optimal_termination(res) + res = config.solver.solve(m, tee=False) + assert_optimal_termination(res) obj_values.append(pe.value(obj.expr)) for v in controls: From 66b66ce0528def5fcfb21c87a3119a0a97540e50 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 18 Sep 2023 09:38:58 -0600 Subject: [PATCH 20/60] update example --- .../examples/linear_hx_network.py | 16 +++++++++++----- idaes/apps/flexibility_analysis/sampling.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 4d1cec041e..81ee0aa677 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -3,6 +3,8 @@ import idaes.apps.flexibility_analysis as flexibility from typing import Tuple, Mapping from pyomo.contrib.fbbt import interval +import numpy as np +import random def create_model() -> Tuple[_BlockData, Mapping, Mapping]: @@ -37,7 +39,7 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: m.variable_temps_set = pe.Set(initialize=[2, 4, 6, 7]) m.variable_temps = pe.Var(m.variable_temps_set, bounds=(0, 1000)) - m.qc = pe.Var() + m.qc = pe.Var(initialize=0) m.balances = pe.Constraint([1, 2, 3, 4]) m.balances[1] = 1.5 * (m.uncertain_temps[1] - m.variable_temps[2]) == 2 * ( @@ -71,28 +73,32 @@ def get_var_bounds(m): def main(flex_index: bool = False, method: flexibility.FlexTestMethod = flexibility.FlexTestMethod.active_constraint, plot_history=True): + np.random.seed(0) + random.seed(1) m, nominal_values, param_bounds = create_model() var_bounds = get_var_bounds(m) config = flexibility.FlexTestConfig() config.feasibility_tol = 1e-6 config.terminate_early = False # TODO: this does not do anything yet config.method = method - config.minlp_solver = pe.SolverFactory("gurobi_direct") # TODO: rename minlp_solver to describe what it is solving + config.minlp_solver = pe.SolverFactory("gurobi_direct") config.sampling_config.solver = pe.SolverFactory("appsi_gurobi") config.sampling_config.strategy = 'lhs' - config.sampling_config.num_points = 200 + config.sampling_config.num_points = 600 + config.sampling_config.initialization_strategy = 'square' if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 - config.decision_rule_config.n_nodes_per_layer = 10 + config.decision_rule_config.n_nodes_per_layer = 15 config.decision_rule_config.epochs = 3000 - config.decision_rule_config.batch_size = 50 + config.decision_rule_config.batch_size = 150 config.decision_rule_config.scale_inputs = True config.decision_rule_config.scale_outputs = True config.decision_rule_config.plot_history = plot_history + config.decision_rule_config.tensorflow_seed = 2 if not flex_index: results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), param_nominal_values=nominal_values, param_bounds=param_bounds, diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index f59a1aad7a..0e47c21046 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -345,7 +345,7 @@ def _perform_sampling( for v, val in zip(controls, control_vals): control_values[v].append(val) else: - res = config.solver.solve(m, tee=False) + res = config.solver.solve(m) assert_optimal_termination(res) obj_values.append(pe.value(obj.expr)) From 91b8051cfcd16a2953068318fccad7dcde654b54 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 21 Dec 2023 14:58:21 -0700 Subject: [PATCH 21/60] working on docs for flexibility analysis --- docs/conf.py | 1 + .../flexibility_analysis/index.rst | 12 +++ .../flexibility_analysis/overview.rst | 75 +++++++++++++++++ .../flexibility_analysis/reference.rst | 62 ++++++++++++++ .../modeling_extensions/index.rst | 1 + idaes/apps/flexibility_analysis/flextest.py | 80 ++++++++++++++++--- 6 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 docs/explanations/modeling_extensions/flexibility_analysis/index.rst create mode 100644 docs/explanations/modeling_extensions/flexibility_analysis/overview.rst create mode 100644 docs/explanations/modeling_extensions/flexibility_analysis/reference.rst diff --git a/docs/conf.py b/docs/conf.py index 625a2400ee..3695f76001 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,7 @@ "sphinx.ext.autosectionlabel", "sphinxarg.ext", "sphinx.ext.doctest", + "enum_tools.autoenum", ] # Put type hints in the description, not signature diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/index.rst b/docs/explanations/modeling_extensions/flexibility_analysis/index.rst new file mode 100644 index 0000000000..0bd0d40675 --- /dev/null +++ b/docs/explanations/modeling_extensions/flexibility_analysis/index.rst @@ -0,0 +1,12 @@ + +Flexibility Analysis +==================== + +A module for performing flexibility analysis. + +.. toctree:: + :maxdepth: 1 + + overview + reference + diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst new file mode 100644 index 0000000000..c39b469099 --- /dev/null +++ b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst @@ -0,0 +1,75 @@ + +Flexibility Analysis Overview +============================= + +The flexibility analysis module within IDAES provides a framework for +evaluating how well a given system performs with respect to a set of +uncertain parameters. Two methods are provided. The flexibility (or feasibility) test +(FT) can be used to determine if a set of performance constraints can +be satisfied for any realization of uncertain parameters. The +flexibility index (FI) can be used to quantify the size of the +uncertainty region for which the performance constraints can be +satisfied [Grossmann1987]_. + +The FT is given by + +.. math:: + + \phi(\underline{\theta}, \overline{\theta}) = &\max_{\theta} \min_{z} u \\ + & s.t. \\ + & g_{j}(x,z,\theta) \leq u \\ + & h(x,z,\theta) = 0 \\ + & \underline{\theta} \leq \theta \leq \overline{\theta} + +where the uncertain parameters are given by :math:`\theta`, :math:`z` +are the controls, :math:`u` is the maximum constraint violation, +:math:`g_j` are the performance constraints, and :math:`h` are the +constraints which represent physics (e.g., mass balances). Note that +the dimension of :math:`x` must match the dimension of :math:`h`. In +other words, if :math:`\theta` and :math:`z` are fixed, then :math:`h` +should completely determine :math:`x`. If +:math:`\phi(\underline{\theta}, \overline{\theta})` is less than or +equal to zero, then the FT passes indicating that, for any +:math:`\theta` between :math:`\underline{\theta}` and +:math:`\overline{\theta}`, there exists a :math:`z` such that +:math:`g_j(x, z, \theta) \leq 0` and :math:`h(x, z, \theta) = 0`. If +:math:`\phi(\underline{\theta}, \overline{\theta})` is greater than +zero, then the FT fails indicating that the performance constraints +cannot be satisfied for at least one value of :math:`\theta` between +:math:`\underline{\theta}` and :math:`\overline{\theta}`. Also note +that this formulation assumes that :math:`h(x,z,\theta) = 0` can be +satisfied for any :math:`\theta` between :math:`\underline{\theta}` +and :math:`\overline{\theta}`. + +The FI is given by + +.. math:: + + \psi(\theta^{N}, \Delta \theta) = &\max \delta \\ + & s.t. \\ + & \phi(\underline{\theta}, \overline{\theta}) \leq 0 \\ + & \underline{\theta} = \theta^{N} - \delta \Delta \theta \\ + & \overline{\theta} = \theta^{N} + \delta \Delta \theta + +where :math:`\theta^{N}` is a "nominal" point. The goal of the FI is +to find the largest region around this nominal point for which the +performance constraints can be satisfied. As written, the FI searches +for the largest hyperrectangle, but other shapes (e.g., ellipses) can +be used. The hyperrectangle is all that is currently supported in the +flexibility analysis module in IDAES. Typically, :math:`\delta` is +bounded between 0 and 1. + +Usage +----- + +The flexibility analysis module within IDAES provides two primary +functions. The first is +:meth:`solve_flextest`. The +:class:`FlexTestConfig` +specifies how the flexibility test should be solved. + +.. [Grossmann1987] Grossmann, Ignacio E., and + Christodoulos A. Floudas. "Active constraint + strategy for flexibility analysis in chemical + processes." Computers & Chemical Engineering 11.6 + (1987): 675-693. diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst b/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst new file mode 100644 index 0000000000..412280eb61 --- /dev/null +++ b/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst @@ -0,0 +1,62 @@ +Flexibility Analysis Reference +============================== + +Table of Contents +----------------- +Enumerations + +* :py:enum:`FlexTestMethod` +* :py:enum:`FlexTestTermination` +* :py:enum:`DecisionRuleTypes` + +Configuration + +* :class:`FlexTestConfig` +* :class:`ActiveConstraintConfig` +* :class:`SamplingConfig` +* :class:`LinearDRConfig` +* :class:`ReluDRConfig` + +Results + +* :class:`FlexTestResults` + +Functions + +* :class:`solve_flextest` +* :class:`solve_flex_index` + +Enumerations +------------ + +.. autoenum:: idaes.apps.flexibility_analysis.FlexTestMethod + +.. autoenum:: idaes.apps.flexibility_analysis.FlexTestTermination + +.. autoenum:: idaes.apps.flexibility_analysis.DecisionRuleTypes + +Configuration +------------- + +.. autoclass:: idaes.apps.flexibility_analysis.FlexTestConfig + +.. autoclass:: idaes.apps.flexibility_analysis.ActiveConstraintConfig + +.. autoclass:: idaes.apps.flexibility_analysis.SamplingConfig + +.. autoclass:: idaes.apps.flexibility_analysis.LinearDRConfig + +.. autoclass:: idaes.apps.flexibility_analysis.ReluDRConfig + + +Results +------- + +.. autoclass:: idaes.apps.flexibility_analysis.FlexTestResults + +Functions +--------- + +.. autofunction:: idaes.apps.flexibility_analysis.solve_flextest + +.. autofunction:: idaes.apps.flexibility_analysis.solve_flex_index diff --git a/docs/explanations/modeling_extensions/index.rst b/docs/explanations/modeling_extensions/index.rst index e603091d71..57f48d5840 100644 --- a/docs/explanations/modeling_extensions/index.rst +++ b/docs/explanations/modeling_extensions/index.rst @@ -16,6 +16,7 @@ provided below. caprese/index uncertainty_propagation/index diagnostics/index + flexibility_analysis/index .. rubric:: PySMO: Python-based Surrogate Modeling Objects diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 692794f4c7..9e65faf54c 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -19,6 +19,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from .decision_rules.linear_dr import construct_linear_decision_rule +from .decision_rules.dr_config import DRConfig from pyomo.common.dependencies import attempt_import from .sampling import ( SamplingStrategy, @@ -35,6 +36,8 @@ MarkImmutable, NonNegativeFloat ) +from pyomo.contrib.appsi.base import Solver +from pyomo.opt.base import OptSolver relu_dr, relu_dr_available = attempt_import('idaes.apps.flexibility_analysis.decision_rules.relu_dr', 'The ReLU decision rule requires Tensorflow and OMLT') @@ -62,10 +65,15 @@ class FlexTestMethod(enum.Enum): active_constraint = enum.auto() linear_decision_rule = enum.auto() relu_decision_rule = enum.auto() - custom_decision_rule = enum.auto() vertex_enumeration = enum.auto() sampling = enum.auto() +FlexTestMethod.active_constraint.__doc__ = r"Solve the flexibility test using the active constraint method described in [Grossmann1987]_." +FlexTestMethod.linear_decision_rule.__doc__ = r"Solve the flexibility test by converting the inner minimization problem to a square problem by removing all degrees of freedom by creating a linear decision rule of the form :math:`z = A \theta + b`" +FlexTestMethod.relu_decision_rule.__doc__ = r"Solve the flexibility test by converting the inner minimization problem to a square problem by removing all degrees of freedom by creating a decision rule of the form :math:`z = f(\theta)` where :math:`f(\theta)` is a nueral network with ReLU activation functions." +FlexTestMethod.vertex_enumeration.__doc__ = r"Solve the flexibility test by solving the inner minimization problem at every vertex of the hyperrectangle defined by :math:`(\underline{\theta}, \overline{\theta})`." +FlexTestMethod.sampling.__doc__ = r"Solve the flexibility test by solving the inner minimization problem at random samples of :math:`\theta \in [\underline{\theta}, \overline{\theta}]`." + class ActiveConstraintConfig(ConfigDict): def __init__( @@ -101,20 +109,44 @@ def __init__( class FlexTestConfig(ConfigDict): - def __init__( - self, + r""" + A class for specifying options for solving the flexibility test. + + Attributes + ---------- + feasibility_tol: float + Tolerance for considering constraints to be satisfied. In particular, if the + maximum constraint violation is less than or equal to ``feasibility_tol``, then + the flexibility test passes. + terminate_early: bool + If True, the specified algorithm should terminate as soon as a point + (:math:`\theta`) is found that confirms the flexibility test fails. If + False, the specified algorithm will continue until the :math:`\theta` + that maximizes the constraint violation is found. + method: FlexTestMethod + The method that should be used to solve the flexibility test. + minlp_solver: Union[Solver, OptSolver] + A Pyomo solver interface appropriate for solving MINLPs + sampling_config: SamplingConfig + A config object for specifying how sampling should be performed when either + generating data to create a decision rule or using sampling to solve the + flexibility test. + decision_rule_config: DRConfig + Only used if method is one of the decision rules. Should be either a LinearDRConfig + or a ReluDRConfig. + active_constraint_config: ActiveConstraintConfig + Only used if method is FlexTestMethod.active_constraint + total_violation: bool + If False, the maximum constraint violation is considered. If True, the sum + of the violations of all constraints is considered. Should normally be False + """ + def __init__(self): + super().__init__( description=None, doc=None, implicit=False, implicit_domain=None, visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, ) self.feasibility_tol: float = self.declare( "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-6) @@ -544,6 +576,34 @@ def solve_flextest( in_place: bool = False, config: Optional[FlexTestConfig] = None, ) -> FlexTestResults: + r""" + Parameters + ---------- + m: _BlockData + The pyomo model to be used for the feasibility/flexibility test. + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]] + A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). + These can be pyomo variables (Var) or parameters (param). However, if parameters are used, + they must be mutable. + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + nominal values (:math:`\theta^{N}`). + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`). + controls: Sequence[_GeneralVarData] + A sequence (e.g., list) defining the set of control variables (:math:`z`). + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]] + A mapping (e.g., ComponentMap) defining bounds for all variables (:math:`x` and :math:`z`) that + should be valid for any :math:`\theta` between :math:`\underline{\theta}` and + :math:`\overline{\theta}`. These are only used to make the resulting flexibility test problem + more computationally tractable. + in_place: bool + If True, m is modified in place to generate the model for solving the flexibility test. If False, + the model is cloned first. + config: Optional[FlexTestConfig] + An object defining options for how the flexibility test should be solved. + """ if config is None: config = FlexTestConfig() From 597050450b64019981c9fe44b9ac3f9594dedf8e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 9 Jan 2024 06:15:13 -0700 Subject: [PATCH 22/60] flexibility analysis docs --- .../flexibility_analysis/overview.rst | 69 +++++++++++++++++++ .../flexibility_analysis/reference.rst | 7 +- idaes/apps/flexibility_analysis/flextest.py | 21 ++++-- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst index c39b469099..940297f008 100644 --- a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst +++ b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst @@ -2,6 +2,13 @@ Flexibility Analysis Overview ============================= +.. contents:: + :depth: 3 + :local: + +Introduction +------------ + The flexibility analysis module within IDAES provides a framework for evaluating how well a given system performs with respect to a set of uncertain parameters. Two methods are provided. The flexibility (or feasibility) test @@ -13,6 +20,8 @@ satisfied [Grossmann1987]_. The FT is given by +.. _FT: + .. math:: \phi(\underline{\theta}, \overline{\theta}) = &\max_{\theta} \min_{z} u \\ @@ -59,6 +68,61 @@ be used. The hyperrectangle is all that is currently supported in the flexibility analysis module in IDAES. Typically, :math:`\delta` is bounded between 0 and 1. +Flexibility Test Solution Methods +--------------------------------- + +Vertex Enumeration +^^^^^^^^^^^^^^^^^^ + +Vertex enumeration solves the inner minimization problem + +.. _innerProblem: + +.. math:: + + & \min_{z} u \\ + & s.t. \\ + & g_{j}(x,z,\theta) \leq u \\ + & h(x,z,\theta) = 0 + +at each vertex of the hyperrectangle :math:`[\underline{\theta}, \overline{\theta}]`. For certain problem types (e.g., linear), the solution to :ref:`FT` is guaranteed to be at one of the vertices of this hyperrectangle [Swaney1985]_. For other problem types, vertex enumeration is only a heuristic that may not find the value of :math:`\theta` that maximizes the violation of the performance constraints. + +Active Constraint Method +^^^^^^^^^^^^^^^^^^^^^^^^ + +The active constraint method converts the bilevel problem given by :ref:`FT` to a single-level problem by formulating the KKT conditions of the :ref:`inner minimization problem` and embedding them as constraints in the outer problem [Grossmann1987]_. Note that this method assumes the Haar Conditions hold and a constraint is added enforcing that the number of active inequalities (i.e., :math:`g_{j}(x,z,\theta) = u`) is equal to the number of controls plus one. If the resulting single-level problem is solved to global optimality, this method will be conservative because it will find the "worst" local minima of the inner minimization problem. + +Sampling +^^^^^^^^ + +Sampling is similar to `Vertex Enumeration`_ except that the :ref:`inner minimization problem` is solved at random samples of :math:`\theta` rather than only at the vertices of :math:`[\underline{\theta}, \overline{\theta}]`. This can be useful for nonlinear problems but still may miss the worst-case :math:`\theta`. + +Decision Rules +^^^^^^^^^^^^^^ + +Decision rules can be used to convert the bilevel problem given by :ref:`FT` to a single-level problem by removing all degrees of freedom of the inner problem with a control policy. Suppose we have a decision rule give by :math:`z = d(\theta)`. Because the only degrees of freedom in the inner problem are :math:`z`, the :ref:`FT` may be reformulated as + +.. math:: + + & \max_{\theta} \overline{u} \\ + & s.t. \\ + & g_{j}(x,z,\theta) = u_{j} \\ + & h(x,z,\theta) = 0 \\ + & \overline{u} = \sum u_{j} y_{j} \\ + & \sum y_{j} = 1 \\ + & z = d(\theta) \\ + & \underline{\theta} \leq \theta \leq \overline{\theta} + +Currently, the two types of decision rules supported are linear decision rules and neural network decision rules with ReLU activation functions. Because the decision rules result in suboptimal values of :math:`z`, this method is conservative. + +Flexibility Index Solution Methods +---------------------------------- + +Bisection Method +^^^^^^^^^^^^^^^^ + +The bisection method simply uses bisection to find the :math:`\delta` such that :math:`\phi(\underline{\theta}, \overline{\theta}) = 0` (:math:`\phi(\underline{\theta}, \overline{\theta})` is monotonically increasing with :math:`\delta`). Each subproblem solves the :ref:`FT` using one of the methods described above. + Usage ----- @@ -73,3 +137,8 @@ specifies how the flexibility test should be solved. strategy for flexibility analysis in chemical processes." Computers & Chemical Engineering 11.6 (1987): 675-693. + +.. [Swaney1985] Swaney, Ross Edward, and Ignacio E. Grossmann. + "An index for operational flexibility in chemical + process design. Part I: Formulation and theory." + AIChE Journal 31.4 (1985): 621-630. diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst b/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst index 412280eb61..b7ea37608f 100644 --- a/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst +++ b/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst @@ -7,7 +7,6 @@ Enumerations * :py:enum:`FlexTestMethod` * :py:enum:`FlexTestTermination` -* :py:enum:`DecisionRuleTypes` Configuration @@ -23,8 +22,8 @@ Results Functions -* :class:`solve_flextest` -* :class:`solve_flex_index` +* :meth:`solve_flextest` +* :meth:`solve_flex_index` Enumerations ------------ @@ -33,8 +32,6 @@ Enumerations .. autoenum:: idaes.apps.flexibility_analysis.FlexTestTermination -.. autoenum:: idaes.apps.flexibility_analysis.DecisionRuleTypes - Configuration ------------- diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 9e65faf54c..4ccffa5f90 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -116,15 +116,15 @@ class FlexTestConfig(ConfigDict): ---------- feasibility_tol: float Tolerance for considering constraints to be satisfied. In particular, if the - maximum constraint violation is less than or equal to ``feasibility_tol``, then - the flexibility test passes. + maximum constraint violation is less than or equal to :py:attr:`feasibility_tol`, then + the flexibility test passes. (default: 1e-6) terminate_early: bool If True, the specified algorithm should terminate as soon as a point (:math:`\theta`) is found that confirms the flexibility test fails. If False, the specified algorithm will continue until the :math:`\theta` - that maximizes the constraint violation is found. + that maximizes the constraint violation is found. (default: False) method: FlexTestMethod - The method that should be used to solve the flexibility test. + The method that should be used to solve the flexibility test. (default: :py:attr:`active_constraint`) minlp_solver: Union[Solver, OptSolver] A Pyomo solver interface appropriate for solving MINLPs sampling_config: SamplingConfig @@ -135,10 +135,10 @@ class FlexTestConfig(ConfigDict): Only used if method is one of the decision rules. Should be either a LinearDRConfig or a ReluDRConfig. active_constraint_config: ActiveConstraintConfig - Only used if method is FlexTestMethod.active_constraint + Only used if :py:attr:`method` is :py:attr:`active_constraint` total_violation: bool If False, the maximum constraint violation is considered. If True, the sum - of the violations of all constraints is considered. Should normally be False + of the violations of all constraints is considered. Should normally be False. (default: False) """ def __init__(self): super().__init__( @@ -182,6 +182,10 @@ class FlexTestTermination(enum.Enum): proven_feasible = enum.auto() uncertain = enum.auto() +FlexTestTermination.found_infeasible_point.__doc__ = r"The meaning of this member depends on the method used to solve the flexibility/feasibility test, but it generally means that the flexibility test failed. If the solution method is not conservative (:py:attr:`FlexTestMethod.vertex_enumeration`, :py:attr:`FlexTestMethod.sampling`), then :py:attr:`FlexTestTermination.found_infeasible_point` indicates that a value of :math:`\theta` was found where at least one performance constraint (:math:`g_{j}(x, z, \theta) \leq 0`) is violated. Otherwise, :py:attr:`FlexTestTermination.found_infeasible_point` indicates that a point was found where the performance constraints might be violated." +FlexTestTermination.proven_feasible.__doc__ = r"The meaning of this member depends on the method used to solve the flexibility/feasibility test, but it generally means that the flexibility test passed. If the solution method is conservative (:py:attr:`FlexTestMethod.active_constraint`, :py:attr:`FlexTestMethod.linear_decision_rule`, :py:attr:`FlexTestMethod.relu_decision_rule`), then :py:attr:`FlexTestTermination.proven_feasible` indicates that, for any :math:`\theta \in [\underline{\theta}, \overline{\theta}]`, there exists a :math:`z` such that all of the performance constraints (:math:`g_{j}(x, z, \theta) \leq 0`) are satisfied. Otherwise, :py:attr:`FlexTestTermination.proven_feasible` just indicates that no :math:`\theta \in [\underline{\theta}, \overline{\theta}]` was found that violates the performance constraints for all :math:`z`." +FlexTestTermination.uncertain.__doc__ = r"Cannot definitively say whether the flexibility test passes or fails. This usually indicates an error was encountered." + class FlexTestResults(object): def __init__(self): @@ -597,7 +601,10 @@ def solve_flextest( A mapping (e.g., ComponentMap) defining bounds for all variables (:math:`x` and :math:`z`) that should be valid for any :math:`\theta` between :math:`\underline{\theta}` and :math:`\overline{\theta}`. These are only used to make the resulting flexibility test problem - more computationally tractable. + more computationally tractable. All variable bounds in the model `m` are treated as performance + constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` + are applied to the single-level problem generated from the active constraint method or one of + the decision rules. This argument is not necessary for vertex enumeration or sampling. in_place: bool If True, m is modified in place to generate the model for solving the flexibility test. If False, the model is cloned first. From accd15d9f9bb8138c302cd91b644969bd33ef3d5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 9 Jan 2024 15:59:43 -0700 Subject: [PATCH 23/60] flexibility analysis docs --- idaes/apps/flexibility_analysis/flex_index.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index f1f35de049..8b9ca37516 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -72,6 +72,54 @@ def solve_flex_index( config: Optional[FlexTestConfig] = None, log_level: int = logging.INFO, ) -> float: + r""" + Use bisection to solve the flexibility index problem. + + Parameters + ---------- + m: _BlockData + The pyomo model to be used for the feasibility/flexibility test. + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]] + A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). + These can be pyomo variables (Var) or parameters (param). However, if parameters are used, + they must be mutable. + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + nominal values (:math:`\theta^{N}`). + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`). + controls: Sequence[_GeneralVarData] + A sequence (e.g., list) defining the set of control variables (:math:`z`). + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]] + A mapping (e.g., ComponentMap) defining bounds for all variables (:math:`x` and :math:`z`) that + should be valid for any :math:`\theta` between :math:`\underline{\theta}` and + :math:`\overline{\theta}`. These are only used to make the resulting flexibility test problem + more computationally tractable. All variable bounds in the model `m` are treated as performance + constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` + are applied to the single-level problem generated from the active constraint method or one of + the decision rules. This argument is not necessary for vertex enumeration or sampling. + in_place: bool + If True, m is modified in place to generate the model for solving the flexibility test. If False, + the model is cloned first. + cap_index_at_1: bool + If False, the flexibility index (:math:`\delta`) will be allowed to be larger than 1. Otherwise, + it will be between 0 and 1. (default: True) + reconstruct_decision_rule: Optional[bool] + If True, the decision rule will be re-trained for every flexibility test subproblem solved in the + bisection method. + config: Optional[FlexTestConfig] + An object defining options for how the flexibility test should be solved for each subproblem + in the bisection method. + log_level: int + The level at which to log progress (default: logging.INFO) + + Returns + ------- + index: float + The flexibility index, :math:`\delta` + """ + original_uncertain_params = uncertain_params original_param_nominal_values = param_nominal_values original_param_bounds = param_bounds From 4c5a41037994b9b01f7097ae1f739036302316a8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 30 Jan 2024 14:36:32 -0700 Subject: [PATCH 24/60] working on docs --- .../flexibility_analysis/overview.rst | 7 ++- .../flexibility_analysis/reference.rst | 8 +++ idaes/apps/flexibility_analysis/__init__.py | 2 +- .../decision_rules/dr_config.py | 4 ++ .../decision_rules/linear_dr.py | 9 ++++ .../decision_rules/relu_dr_config.py | 27 ++++++++++ idaes/apps/flexibility_analysis/flextest.py | 54 ++++++++++++++----- idaes/apps/flexibility_analysis/sampling.py | 51 +++++++++++++----- 8 files changed, 132 insertions(+), 30 deletions(-) diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst index 940297f008..961c2ad913 100644 --- a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst +++ b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst @@ -121,7 +121,7 @@ Flexibility Index Solution Methods Bisection Method ^^^^^^^^^^^^^^^^ -The bisection method simply uses bisection to find the :math:`\delta` such that :math:`\phi(\underline{\theta}, \overline{\theta}) = 0` (:math:`\phi(\underline{\theta}, \overline{\theta})` is monotonically increasing with :math:`\delta`). Each subproblem solves the :ref:`FT` using one of the methods described above. +The bisection method simply uses bisection to find the :math:`\delta` such that :math:`\phi(\underline{\theta}, \overline{\theta}) = 0`. Bisection works because :math:`\phi(\underline{\theta}, \overline{\theta})` is monotonically increasing with :math:`\delta`. Each subproblem solves the :ref:`FT` using one of the methods described above. Usage ----- @@ -130,7 +130,10 @@ The flexibility analysis module within IDAES provides two primary functions. The first is :meth:`solve_flextest`. The :class:`FlexTestConfig` -specifies how the flexibility test should be solved. +specifies how the flexibility test should be solved. The second is +:meth:`solve_flextest`. Examples +can be found `here +`_. .. [Grossmann1987] Grossmann, Ignacio E., and Christodoulos A. Floudas. "Active constraint diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst b/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst index b7ea37608f..fedd09bdd4 100644 --- a/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst +++ b/docs/explanations/modeling_extensions/flexibility_analysis/reference.rst @@ -7,6 +7,8 @@ Enumerations * :py:enum:`FlexTestMethod` * :py:enum:`FlexTestTermination` +* :py:enum:`SamplingStrategy` +* :py:enum:`SamplingIniStrategy` Configuration @@ -32,6 +34,10 @@ Enumerations .. autoenum:: idaes.apps.flexibility_analysis.FlexTestTermination +.. autoenum:: idaes.apps.flexibility_analysis.SamplingStrategy + +.. autoenum:: idaes.apps.flexibility_analysis.SamplingInitStrategy + Configuration ------------- @@ -41,6 +47,8 @@ Configuration .. autoclass:: idaes.apps.flexibility_analysis.SamplingConfig +.. autoclass:: idaes.apps.flexibility_analysis.decision_rules.dr_config.DRConfig + .. autoclass:: idaes.apps.flexibility_analysis.LinearDRConfig .. autoclass:: idaes.apps.flexibility_analysis.ReluDRConfig diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index 83e8d7c5a6..8808126ab3 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -15,4 +15,4 @@ from .decision_rules.linear_dr import LinearDRConfig from .decision_rules.relu_dr_config import ReluDRConfig from .flex_index import solve_flex_index -from .sampling import perform_sampling +from .sampling import perform_sampling, SamplingInitStrategy diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py index 6e1c1403bf..f00e8d4e5d 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py @@ -2,6 +2,10 @@ class DRConfig(ConfigDict): + r""" + A base class for specifying options for building + decision rules. + """ def __init__( self, description=None, diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index 57ab0ab153..b9bdaf975e 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -9,6 +9,15 @@ class LinearDRConfig(DRConfig): + r""" + A class for specifying options for constructing linear decision rules + for use in the flexibility test problem. + + Attributes + ---------- + solver: Union[Solver, OptSolver] + The solver to use for building the linear decision rule (an LP solver). + """ def __init__( self, description=None, diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py index 2a034f53bb..c49fc16ac7 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py @@ -3,6 +3,33 @@ class ReluDRConfig(DRConfig): + r""" + A class for specifying options for constructing neural network based decision rules + for use in the flexibility test problem. + + Attributes + ---------- + n_layers: int + The number of layers in the neural network (default=: 4) + n_nodes_per_layer: int + The number of nodes in each layer of the neural network (default: 4) + tensorflow_seed: int + The seed to pass to tensorflow during training + scale_inputs: bool + If False, the inputs to the neural network (uncertain parameter values) + will not be scaled for training (default: True) + scale_outputs: bool + If False, the outputs to the neural network (controls) + will not be scaled for training (default: True) + epochs: int + The number of epochs to use in training the neural network (default: 2000) + batch_size: int + The batch size to use in training the neural network (default: 20) + learning_rate: float + The learning rate for training the neural network (default: None) + plot_history: bool + If True, the training history will be plotted (default: False) + """ def __init__( self, description=None, diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 4ccffa5f90..fdb1247184 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -76,20 +76,35 @@ class FlexTestMethod(enum.Enum): class ActiveConstraintConfig(ConfigDict): - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): + r""" + A class for specifying options for the active constraint method for the + flexibility test problem. + + Attributes + ---------- + use_haar_conditions: bool + If False, no constraint will be added to constraint the number of + active inequalities. (default: True) + default_BigM: float + Default value for the bigM parameter used to reformulate the + complimentarity conditions in the KKT system. (default: None) + enforce_equalities: bool + If False, :math:`h(x, z, \theta) = 0` is treated as two inequalities + (performance constraints) that can be violated (:math:`h_{i}(x, z, \theta) \leq u` + and :math:`-h_{i}(x, z, \theta) \leq u`) (default: True) + skip_scaling_check: bool + If True, the model scaling will not be checked. (default: False) + total_violation: bool + If True, the objective of the flexibility test will be the sum of + the constraint violations instead of the maximum violation. (default: False) + """ + def __init__(self): super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, ) self.use_haar_conditions: bool = self.declare( 'use_haar_conditions', ConfigValue(domain=bool, default=True) @@ -98,7 +113,7 @@ def __init__( 'default_BigM', ConfigValue(domain=NonNegativeFloat, default=None) ) self.enforce_equalities: bool = self.declare( - 'enforce_equalities', ConfigValue(domain=bool, default=False) + 'enforce_equalities', ConfigValue(domain=bool, default=True) ) self.skip_scaling_check: bool = self.declare( 'skip_scaling_check', ConfigValue(domain=bool, default=False) @@ -188,6 +203,17 @@ class FlexTestTermination(enum.Enum): class FlexTestResults(object): + r""" + Results for the flexibility test problem. + + Attributes + ---------- + termination: FlexTestTermination + max_constraint_violation: float + The largest constraint violation found (:math:`u`) + unc_param_values_at_max_violation: Optional[MutableMapping[Union[_GeneralVarData, _ParamData], float]] + The values of the uncertain parameters that generated the maximum constraint violation + """ def __init__(self): self.termination = FlexTestTermination.uncertain self.max_constraint_violation: Optional[float] = None diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 0e47c21046..6634bcb455 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -28,6 +28,9 @@ class SamplingStrategy(enum.Enum): grid = 'grid' lhs = 'lhs' +SamplingStrategy.grid.__doc__ = r"Use evenly spaced samples" +SamplingStrategy.lhs.__doc__ = r"Use latin hypercube sampling" + class SamplingInitStrategy(enum.Enum): none = 'none' @@ -35,6 +38,11 @@ class SamplingInitStrategy(enum.Enum): min_control_deviation = 'min_control_deviation' all = 'all' +SamplingInitStrategy.none.__doc__ = r"Use the solution from the previous sample to initialize the inner problem" +SamplingInitStrategy.square.__doc__ = r"Fix the controls and solve a square problem to initialize the inner problem" +SamplingInitStrategy.min_control_deviation.__doc__ = r"Fix the maximum constraint violation to 0 and minimized the square of the differences between the controls and their current values" +SamplingInitStrategy.all.__doc__ = r"Try both square and min_control_deviation" + class _GridSamplingState(enum.Enum): increment = 'increment' @@ -169,20 +177,37 @@ def _lhs_sampling( class SamplingConfig(ConfigDict): - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): + r""" + A class for specifying options for sampling the uncertain parameter values + and solving the inner problem of the flexibility test. + + Attributes + ---------- + strategy: SamplingStrategy + The method for sampling the uncertain parameters. (default: SamplingStrategy.lhs) + lhs_seed: int + The seed used for latin hypercube sampling (default: 0) + solver: Union[Solver, OptSolver] + The solver to use for the inner problem of the flexibility test problem + num_points: int + The number of samples of uncertain parameter values to use (default: 100) + enable_progress_bar: bool + If False, no progress bar will be shown (default: True) + initialization_strategy: SamplingInitStrategy + The initialization strategy to use for the inner problems of the + flexibility test at each sample of the uncertain parameter values. + (default: SamplingInitStrategy.none) + total_violation: bool + If True, the objective of the flexibility test will be the sum of + the constraint violations instead of the maximum violation. (default: False) + """ + def __init__(self): super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, ) self.strategy: SamplingStrategy = self.declare( "strategy", From 795bd3a9e193b469fdb82d695b9912e68ba864af Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 09:22:01 -0600 Subject: [PATCH 25/60] run black --- idaes/apps/flexibility_analysis/__init__.py | 2 +- .../_check_dependencies.py | 7 +- .../_check_relu_dependencies.py | 7 +- .../flexibility_analysis/check_optimal.py | 2 +- .../decision_rules/dr_config.py | 3 +- .../decision_rules/linear_dr.py | 1 + .../decision_rules/relu_dr.py | 2 +- .../decision_rules/relu_dr_config.py | 9 +- .../decision_rules/tests/test_linear_dr.py | 4 +- .../examples/idaes_hx_network.py | 5 +- .../examples/linear_hx_network.py | 22 +++-- .../examples/tests/test_examples.py | 83 +++++++++++++--- idaes/apps/flexibility_analysis/flex_index.py | 20 ++-- idaes/apps/flexibility_analysis/flextest.py | 98 +++++++++++-------- .../flexibility_analysis/inner_problem.py | 85 +++++++++++----- idaes/apps/flexibility_analysis/kkt.py | 5 +- idaes/apps/flexibility_analysis/sampling.py | 70 ++++++++----- .../tests/test_flextest.py | 6 +- .../tests/test_sampling.py | 14 ++- idaes/apps/flexibility_analysis/var_utils.py | 5 +- 20 files changed, 301 insertions(+), 149 deletions(-) diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index 8808126ab3..2689685351 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -9,7 +9,7 @@ FlexTest, ActiveConstraintConfig, build_active_constraint_flextest, - build_flextest_with_dr + build_flextest_with_dr, ) from .decision_rules.dr_enum import DecisionRuleTypes from .decision_rules.linear_dr import LinearDRConfig diff --git a/idaes/apps/flexibility_analysis/_check_dependencies.py b/idaes/apps/flexibility_analysis/_check_dependencies.py index 7c3487806f..de7355310b 100644 --- a/idaes/apps/flexibility_analysis/_check_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_dependencies.py @@ -1,6 +1,7 @@ from pyomo.common.dependencies import attempt_import import unittest -coramin, coramin_available = attempt_import('coramin') -np, nump_available = attempt_import('numpy') + +coramin, coramin_available = attempt_import("coramin") +np, nump_available = attempt_import("numpy") if not coramin_available or not nump_available: - raise unittest.SkipTest('flexibility_analysis tests require coramin and numpy') + raise unittest.SkipTest("flexibility_analysis tests require coramin and numpy") diff --git a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py index ba5db2630a..9e69936df4 100644 --- a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py @@ -1,6 +1,7 @@ from pyomo.common.dependencies import attempt_import import unittest -tensorflow, tensorflow_available = attempt_import('tensorflow') -omlt, nump_available = attempt_import('omlt') + +tensorflow, tensorflow_available = attempt_import("tensorflow") +omlt, nump_available = attempt_import("omlt") if not tensorflow_available or not nump_available: - raise unittest.SkipTest('flexibility_analysis tests require tensorflow and omlt') + raise unittest.SkipTest("flexibility_analysis tests require tensorflow and omlt") diff --git a/idaes/apps/flexibility_analysis/check_optimal.py b/idaes/apps/flexibility_analysis/check_optimal.py index 2c41ee4f97..2b88a88fa3 100644 --- a/idaes/apps/flexibility_analysis/check_optimal.py +++ b/idaes/apps/flexibility_analysis/check_optimal.py @@ -3,7 +3,7 @@ def assert_optimal_termination(results): - if hasattr(results, 'termination_condition'): + if hasattr(results, "termination_condition"): assert results.termination_condition == appsi.base.TerminationCondition.optimal else: pe.assert_optimal_termination(results) diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py index f00e8d4e5d..02bc176b5d 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py @@ -3,9 +3,10 @@ class DRConfig(ConfigDict): r""" - A base class for specifying options for building + A base class for specifying options for building decision rules. """ + def __init__( self, description=None, diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index b9bdaf975e..7b04cabb30 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -18,6 +18,7 @@ class LinearDRConfig(DRConfig): solver: Union[Solver, OptSolver] The solver to use for building the linear decision rule (an LP solver). """ + def __init__( self, description=None, diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index 009877ce72..d55161573e 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -76,7 +76,7 @@ def construct_relu_decision_rule( training_output, batch_size=config.batch_size, epochs=config.epochs, - #verbose=0, + # verbose=0, ) if config.plot_history: diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py index c49fc16ac7..4934980696 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py @@ -16,10 +16,10 @@ class ReluDRConfig(DRConfig): tensorflow_seed: int The seed to pass to tensorflow during training scale_inputs: bool - If False, the inputs to the neural network (uncertain parameter values) + If False, the inputs to the neural network (uncertain parameter values) will not be scaled for training (default: True) scale_outputs: bool - If False, the outputs to the neural network (controls) + If False, the outputs to the neural network (controls) will not be scaled for training (default: True) epochs: int The number of epochs to use in training the neural network (default: 2000) @@ -30,6 +30,7 @@ class ReluDRConfig(DRConfig): plot_history: bool If True, the training history will be plotted (default: False) """ + def __init__( self, description=None, @@ -64,9 +65,7 @@ def __init__( self.batch_size: int = self.declare( "batch_size", ConfigValue(domain=int, default=20) ) - self.learning_rate = self.declare( - "learning_rate", ConfigValue(default=None) - ) + self.learning_rate = self.declare("learning_rate", ConfigValue(default=None)) self.plot_history: bool = self.declare( "plot_history", ConfigValue(domain=bool, default=False) ) diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index 26cf7fd020..8218783319 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -3,7 +3,7 @@ import pyomo.environ as pe from idaes.apps.flexibility_analysis.decision_rules.linear_dr import ( construct_linear_decision_rule, - LinearDRConfig + LinearDRConfig, ) import numpy as np import pytest @@ -44,7 +44,7 @@ def test_construct_linear_dr(self): output_vals[m.y1] = [float(i) for i in y1_samples] output_vals[m.y2] = [float(i) for i in y2_samples] - opt = pe.SolverFactory('appsi_gurobi') + opt = pe.SolverFactory("appsi_gurobi") config = LinearDRConfig() config.solver = opt m.dr = construct_linear_decision_rule( diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 4887568fcf..c9297a8e21 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -5,7 +5,10 @@ from idaes.core import FlowsheetBlock from idaes.models.unit_models.heater import Heater from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') + +coramin, coramin_available = attempt_import( + "coramin", "coramin is required for flexibility analysis" +) import logging from pyomo.contrib.fbbt.fbbt import fbbt from pyomo.network import Arc diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 81ee0aa677..6486eb681d 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -72,7 +72,11 @@ def get_var_bounds(m): return res -def main(flex_index: bool = False, method: flexibility.FlexTestMethod = flexibility.FlexTestMethod.active_constraint, plot_history=True): +def main( + flex_index: bool = False, + method: flexibility.FlexTestMethod = flexibility.FlexTestMethod.active_constraint, + plot_history=True, +): np.random.seed(0) random.seed(1) m, nominal_values, param_bounds = create_model() @@ -83,9 +87,9 @@ def main(flex_index: bool = False, method: flexibility.FlexTestMethod = flexibil config.method = method config.minlp_solver = pe.SolverFactory("gurobi_direct") config.sampling_config.solver = pe.SolverFactory("appsi_gurobi") - config.sampling_config.strategy = 'lhs' + config.sampling_config.strategy = "lhs" config.sampling_config.num_points = 600 - config.sampling_config.initialization_strategy = 'square' + config.sampling_config.initialization_strategy = "square" if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") @@ -100,9 +104,15 @@ def main(flex_index: bool = False, method: flexibility.FlexTestMethod = flexibil config.decision_rule_config.plot_history = plot_history config.decision_rule_config.tensorflow_seed = 2 if not flex_index: - results = flexibility.solve_flextest(m=m, uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, param_bounds=param_bounds, - controls=[m.qc], valid_var_bounds=var_bounds, config=config) + results = flexibility.solve_flextest( + m=m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + valid_var_bounds=var_bounds, + config=config, + ) print(results) else: results = flexibility.solve_flex_index( diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index 9afd8b5e80..cc5149f9c0 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -1,6 +1,13 @@ -from idaes.apps.flexibility_analysis import _check_dependencies, _check_relu_dependencies +from idaes.apps.flexibility_analysis import ( + _check_dependencies, + _check_relu_dependencies, +) import idaes.apps.flexibility_analysis as flex -from idaes.apps.flexibility_analysis.examples import linear_hx_network, nonlin_hx_network, idaes_hx_network +from idaes.apps.flexibility_analysis.examples import ( + linear_hx_network, + nonlin_hx_network, + idaes_hx_network, +) import unittest import pytest @@ -8,33 +15,77 @@ @pytest.mark.unit class TestExamples(unittest.TestCase): def test_linear_hx_network(self): - res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.active_constraint, plot_history=False) - self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.active_constraint, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) self.assertAlmostEqual(res.max_constraint_violation, 4, 5) - res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.vertex_enumeration, plot_history=False) - self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.vertex_enumeration, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) - res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.linear_decision_rule, plot_history=False) - self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.linear_decision_rule, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) - res = linear_hx_network.main(flex_index=False, method=flex.FlexTestMethod.relu_decision_rule, plot_history=False) - self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.relu_decision_rule, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) - res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.active_constraint, plot_history=False) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.active_constraint, + plot_history=False, + ) self.assertAlmostEqual(res, 0.5, 5) - res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.vertex_enumeration, plot_history=False) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.vertex_enumeration, + plot_history=False, + ) self.assertAlmostEqual(res, 0.5, 5) - res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.linear_decision_rule, plot_history=False) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.linear_decision_rule, + plot_history=False, + ) self.assertLessEqual(res, 0.5) - res = linear_hx_network.main(flex_index=True, method=flex.FlexTestMethod.relu_decision_rule, plot_history=False) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.relu_decision_rule, + plot_history=False, + ) self.assertAlmostEqual(res, 0.5, 2) def test_idaes_hx_network(self): res = idaes_hx_network.main(flex.FlexTestMethod.sampling) - self.assertEqual(res.termination, flex.FlexTestTermination.found_infeasible_point) - self.assertAlmostEqual(res.max_constraint_violation, 0.375170890924453) # regression + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) + self.assertAlmostEqual( + res.max_constraint_violation, 0.375170890924453 + ) # regression diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index 8b9ca37516..919373a45c 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -80,9 +80,9 @@ def solve_flex_index( m: _BlockData The pyomo model to be used for the feasibility/flexibility test. uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]] - A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). + A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). These can be pyomo variables (Var) or parameters (param). However, if parameters are used, - they must be mutable. + they must be mutable. param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float] A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their nominal values (:math:`\theta^{N}`). @@ -93,23 +93,23 @@ def solve_flex_index( A sequence (e.g., list) defining the set of control variables (:math:`z`). valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]] A mapping (e.g., ComponentMap) defining bounds for all variables (:math:`x` and :math:`z`) that - should be valid for any :math:`\theta` between :math:`\underline{\theta}` and + should be valid for any :math:`\theta` between :math:`\underline{\theta}` and :math:`\overline{\theta}`. These are only used to make the resulting flexibility test problem - more computationally tractable. All variable bounds in the model `m` are treated as performance - constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` - are applied to the single-level problem generated from the active constraint method or one of + more computationally tractable. All variable bounds in the model `m` are treated as performance + constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` + are applied to the single-level problem generated from the active constraint method or one of the decision rules. This argument is not necessary for vertex enumeration or sampling. in_place: bool - If True, m is modified in place to generate the model for solving the flexibility test. If False, + If True, m is modified in place to generate the model for solving the flexibility test. If False, the model is cloned first. cap_index_at_1: bool - If False, the flexibility index (:math:`\delta`) will be allowed to be larger than 1. Otherwise, + If False, the flexibility index (:math:`\delta`) will be allowed to be larger than 1. Otherwise, it will be between 0 and 1. (default: True) reconstruct_decision_rule: Optional[bool] - If True, the decision rule will be re-trained for every flexibility test subproblem solved in the + If True, the decision rule will be re-trained for every flexibility test subproblem solved in the bisection method. config: Optional[FlexTestConfig] - An object defining options for how the flexibility test should be solved for each subproblem + An object defining options for how the flexibility test should be solved for each subproblem in the bisection method. log_level: int The level at which to log progress (default: logging.INFO) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index fdb1247184..03314e194a 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -2,7 +2,10 @@ from .kkt import add_kkt_with_milp_complementarity_conditions from pyomo.core.base.block import _BlockData from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') + +coramin, coramin_available = attempt_import( + "coramin", "coramin is required for flexibility analysis" +) import pyomo.environ as pe from .var_utils import ( get_used_unfixed_variables, @@ -34,12 +37,15 @@ PositiveFloat, InEnum, MarkImmutable, - NonNegativeFloat + NonNegativeFloat, ) from pyomo.contrib.appsi.base import Solver from pyomo.opt.base import OptSolver -relu_dr, relu_dr_available = attempt_import('idaes.apps.flexibility_analysis.decision_rules.relu_dr', - 'The ReLU decision rule requires Tensorflow and OMLT') + +relu_dr, relu_dr_available = attempt_import( + "idaes.apps.flexibility_analysis.decision_rules.relu_dr", + "The ReLU decision rule requires Tensorflow and OMLT", +) logger = logging.getLogger(__name__) @@ -68,6 +74,7 @@ class FlexTestMethod(enum.Enum): vertex_enumeration = enum.auto() sampling = enum.auto() + FlexTestMethod.active_constraint.__doc__ = r"Solve the flexibility test using the active constraint method described in [Grossmann1987]_." FlexTestMethod.linear_decision_rule.__doc__ = r"Solve the flexibility test by converting the inner minimization problem to a square problem by removing all degrees of freedom by creating a linear decision rule of the form :math:`z = A \theta + b`" FlexTestMethod.relu_decision_rule.__doc__ = r"Solve the flexibility test by converting the inner minimization problem to a square problem by removing all degrees of freedom by creating a decision rule of the form :math:`z = f(\theta)` where :math:`f(\theta)` is a nueral network with ReLU activation functions." @@ -77,27 +84,28 @@ class FlexTestMethod(enum.Enum): class ActiveConstraintConfig(ConfigDict): r""" - A class for specifying options for the active constraint method for the + A class for specifying options for the active constraint method for the flexibility test problem. Attributes ---------- use_haar_conditions: bool - If False, no constraint will be added to constraint the number of + If False, no constraint will be added to constraint the number of active inequalities. (default: True) default_BigM: float - Default value for the bigM parameter used to reformulate the + Default value for the bigM parameter used to reformulate the complimentarity conditions in the KKT system. (default: None) enforce_equalities: bool If False, :math:`h(x, z, \theta) = 0` is treated as two inequalities - (performance constraints) that can be violated (:math:`h_{i}(x, z, \theta) \leq u` + (performance constraints) that can be violated (:math:`h_{i}(x, z, \theta) \leq u` and :math:`-h_{i}(x, z, \theta) \leq u`) (default: True) skip_scaling_check: bool If True, the model scaling will not be checked. (default: False) total_violation: bool - If True, the objective of the flexibility test will be the sum of + If True, the objective of the flexibility test will be the sum of the constraint violations instead of the maximum violation. (default: False) """ + def __init__(self): super().__init__( description=None, @@ -107,16 +115,16 @@ def __init__(self): visibility=0, ) self.use_haar_conditions: bool = self.declare( - 'use_haar_conditions', ConfigValue(domain=bool, default=True) + "use_haar_conditions", ConfigValue(domain=bool, default=True) ) self.default_BigM: Optional[float] = self.declare( - 'default_BigM', ConfigValue(domain=NonNegativeFloat, default=None) + "default_BigM", ConfigValue(domain=NonNegativeFloat, default=None) ) self.enforce_equalities: bool = self.declare( - 'enforce_equalities', ConfigValue(domain=bool, default=True) + "enforce_equalities", ConfigValue(domain=bool, default=True) ) self.skip_scaling_check: bool = self.declare( - 'skip_scaling_check', ConfigValue(domain=bool, default=False) + "skip_scaling_check", ConfigValue(domain=bool, default=False) ) self.total_violation: bool = self.declare( "total_violation", ConfigValue(domain=bool, default=False) @@ -130,31 +138,32 @@ class FlexTestConfig(ConfigDict): Attributes ---------- feasibility_tol: float - Tolerance for considering constraints to be satisfied. In particular, if the - maximum constraint violation is less than or equal to :py:attr:`feasibility_tol`, then + Tolerance for considering constraints to be satisfied. In particular, if the + maximum constraint violation is less than or equal to :py:attr:`feasibility_tol`, then the flexibility test passes. (default: 1e-6) terminate_early: bool - If True, the specified algorithm should terminate as soon as a point - (:math:`\theta`) is found that confirms the flexibility test fails. If - False, the specified algorithm will continue until the :math:`\theta` + If True, the specified algorithm should terminate as soon as a point + (:math:`\theta`) is found that confirms the flexibility test fails. If + False, the specified algorithm will continue until the :math:`\theta` that maximizes the constraint violation is found. (default: False) method: FlexTestMethod - The method that should be used to solve the flexibility test. (default: :py:attr:`active_constraint`) + The method that should be used to solve the flexibility test. (default: :py:attr:`active_constraint`) minlp_solver: Union[Solver, OptSolver] A Pyomo solver interface appropriate for solving MINLPs sampling_config: SamplingConfig - A config object for specifying how sampling should be performed when either - generating data to create a decision rule or using sampling to solve the + A config object for specifying how sampling should be performed when either + generating data to create a decision rule or using sampling to solve the flexibility test. decision_rule_config: DRConfig - Only used if method is one of the decision rules. Should be either a LinearDRConfig + Only used if method is one of the decision rules. Should be either a LinearDRConfig or a ReluDRConfig. active_constraint_config: ActiveConstraintConfig Only used if :py:attr:`method` is :py:attr:`active_constraint` total_violation: bool - If False, the maximum constraint violation is considered. If True, the sum + If False, the maximum constraint violation is considered. If True, the sum of the violations of all constraints is considered. Should normally be False. (default: False) """ + def __init__(self): super().__init__( description=None, @@ -175,9 +184,7 @@ def __init__(self): domain=InEnum(FlexTestMethod), default=FlexTestMethod.active_constraint ), ) - self.minlp_solver = self.declare( - "minlp_solver", ConfigValue() - ) + self.minlp_solver = self.declare("minlp_solver", ConfigValue()) self.sampling_config: SamplingConfig = self.declare( "sampling_config", SamplingConfig() ) @@ -197,6 +204,7 @@ class FlexTestTermination(enum.Enum): proven_feasible = enum.auto() uncertain = enum.auto() + FlexTestTermination.found_infeasible_point.__doc__ = r"The meaning of this member depends on the method used to solve the flexibility/feasibility test, but it generally means that the flexibility test failed. If the solution method is not conservative (:py:attr:`FlexTestMethod.vertex_enumeration`, :py:attr:`FlexTestMethod.sampling`), then :py:attr:`FlexTestTermination.found_infeasible_point` indicates that a value of :math:`\theta` was found where at least one performance constraint (:math:`g_{j}(x, z, \theta) \leq 0`) is violated. Otherwise, :py:attr:`FlexTestTermination.found_infeasible_point` indicates that a point was found where the performance constraints might be violated." FlexTestTermination.proven_feasible.__doc__ = r"The meaning of this member depends on the method used to solve the flexibility/feasibility test, but it generally means that the flexibility test passed. If the solution method is conservative (:py:attr:`FlexTestMethod.active_constraint`, :py:attr:`FlexTestMethod.linear_decision_rule`, :py:attr:`FlexTestMethod.relu_decision_rule`), then :py:attr:`FlexTestTermination.proven_feasible` indicates that, for any :math:`\theta \in [\underline{\theta}, \overline{\theta}]`, there exists a :math:`z` such that all of the performance constraints (:math:`g_{j}(x, z, \theta) \leq 0`) are satisfied. Otherwise, :py:attr:`FlexTestTermination.proven_feasible` just indicates that no :math:`\theta \in [\underline{\theta}, \overline{\theta}]` was found that violates the performance constraints for all :math:`z`." FlexTestTermination.uncertain.__doc__ = r"Cannot definitively say whether the flexibility test passes or fails. This usually indicates an error was encountered." @@ -214,6 +222,7 @@ class FlexTestResults(object): unc_param_values_at_max_violation: Optional[MutableMapping[Union[_GeneralVarData, _ParamData], float]] The values of the uncertain parameters that generated the maximum constraint violation """ + def __init__(self): self.termination = FlexTestTermination.uncertain self.max_constraint_violation: Optional[float] = None @@ -250,9 +259,9 @@ def _get_dof(m: _BlockData): dr_construction_map = dict() -dr_construction_map[ - FlexTestMethod.linear_decision_rule -] = construct_linear_decision_rule +dr_construction_map[FlexTestMethod.linear_decision_rule] = ( + construct_linear_decision_rule +) def build_flextest_with_dr( @@ -267,8 +276,9 @@ def build_flextest_with_dr( config.sampling_config.total_violation = config.total_violation # this has to be here in case tensorflow or omlt are not installed - dr_construction_map[ - FlexTestMethod.relu_decision_rule] = relu_dr.construct_relu_decision_rule + dr_construction_map[FlexTestMethod.relu_decision_rule] = ( + relu_dr.construct_relu_decision_rule + ) # enforce_equalities must be true for this method, or the resulting # problem will be unbounded; the key is degrees of freedom @@ -342,7 +352,7 @@ def build_flextest_with_dr( unique_constraint_violations=True, valid_var_bounds=valid_var_bounds, total_violation=config.total_violation, - total_violation_disjunctions=total_violation_disjunctions + total_violation_disjunctions=total_violation_disjunctions, ) _apply_var_bounds(valid_var_bounds) m.decision_rule = dr @@ -434,8 +444,12 @@ def build_active_constraint_flextest( key1 = _ConIndex(c, "ub") key2 = _ConIndex(m.ineq_violation_cons[key1], "ub") y2 = m.active_indicator[key2] - m.equality_cuts.add(m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub) - m.equality_cuts.add(m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb) + m.equality_cuts.add( + m.max_constraint_violation <= (1 - y1 * y2) * max_viol_ub + ) + m.equality_cuts.add( + m.max_constraint_violation >= (1 - y1 * y2) * max_viol_lb + ) if config.use_haar_conditions and not config.total_violation: m.n_active_ineqs = pe.Constraint(expr=sum(m.active_indicator.values()) == n_dof) @@ -465,7 +479,7 @@ def _solve_flextest_active_constraint( param_nominal_values=param_nominal_values, param_bounds=param_bounds, valid_var_bounds=valid_var_bounds, - config=config.active_constraint_config + config=config.active_constraint_config, ) opt = config.minlp_solver res = opt.solve(m) @@ -612,9 +626,9 @@ def solve_flextest( m: _BlockData The pyomo model to be used for the feasibility/flexibility test. uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]] - A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). + A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). These can be pyomo variables (Var) or parameters (param). However, if parameters are used, - they must be mutable. + they must be mutable. param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float] A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their nominal values (:math:`\theta^{N}`). @@ -625,14 +639,14 @@ def solve_flextest( A sequence (e.g., list) defining the set of control variables (:math:`z`). valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]] A mapping (e.g., ComponentMap) defining bounds for all variables (:math:`x` and :math:`z`) that - should be valid for any :math:`\theta` between :math:`\underline{\theta}` and + should be valid for any :math:`\theta` between :math:`\underline{\theta}` and :math:`\overline{\theta}`. These are only used to make the resulting flexibility test problem - more computationally tractable. All variable bounds in the model `m` are treated as performance - constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` - are applied to the single-level problem generated from the active constraint method or one of + more computationally tractable. All variable bounds in the model `m` are treated as performance + constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` + are applied to the single-level problem generated from the active constraint method or one of the decision rules. This argument is not necessary for vertex enumeration or sampling. in_place: bool - If True, m is modified in place to generate the model for solving the flexibility test. If False, + If True, m is modified in place to generate the model for solving the flexibility test. If False, the model is cloned first. config: Optional[FlexTestConfig] An object defining options for how the flexibility test should be solved. diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index e414cc1e94..239366f50f 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -3,8 +3,17 @@ from pyomo.core.base.var import _GeneralVarData, ScalarVar from pyomo.common.dependencies import attempt_import from pyomo.core.expr.numeric_expr import ExpressionBase -coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') -from .var_utils import get_all_unfixed_variables, BoundsManager, _apply_var_bounds, get_used_unfixed_variables, _remove_var_bounds + +coramin, coramin_available = attempt_import( + "coramin", "coramin is required for flexibility analysis" +) +from .var_utils import ( + get_all_unfixed_variables, + BoundsManager, + _apply_var_bounds, + get_used_unfixed_variables, + _remove_var_bounds, +) from .indices import _ConIndex, _VarIndex from typing import MutableMapping, Tuple, Optional, Mapping, Union from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr @@ -59,23 +68,29 @@ def _get_g_bounds( return g_bounds -def _add_total_violation_disjunctions(m: _BlockData, g: ExpressionBase, key, - g_bounds: MutableMapping): +def _add_total_violation_disjunctions( + m: _BlockData, g: ExpressionBase, key, g_bounds: MutableMapping +): m.zero_violation_cons[key] = ( None, - (m.constraint_violation[key] - - (1 - m.zero_violation[key]) * m.violation_disjunction_BigM[key]), + ( + m.constraint_violation[key] + - (1 - m.zero_violation[key]) * m.violation_disjunction_BigM[key] + ), 0, ) m.nonzero_violation_cons[key] = ( None, - (m.constraint_violation[key] - g - - (1 - m.nonzero_violation[key]) * m.violation_disjunction_BigM[key]), + ( + m.constraint_violation[key] + - g + - (1 - m.nonzero_violation[key]) * m.violation_disjunction_BigM[key] + ), 0, ) m.violation_disjunction_cons[key] = ( m.zero_violation[key] + m.nonzero_violation[key], - 1 + 1, ) lb, ub = g_bounds[key] m.violation_disjunction_BigM[key].value = max(abs(lb), abs(ub)) @@ -253,15 +268,27 @@ def _build_inner_problem( if c.lower is not None: key = _ConIndex(c, "lb") g = c.lower - c.body - _process_constraint(m, g, key, unique_constraint_violations, - total_violation, total_violation_disjunctions, - g_bounds) + _process_constraint( + m, + g, + key, + unique_constraint_violations, + total_violation, + total_violation_disjunctions, + g_bounds, + ) if c.upper is not None: key = _ConIndex(c, "ub") g = c.body - c.upper - _process_constraint(m, g, key, unique_constraint_violations, - total_violation, total_violation_disjunctions, - g_bounds) + _process_constraint( + m, + g, + key, + unique_constraint_violations, + total_violation, + total_violation_disjunctions, + g_bounds, + ) for v in original_vars: if v.is_integer(): @@ -269,15 +296,27 @@ def _build_inner_problem( if v.lb is not None: key = _VarIndex(v, "lb") g = v.lb - v - _process_constraint(m, g, key, unique_constraint_violations, - total_violation, total_violation_disjunctions, - g_bounds) + _process_constraint( + m, + g, + key, + unique_constraint_violations, + total_violation, + total_violation_disjunctions, + g_bounds, + ) if v.ub is not None: key = _VarIndex(v, "ub") g = v - v.ub - _process_constraint(m, g, key, unique_constraint_violations, - total_violation, total_violation_disjunctions, - g_bounds) + _process_constraint( + m, + g, + key, + unique_constraint_violations, + total_violation, + total_violation_disjunctions, + g_bounds, + ) if total_violation: m.total_constraint_violation_obj = pe.Objective( @@ -294,7 +333,9 @@ def _build_inner_problem( key.var.setub(None) key.var.domain = pe.Reals - if (total_violation or unique_constraint_violations) and valid_var_bounds is not None: + if ( + total_violation or unique_constraint_violations + ) and valid_var_bounds is not None: for key in m.ineq_violation_set: lb, ub = g_bounds[key] v = m.constraint_violation[key] diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index 78f7fcd05a..7c685a0110 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -1,7 +1,10 @@ import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') + +coramin, coramin_available = attempt_import( + "coramin", "coramin is required for flexibility analysis" +) from pyomo.core.base.block import _BlockData from pyomo.contrib.fbbt.fbbt import fbbt from .var_utils import ( diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 6634bcb455..6dd2946f41 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -17,36 +17,44 @@ from pyomo.common.errors import ApplicationError from pyomo.contrib import appsi from .check_optimal import assert_optimal_termination + try: from tqdm import tqdm except ImportError: + def tqdm(items, ncols, desc, disable): return items class SamplingStrategy(enum.Enum): - grid = 'grid' - lhs = 'lhs' + grid = "grid" + lhs = "lhs" + SamplingStrategy.grid.__doc__ = r"Use evenly spaced samples" SamplingStrategy.lhs.__doc__ = r"Use latin hypercube sampling" class SamplingInitStrategy(enum.Enum): - none = 'none' - square = 'square' - min_control_deviation = 'min_control_deviation' - all = 'all' - -SamplingInitStrategy.none.__doc__ = r"Use the solution from the previous sample to initialize the inner problem" -SamplingInitStrategy.square.__doc__ = r"Fix the controls and solve a square problem to initialize the inner problem" + none = "none" + square = "square" + min_control_deviation = "min_control_deviation" + all = "all" + + +SamplingInitStrategy.none.__doc__ = ( + r"Use the solution from the previous sample to initialize the inner problem" +) +SamplingInitStrategy.square.__doc__ = ( + r"Fix the controls and solve a square problem to initialize the inner problem" +) SamplingInitStrategy.min_control_deviation.__doc__ = r"Fix the maximum constraint violation to 0 and minimized the square of the differences between the controls and their current values" SamplingInitStrategy.all.__doc__ = r"Try both square and min_control_deviation" class _GridSamplingState(enum.Enum): - increment = 'increment' - decrement = 'decrement' + increment = "increment" + decrement = "decrement" class _ParamIterator(object): @@ -58,7 +66,9 @@ def __init__( ): self.state = _GridSamplingState.increment self.ndx = 0 - self.pts = list(set([float(i) for i in np.linspace(param.lb, param.ub, num_points)])) + self.pts = list( + set([float(i) for i in np.linspace(param.lb, param.ub, num_points)]) + ) self.pts.sort() self.next_param = next_param @@ -101,9 +111,7 @@ def step(self) -> bool: class _GridSamplingIterator(object): - def __init__( - self, uncertain_params: Sequence[_GeneralVarData], num_points: int - ): + def __init__(self, uncertain_params: Sequence[_GeneralVarData], num_points: int): self.params = list(uncertain_params) self.param_iterators: List[Optional[_ParamIterator]] = [None] * len(self.params) self.param_iterators[-1] = _ParamIterator( @@ -111,8 +119,9 @@ def __init__( ) for ndx in reversed(range(len(self.params) - 1)): self.param_iterators[ndx] = _ParamIterator( - param=self.params[ndx], num_points=num_points, - next_param=self.param_iterators[ndx + 1] + param=self.params[ndx], + num_points=num_points, + next_param=self.param_iterators[ndx + 1], ) self.done = False @@ -194,13 +203,14 @@ class SamplingConfig(ConfigDict): enable_progress_bar: bool If False, no progress bar will be shown (default: True) initialization_strategy: SamplingInitStrategy - The initialization strategy to use for the inner problems of the + The initialization strategy to use for the inner problems of the flexibility test at each sample of the uncertain parameter values. (default: SamplingInitStrategy.none) total_violation: bool - If True, the objective of the flexibility test will be the sum of + If True, the objective of the flexibility test will be the sum of the constraint violations instead of the maximum violation. (default: False) """ + def __init__(self): super().__init__( description=None, @@ -229,7 +239,7 @@ def __init__(self): "initialization_strategy", ConfigValue( domain=InEnum(SamplingInitStrategy), default=SamplingInitStrategy.none - ) + ), ) self.total_violation: bool = self.declare( "total_violation", ConfigValue(domain=bool, default=False) @@ -249,8 +259,8 @@ def _init_with_square_problem(m: _BlockData, controls, solver): for v in controls: if v.value is None: raise RuntimeError( - 'Cannot initialize sampling problem with square problem because the ' - 'control values are not initialized.' + "Cannot initialize sampling problem with square problem because the " + "control values are not initialized." ) v.fix() if m.max_constraint_violation.value is None: @@ -292,7 +302,7 @@ def _solve_with_max_viol_fixed(m: _BlockData, controls, solver): if v.value is None: obj_expr += v**2 else: - obj_expr += (v - v.value)**2 + obj_expr += (v - v.value) ** 2 m.control_setpoints_obj = pe.Objective(expr=obj_expr) try: @@ -356,12 +366,20 @@ def _perform_sampling( for p, p_vals in sample_points.items(): p.fix(p_vals[sample_ndx]) - if not config.total_violation and config.initialization_strategy in {SamplingInitStrategy.square, SamplingInitStrategy.all}: + if not config.total_violation and config.initialization_strategy in { + SamplingInitStrategy.square, + SamplingInitStrategy.all, + }: max_viol_ub = _init_with_square_problem(m, controls, config.solver) if max_viol_ub is not None: m.max_constraint_violation.setub(max_viol_ub) - if not config.total_violation and config.initialization_strategy in {SamplingInitStrategy.min_control_deviation, SamplingInitStrategy.all}: - feasible, control_vals = _solve_with_max_viol_fixed(m, controls, config.solver) + if not config.total_violation and config.initialization_strategy in { + SamplingInitStrategy.min_control_deviation, + SamplingInitStrategy.all, + }: + feasible, control_vals = _solve_with_max_viol_fixed( + m, controls, config.solver + ) else: feasible = False control_vals = None diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 9b23ce481d..1b71799ed2 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -16,7 +16,7 @@ def create_poly_model(): offset = 1.5 - m.obj = pe.Objective(expr=m.z ** 2) + m.obj = pe.Objective(expr=m.z**2) m.c1 = pe.Constraint( expr=0.01 * (m.z - offset) ** 4 - 0.05 * (m.z - offset) ** 3 @@ -105,7 +105,9 @@ def test_poly(self): ) nlp_solver = appsi.solvers.Ipopt() mip_solver = appsi.solvers.Gurobi() - opt = coramin.algorithms.multitree.multitree.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) + opt = coramin.algorithms.multitree.multitree.MultiTree( + mip_solver=mip_solver, nlp_solver=nlp_solver + ) opt.config.stream_solver = False opt.config.obbt_at_new_incumbents = True opt.config.relax_integers_for_obbt = False diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index dce89b1b05..43741c9483 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -1,6 +1,10 @@ from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe -from idaes.apps.flexibility_analysis.sampling import perform_sampling, SamplingConfig, SamplingStrategy +from idaes.apps.flexibility_analysis.sampling import ( + perform_sampling, + SamplingConfig, + SamplingStrategy, +) import unittest import numpy as np import pytest @@ -15,7 +19,7 @@ def create_poly_model(): offset = 1.5 - m.obj = pe.Objective(expr=m.z ** 2) + m.obj = pe.Objective(expr=m.z**2) m.c1 = pe.Constraint( expr=0.01 * (m.z - offset) ** 4 - 0.05 * (m.z - offset) ** 3 @@ -109,7 +113,7 @@ def test_poly(self): param_bounds=param_bounds, controls=[m.z], in_place=True, - config=config + config=config, ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) @@ -131,7 +135,7 @@ def test_hx_network(self): param_bounds=param_bounds, controls=[m.qc], in_place=True, - config=config + config=config, ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) @@ -151,7 +155,7 @@ def test_hx_network3(self): param_bounds=param_bounds, controls=[m.qc], in_place=True, - config=config + config=config, ) sample_points, max_violation_values, control_values = tmp max_viol_ndx = np.argmax(max_violation_values) diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index dfc8a0eca5..02fc667322 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -3,7 +3,10 @@ from pyomo.common.collections import ComponentSet from pyomo.core.expr.visitor import identify_variables from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import('coramin', 'coramin is required for flexibility analysis') + +coramin, coramin_available = attempt_import( + "coramin", "coramin is required for flexibility analysis" +) from typing import Mapping, Sequence from pyomo.core.base.var import _GeneralVarData From 63d10453fdc0b3ca1f4cc0fef18437fa2a34e27e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 10:59:47 -0600 Subject: [PATCH 26/60] flexibility analysis: cleanup imports --- .../flexibility_analysis/_check_dependencies.py | 5 ++--- .../examples/idaes_hx_network.py | 13 +++---------- idaes/apps/flexibility_analysis/flextest.py | 6 ++---- idaes/apps/flexibility_analysis/inner_problem.py | 8 ++------ idaes/apps/flexibility_analysis/kkt.py | 8 +++----- idaes/apps/flexibility_analysis/sampling.py | 6 +++--- .../flexibility_analysis/tests/test_flextest.py | 14 ++------------ .../flexibility_analysis/tests/test_sampling.py | 7 +------ idaes/apps/flexibility_analysis/var_utils.py | 6 ++---- 9 files changed, 20 insertions(+), 53 deletions(-) diff --git a/idaes/apps/flexibility_analysis/_check_dependencies.py b/idaes/apps/flexibility_analysis/_check_dependencies.py index de7355310b..4baee6045a 100644 --- a/idaes/apps/flexibility_analysis/_check_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_dependencies.py @@ -1,7 +1,6 @@ from pyomo.common.dependencies import attempt_import import unittest -coramin, coramin_available = attempt_import("coramin") np, nump_available = attempt_import("numpy") -if not coramin_available or not nump_available: - raise unittest.SkipTest("flexibility_analysis tests require coramin and numpy") +if not nump_available: + raise unittest.SkipTest("flexibility_analysis tests require numpy") diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index c9297a8e21..f4add55fea 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -4,11 +4,7 @@ ) from idaes.core import FlowsheetBlock from idaes.models.unit_models.heater import Heater -from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import( - "coramin", "coramin is required for flexibility analysis" -) import logging from pyomo.contrib.fbbt.fbbt import fbbt from pyomo.network import Arc @@ -16,13 +12,10 @@ from pyomo.util.infeasible import log_infeasible_constraints, log_infeasible_bounds from idaes.core.base.control_volume_base import ControlVolumeBlockData from pyomo.core.base.block import _BlockData -import numpy as np import idaes.apps.flexibility_analysis as flexibility from idaes.apps.flexibility_analysis.var_utils import BoundsManager -from pyomo.core.expr.numvalue import polynomial_degree -from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression -from pyomo.repn.standard_repn import generate_standard_repn -from coramin.utils.pyomo_utils import simplify_expr +from idaes.apps.flexibility_analysis.simplify import simplify_expr +from pyomo.contrib.solver.util import get_objective logging.basicConfig(level=logging.INFO) @@ -307,7 +300,7 @@ def scale_model(m): m.scaling_factor[m.fs.s1_expanded.pressure_equality[0]] = 1e-4 m.scaling_factor[m.fs.s2_expanded.pressure_equality[0]] = 1e-4 m.scaling_factor[m.fs.s3_expanded.pressure_equality[0]] = 1e-4 - m.scaling_factor[coramin.utils.get_objective(m)] = 1e-4 + m.scaling_factor[get_objective(m)] = 1e-4 pe.TransformationFactory("core.scale_model").apply_to(m, rename=False) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 03314e194a..a80069b988 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -3,9 +3,6 @@ from pyomo.core.base.block import _BlockData from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import( - "coramin", "coramin is required for flexibility analysis" -) import pyomo.environ as pe from .var_utils import ( get_used_unfixed_variables, @@ -21,6 +18,7 @@ from typing import Sequence, Union, Mapping, MutableMapping, Optional, Tuple from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData +from pyomo.contrib.solver.util import get_objective from .decision_rules.linear_dr import construct_linear_decision_rule from .decision_rules.dr_config import DRConfig from pyomo.common.dependencies import attempt_import @@ -357,7 +355,7 @@ def build_flextest_with_dr( _apply_var_bounds(valid_var_bounds) m.decision_rule = dr - obj = coramin.utils.get_objective(m) + obj = get_objective(m) obj.deactivate() if config.total_violation: diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index 239366f50f..0afdd27d0e 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -1,14 +1,10 @@ import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData, ScalarVar -from pyomo.common.dependencies import attempt_import from pyomo.core.expr.numeric_expr import ExpressionBase -coramin, coramin_available = attempt_import( - "coramin", "coramin is required for flexibility analysis" -) +from pyomo.contrib.solver.util import get_objective from .var_utils import ( - get_all_unfixed_variables, BoundsManager, _apply_var_bounds, get_used_unfixed_variables, @@ -227,7 +223,7 @@ def _build_inner_problem( if total_violation_disjunctions: assert total_violation - obj = coramin.utils.get_objective(m) + obj = get_objective(m) if obj is not None: obj.deactivate() diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index 7c685a0110..742b9f9902 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -2,9 +2,6 @@ from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.common.dependencies import attempt_import -coramin, coramin_available = attempt_import( - "coramin", "coramin is required for flexibility analysis" -) from pyomo.core.base.block import _BlockData from pyomo.contrib.fbbt.fbbt import fbbt from .var_utils import ( @@ -14,6 +11,7 @@ from typing import Sequence, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression +from pyomo.contrib.solver.util import get_objective from .indices import _VarIndex, _ConIndex @@ -33,7 +31,7 @@ def _add_grad_lag_constraints(m: _BlockData) -> _BlockData: m.duals_ineq_set = pe.Set() m.duals_ineq = pe.Var(m.duals_ineq_set, bounds=(0, None)) - obj = coramin.utils.get_objective(m) + obj = get_objective(m) assert obj.sense == pe.minimize if obj is None: lagrangian = 0 @@ -139,7 +137,7 @@ def add_kkt_with_milp_complementarity_conditions( v.fix() _add_grad_lag_constraints(m) - obj = coramin.utils.get_objective(m) + obj = get_objective(m) obj.deactivate() _apply_var_bounds(valid_var_bounds) diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 6dd2946f41..ace896ab06 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -13,7 +13,7 @@ from idaes.core.surrogate.pysmo.sampling import LatinHypercubeSampling from .indices import _VarIndex from pyomo.common.config import ConfigDict, ConfigValue, InEnum -import coramin +from pyomo.contrib.solver.util import get_objective from pyomo.common.errors import ApplicationError from pyomo.contrib import appsi from .check_optimal import assert_optimal_termination @@ -292,7 +292,7 @@ def _init_with_square_problem(m: _BlockData, controls, solver): def _solve_with_max_viol_fixed(m: _BlockData, controls, solver): - orig_obj = coramin.utils.get_objective(m) + orig_obj = get_objective(m) orig_obj.deactivate() orig_max_viol_value = m.max_constraint_violation.value m.max_constraint_violation.fix(0) @@ -347,7 +347,7 @@ def _perform_sampling( ) obj_values = list() - obj = coramin.utils.get_objective(m) + obj = get_objective(m) control_values = pe.ComponentMap() for v in controls: diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 1b71799ed2..ca7f278e20 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -5,8 +5,6 @@ from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval import pytest -import coramin -from pyomo.contrib import appsi def create_poly_model(): @@ -103,17 +101,9 @@ def test_poly(self): param_bounds=param_bounds, valid_var_bounds=var_bounds, ) - nlp_solver = appsi.solvers.Ipopt() - mip_solver = appsi.solvers.Gurobi() - opt = coramin.algorithms.multitree.multitree.MultiTree( - mip_solver=mip_solver, nlp_solver=nlp_solver - ) - opt.config.stream_solver = False - opt.config.obbt_at_new_incumbents = True - opt.config.relax_integers_for_obbt = False - opt.config.mip_gap = 1e-4 + opt = pe.SolverFactory('scip') res = opt.solve(m) - assert res.termination_condition == appsi.base.TerminationCondition.optimal + pe.assert_optimal_termination(res) self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) self.assertAlmostEqual(m.z.value, -2.6513, 4) ndx = _VarIndex(m.theta, None) diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 43741c9483..52f4bdcc91 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -8,7 +8,6 @@ import unittest import numpy as np import pytest -import coramin from pyomo.contrib import appsi @@ -97,11 +96,7 @@ def create_hx_network_model(): class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() - mip_solver = appsi.solvers.Gurobi() - nlp_solver = appsi.solvers.Ipopt() - opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) - opt.config.stream_solver = False - opt.config.convexity_effort = coramin.Effort.none + opt = pe.SolverFactory('scip') config = SamplingConfig() config.solver = opt config.num_points = 5 diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 02fc667322..2b3f5c1e6b 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -3,10 +3,8 @@ from pyomo.common.collections import ComponentSet from pyomo.core.expr.visitor import identify_variables from pyomo.common.dependencies import attempt_import +from pyomo.contrib.solver.util import get_objective -coramin, coramin_available = attempt_import( - "coramin", "coramin is required for flexibility analysis" -) from typing import Mapping, Sequence from pyomo.core.base.var import _GeneralVarData @@ -23,7 +21,7 @@ def get_used_unfixed_variables(m: _BlockData): res = ComponentSet() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): res.update(v for v in identify_variables(c.body, include_fixed=False)) - obj = coramin.utils.get_objective(m) + obj = get_objective(m) if obj is not None: res.update(identify_variables(obj.expr, include_fixed=False)) return res From aab76b5fd9b8a6a56096681942bcfe519f792183 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 11:02:04 -0600 Subject: [PATCH 27/60] flexibility analysis: cleanup imports --- idaes/apps/flexibility_analysis/simplify.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 idaes/apps/flexibility_analysis/simplify.py diff --git a/idaes/apps/flexibility_analysis/simplify.py b/idaes/apps/flexibility_analysis/simplify.py new file mode 100644 index 0000000000..671cb4cf2f --- /dev/null +++ b/idaes/apps/flexibility_analysis/simplify.py @@ -0,0 +1,12 @@ +from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression +from pyomo.core.expr import is_fixed, value + + +def simplify_expr(expr: NumericExpression): + om, se = sympyify_expression(expr) + se = se.simplify() + new_expr = sympy2pyomo_expression(se, om) + if is_fixed(new_expr): + new_expr = value(new_expr) + return new_expr From 4e3732553976bcc25196aac8392ec78e2e6935cf Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 11:30:13 -0600 Subject: [PATCH 28/60] flexibility analysis: cleanup imports --- idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py | 3 +-- .../apps/flexibility_analysis/examples/tests/test_examples.py | 1 - idaes/apps/flexibility_analysis/flextest.py | 3 --- idaes/apps/flexibility_analysis/kkt.py | 1 - idaes/apps/flexibility_analysis/sampling.py | 3 --- idaes/apps/flexibility_analysis/var_utils.py | 1 - 6 files changed, 1 insertion(+), 11 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index 893a8aa0c6..4a1e3f3207 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -1,9 +1,8 @@ import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData import idaes.apps.flexibility_analysis as flexibility -from typing import Tuple, MutableMapping, Union +from typing import Tuple, MutableMapping def create_model() -> Tuple[ diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index cc5149f9c0..67fac852a5 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -5,7 +5,6 @@ import idaes.apps.flexibility_analysis as flex from idaes.apps.flexibility_analysis.examples import ( linear_hx_network, - nonlin_hx_network, idaes_hx_network, ) import unittest diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index a80069b988..9334b4bf0b 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -20,7 +20,6 @@ from pyomo.core.base.param import _ParamData from pyomo.contrib.solver.util import get_objective from .decision_rules.linear_dr import construct_linear_decision_rule -from .decision_rules.dr_config import DRConfig from pyomo.common.dependencies import attempt_import from .sampling import ( SamplingStrategy, @@ -37,8 +36,6 @@ MarkImmutable, NonNegativeFloat, ) -from pyomo.contrib.appsi.base import Solver -from pyomo.opt.base import OptSolver relu_dr, relu_dr_available = attempt_import( "idaes.apps.flexibility_analysis.decision_rules.relu_dr", diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index 742b9f9902..86c4222099 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -1,6 +1,5 @@ import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -from pyomo.common.dependencies import attempt_import from pyomo.core.base.block import _BlockData from pyomo.contrib.fbbt.fbbt import fbbt diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index ace896ab06..e1864256fd 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -2,11 +2,9 @@ from pyomo.core.base.block import _BlockData import pyomo.environ as pe import numpy as np -import itertools from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple, List from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.contrib.appsi.base import PersistentSolver from .uncertain_params import _replace_uncertain_params from .inner_problem import _build_inner_problem import enum @@ -15,7 +13,6 @@ from pyomo.common.config import ConfigDict, ConfigValue, InEnum from pyomo.contrib.solver.util import get_objective from pyomo.common.errors import ApplicationError -from pyomo.contrib import appsi from .check_optimal import assert_optimal_termination try: diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 2b3f5c1e6b..38fef0f2d3 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -2,7 +2,6 @@ from pyomo.core.base.block import _BlockData from pyomo.common.collections import ComponentSet from pyomo.core.expr.visitor import identify_variables -from pyomo.common.dependencies import attempt_import from pyomo.contrib.solver.util import get_objective from typing import Mapping, Sequence From f8d985c93ae0f4f060a3a7903b04a8bb18786962 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 11:40:02 -0600 Subject: [PATCH 29/60] run black --- idaes/apps/flexibility_analysis/tests/test_flextest.py | 2 +- idaes/apps/flexibility_analysis/tests/test_sampling.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index ca7f278e20..35cce20ab8 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -101,7 +101,7 @@ def test_poly(self): param_bounds=param_bounds, valid_var_bounds=var_bounds, ) - opt = pe.SolverFactory('scip') + opt = pe.SolverFactory("scip") res = opt.solve(m) pe.assert_optimal_termination(res) self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 52f4bdcc91..09620fbcc4 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -96,7 +96,7 @@ def create_hx_network_model(): class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() - opt = pe.SolverFactory('scip') + opt = pe.SolverFactory("scip") config = SamplingConfig() config.solver = opt config.num_points = 5 From 7e43bbdc953d2262407480e7162b70d798f7469f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 16:31:48 -0600 Subject: [PATCH 30/60] fix tests --- .../examples/tests/test_examples.py | 2 +- idaes/apps/flexibility_analysis/flextest.py | 42 +++++++++++++------ idaes/apps/flexibility_analysis/sampling.py | 20 ++++++--- .../tests/test_flextest.py | 30 +++++++------ 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index 67fac852a5..facc3b1b21 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -22,7 +22,7 @@ def test_linear_hx_network(self): self.assertEqual( res.termination, flex.FlexTestTermination.found_infeasible_point ) - self.assertAlmostEqual(res.max_constraint_violation, 4, 5) + self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) res = linear_hx_network.main( flex_index=False, diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 9334b4bf0b..1ee991b2b6 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -101,14 +101,22 @@ class ActiveConstraintConfig(ConfigDict): the constraint violations instead of the maximum violation. (default: False) """ - def __init__(self): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): super().__init__( - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, ) + self.use_haar_conditions: bool = self.declare( "use_haar_conditions", ConfigValue(domain=bool, default=True) ) @@ -159,14 +167,22 @@ class FlexTestConfig(ConfigDict): of the violations of all constraints is considered. Should normally be False. (default: False) """ - def __init__(self): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): super().__init__( - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, ) + self.feasibility_tol: float = self.declare( "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-6) ) @@ -567,7 +583,7 @@ def _solve_flextest_vertex_enumeration( param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], controls: Sequence[_GeneralVarData], valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], - config: Optional[FlexTestConfig] = None, + config: FlexTestConfig, ) -> FlexTestResults: config: FlexTestConfig = config() config.sampling_config.num_points = 2 diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index e1864256fd..3a1cdfbcb9 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -208,14 +208,22 @@ class SamplingConfig(ConfigDict): the constraint violations instead of the maximum violation. (default: False) """ - def __init__(self): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): super().__init__( - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, ) + self.strategy: SamplingStrategy = self.declare( "strategy", ConfigValue(domain=InEnum(SamplingStrategy), default=SamplingStrategy.lhs), diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 35cce20ab8..832ac9fede 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -1,6 +1,6 @@ from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe -from idaes.apps.flexibility_analysis.flextest import build_active_constraint_flextest +from idaes.apps.flexibility_analysis.flextest import build_active_constraint_flextest, ActiveConstraintConfig import unittest from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval @@ -115,14 +115,20 @@ def test_hx_network(self): for v in m.variable_temps.values(): var_bounds[v] = (100, 1000) var_bounds[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) - build_active_constraint_flextest( - m, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - valid_var_bounds=var_bounds, - ) - opt = pe.SolverFactory("gurobi_direct") - res = opt.solve(m, tee=False) - pe.assert_optimal_termination(res) - self.assertAlmostEqual(m.max_constraint_violation.value, 4, 4) + config = ActiveConstraintConfig() + expected = [4, 8.8] + enforce_eq = [False, True] + for exp, ee in zip(expected, enforce_eq): + config.enforce_equalities = ee + build_active_constraint_flextest( + m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + valid_var_bounds=var_bounds, + config=config, + ) + opt = pe.SolverFactory("gurobi_direct") + res = opt.solve(m, tee=False) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.max_constraint_violation.value, exp, 4) From 1007c01b8264e7a31ecc756db441ac1c8e27cfd1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 16:33:22 -0600 Subject: [PATCH 31/60] run black --- idaes/apps/flexibility_analysis/tests/test_flextest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 832ac9fede..dcd1e45c30 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -1,6 +1,9 @@ from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe -from idaes.apps.flexibility_analysis.flextest import build_active_constraint_flextest, ActiveConstraintConfig +from idaes.apps.flexibility_analysis.flextest import ( + build_active_constraint_flextest, + ActiveConstraintConfig, +) import unittest from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval From e054982336db9911dc3a72b9e05adc05e598db24 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 16:41:56 -0600 Subject: [PATCH 32/60] fix tests --- .../apps/flexibility_analysis/tests/test_flextest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index dcd1e45c30..6dc2a2def8 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -113,15 +113,15 @@ def test_poly(self): self.assertAlmostEqual(m.unc_param_vars[ndx].value, 65, 5) def test_hx_network(self): - m, nominal_values, param_bounds = create_hx_network_model() - var_bounds = pe.ComponentMap() - for v in m.variable_temps.values(): - var_bounds[v] = (100, 1000) - var_bounds[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) - config = ActiveConstraintConfig() expected = [4, 8.8] enforce_eq = [False, True] for exp, ee in zip(expected, enforce_eq): + m, nominal_values, param_bounds = create_hx_network_model() + var_bounds = pe.ComponentMap() + for v in m.variable_temps.values(): + var_bounds[v] = (100, 1000) + var_bounds[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) + config = ActiveConstraintConfig() config.enforce_equalities = ee build_active_constraint_flextest( m, From d102906d00aebca5336cc53be44dfb98b575f0eb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 17:03:27 -0600 Subject: [PATCH 33/60] update test tags --- .../flexibility_analysis/decision_rules/tests/test_linear_dr.py | 2 +- idaes/apps/flexibility_analysis/examples/tests/test_examples.py | 2 +- idaes/apps/flexibility_analysis/tests/test_flextest.py | 2 +- idaes/apps/flexibility_analysis/tests/test_sampling.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index 8218783319..9d485e3a29 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -18,7 +18,7 @@ def y2_func(x1, x2): class TestLinearDecisionRule(unittest.TestCase): - @pytest.mark.unit + @pytest.mark.component def test_construct_linear_dr(self): x1_samples = [float(i) for i in np.linspace(-5, 5, 100)] x2_samples = [float(i) for i in np.linspace(-5, 5, 100)] diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index facc3b1b21..7674d358a4 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -11,7 +11,7 @@ import pytest -@pytest.mark.unit +@pytest.mark.integration class TestExamples(unittest.TestCase): def test_linear_hx_network(self): res = linear_hx_network.main( diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 6dc2a2def8..8df8dd740b 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -91,7 +91,7 @@ def create_hx_network_model(): return m, nominal_values, param_bounds -@pytest.mark.unit +@pytest.mark.component class TestFlexTest(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 09620fbcc4..5c1b7a22e8 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -92,7 +92,7 @@ def create_hx_network_model(): return m, nominal_values, param_bounds -@pytest.mark.unit +@pytest.mark.component class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() From b794932a10cc1aff83415318ace1127da29e0c76 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 17:05:12 -0600 Subject: [PATCH 34/60] run black --- idaes/apps/flexibility_analysis/tests/test_flextest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 8df8dd740b..cb701ead3c 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -120,7 +120,9 @@ def test_hx_network(self): var_bounds = pe.ComponentMap() for v in m.variable_temps.values(): var_bounds[v] = (100, 1000) - var_bounds[m.qc] = interval.mul(1.5, 1.5, *interval.sub(100, 1000, 350, 350)) + var_bounds[m.qc] = interval.mul( + 1.5, 1.5, *interval.sub(100, 1000, 350, 350) + ) config = ActiveConstraintConfig() config.enforce_equalities = ee build_active_constraint_flextest( From 9d7f84673b49a540017ac03ff42435507eae946b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 18:23:41 -0600 Subject: [PATCH 35/60] update flexibility analysis tests --- idaes/apps/flexibility_analysis/decision_rules/linear_dr.py | 2 +- .../decision_rules/tests/test_linear_dr.py | 2 +- .../apps/flexibility_analysis/examples/idaes_hx_network.py | 2 +- .../apps/flexibility_analysis/examples/linear_hx_network.py | 6 +++--- .../apps/flexibility_analysis/examples/nonlin_hx_network.py | 4 ++-- idaes/apps/flexibility_analysis/tests/test_flextest.py | 2 +- idaes/apps/flexibility_analysis/tests/test_sampling.py | 4 ++-- requirements-dev.txt | 1 + 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index 7b04cabb30..4ed2dcdf0f 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -35,7 +35,7 @@ def __init__( visibility=visibility, ) self.solver = self.declare( - "solver", ConfigValue(default=pe.SolverFactory("appsi_gurobi")) + "solver", ConfigValue(default=pe.SolverFactory("ipopt")) ) diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index 9d485e3a29..f8cff8f4c9 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -44,7 +44,7 @@ def test_construct_linear_dr(self): output_vals[m.y1] = [float(i) for i in y1_samples] output_vals[m.y2] = [float(i) for i in y2_samples] - opt = pe.SolverFactory("appsi_gurobi") + opt = pe.SolverFactory("ipopt") config = LinearDRConfig() config.solver = opt m.dr = construct_linear_decision_rule( diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index f4add55fea..60e798db25 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -341,7 +341,7 @@ def main(method): config.sampling_config.num_points = 3 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() - config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") + config.decision_rule_config.solver = pe.SolverFactory("scip") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 6486eb681d..a1ac85a6c8 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -85,14 +85,14 @@ def main( config.feasibility_tol = 1e-6 config.terminate_early = False # TODO: this does not do anything yet config.method = method - config.minlp_solver = pe.SolverFactory("gurobi_direct") - config.sampling_config.solver = pe.SolverFactory("appsi_gurobi") + config.minlp_solver = pe.SolverFactory("scip") + config.sampling_config.solver = pe.SolverFactory("appsi_highs") config.sampling_config.strategy = "lhs" config.sampling_config.num_points = 600 config.sampling_config.initialization_strategy = "square" if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() - config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") + config.decision_rule_config.solver = pe.SolverFactory("ipopt") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index 4a1e3f3207..925cb8580a 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -58,12 +58,12 @@ def main(method): config.terminate_early = False config.method = method config.minlp_solver = pe.SolverFactory("scip") - config.sampling_config.solver = pe.SolverFactory("gurobi_direct") + config.sampling_config.solver = pe.SolverFactory("scip") config.sampling_config.strategy = flexibility.SamplingStrategy.lhs config.sampling_config.num_points = 100 if method == flexibility.FlexTestMethod.linear_decision_rule: config.decision_rule_config = flexibility.LinearDRConfig() - config.decision_rule_config.solver = pe.SolverFactory("appsi_gurobi") + config.decision_rule_config.solver = pe.SolverFactory("ipopt") elif method == flexibility.FlexTestMethod.relu_decision_rule: config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index cb701ead3c..e350e7c3e4 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -133,7 +133,7 @@ def test_hx_network(self): valid_var_bounds=var_bounds, config=config, ) - opt = pe.SolverFactory("gurobi_direct") + opt = pe.SolverFactory("scip") res = opt.solve(m, tee=False) pe.assert_optimal_termination(res) self.assertAlmostEqual(m.max_constraint_violation.value, exp, 4) diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index 5c1b7a22e8..bd732ffad5 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -118,7 +118,7 @@ def test_poly(self): def test_hx_network(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory("gurobi_direct") + opt = pe.SolverFactory("scip") config = SamplingConfig() config.solver = opt config.num_points = 2 @@ -138,7 +138,7 @@ def test_hx_network(self): def test_hx_network3(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = appsi.solvers.Gurobi() + opt = appsi.solvers.Highs() config = SamplingConfig() config.solver = opt config.num_points = 2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 384dee177e..f2be8e17b7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,6 +29,7 @@ jsonschema jupyter_contrib_nbextensions snowballstemmer==1.2.1 addheader>=0.2.2 +highspy # this will install IDAES in editable mode using the dependencies defined under the `extras_require` tags defined in `setup.py` --editable .[ui,dmf,grid,omlt,coolprop] From 84b6bde4dec416dd1c48f87c676c9afaad20edad Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 21:15:45 -0600 Subject: [PATCH 36/60] flexibility tests --- .../tests/test_flextest.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index e350e7c3e4..20d80aa2a2 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -8,6 +8,22 @@ from idaes.apps.flexibility_analysis.indices import _VarIndex from pyomo.contrib.fbbt import interval import pytest +from idaes.core.util.testing import _enable_scip_solver_for_testing +from contextlib import contextmanager + + +@contextmanager +def scip_solver(): + solver = pe.SolverFactory("scip") + undo_changes = None + + if not solver.available(): + undo_changes = _enable_scip_solver_for_testing() + if not solver.available(): + pytest.skip(reason="SCIP solver not available") + yield solver + if undo_changes is not None: + undo_changes() def create_poly_model(): @@ -104,13 +120,13 @@ def test_poly(self): param_bounds=param_bounds, valid_var_bounds=var_bounds, ) - opt = pe.SolverFactory("scip") - res = opt.solve(m) - pe.assert_optimal_termination(res) - self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) - self.assertAlmostEqual(m.z.value, -2.6513, 4) - ndx = _VarIndex(m.theta, None) - self.assertAlmostEqual(m.unc_param_vars[ndx].value, 65, 5) + with scip_solver() as opt: + res = opt.solve(m) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.max_constraint_violation.value, 48.4649, 4) + self.assertAlmostEqual(m.z.value, -2.6513, 4) + ndx = _VarIndex(m.theta, None) + self.assertAlmostEqual(m.unc_param_vars[ndx].value, 65, 5) def test_hx_network(self): expected = [4, 8.8] @@ -133,7 +149,7 @@ def test_hx_network(self): valid_var_bounds=var_bounds, config=config, ) - opt = pe.SolverFactory("scip") - res = opt.solve(m, tee=False) - pe.assert_optimal_termination(res) - self.assertAlmostEqual(m.max_constraint_violation.value, exp, 4) + with scip_solver() as opt: + res = opt.solve(m, tee=False) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.max_constraint_violation.value, exp, 4) From 00effaa0050f2b0ce61fe5141bb3ce12351e81b8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 21:36:33 -0600 Subject: [PATCH 37/60] add headers --- idaes/apps/flexibility_analysis/__init__.py | 12 ++++++++++++ .../apps/flexibility_analysis/_check_dependencies.py | 12 ++++++++++++ .../flexibility_analysis/_check_relu_dependencies.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/check_optimal.py | 12 ++++++++++++ .../flexibility_analysis/decision_rules/dr_config.py | 12 ++++++++++++ .../flexibility_analysis/decision_rules/dr_enum.py | 12 ++++++++++++ .../flexibility_analysis/decision_rules/linear_dr.py | 12 ++++++++++++ .../flexibility_analysis/decision_rules/relu_dr.py | 12 ++++++++++++ .../decision_rules/relu_dr_config.py | 12 ++++++++++++ .../decision_rules/tests/test_linear_dr.py | 12 ++++++++++++ .../examples/idaes_hx_network.py | 12 ++++++++++++ .../examples/linear_hx_network.py | 12 ++++++++++++ .../examples/nonlin_hx_network.py | 12 ++++++++++++ .../examples/tests/test_examples.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/flex_index.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/flextest.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/indices.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/inner_problem.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/kkt.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/sampling.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/simplify.py | 12 ++++++++++++ .../apps/flexibility_analysis/tests/test_flextest.py | 12 ++++++++++++ .../apps/flexibility_analysis/tests/test_indices.py | 12 ++++++++++++ .../apps/flexibility_analysis/tests/test_sampling.py | 12 ++++++++++++ .../tests/test_uncertain_params.py | 12 ++++++++++++ .../flexibility_analysis/tests/test_var_utils.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/uncertain_params.py | 12 ++++++++++++ idaes/apps/flexibility_analysis/var_utils.py | 12 ++++++++++++ 28 files changed, 336 insertions(+) diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index 2689685351..0d85ba5fbe 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from .flextest import ( solve_flextest, SamplingStrategy, diff --git a/idaes/apps/flexibility_analysis/_check_dependencies.py b/idaes/apps/flexibility_analysis/_check_dependencies.py index 4baee6045a..1e36a72c65 100644 --- a/idaes/apps/flexibility_analysis/_check_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_dependencies.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from pyomo.common.dependencies import attempt_import import unittest diff --git a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py index 9e69936df4..2860b057c0 100644 --- a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from pyomo.common.dependencies import attempt_import import unittest diff --git a/idaes/apps/flexibility_analysis/check_optimal.py b/idaes/apps/flexibility_analysis/check_optimal.py index 2b88a88fa3..ca221083d8 100644 --- a/idaes/apps/flexibility_analysis/check_optimal.py +++ b/idaes/apps/flexibility_analysis/check_optimal.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from pyomo.contrib import appsi diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py index 02bc176b5d..443aafebec 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from pyomo.common.config import ConfigDict diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py b/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py index cdfb732343..2e06aa178c 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import enum diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index 4ed2dcdf0f..d57d232e38 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from typing import MutableMapping, Sequence from pyomo.core.base.var import _GeneralVarData diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index d55161573e..4ffb44869e 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import numpy as np import tensorflow as tf from tensorflow import keras diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py index 4934980696..0f6dc1a9f8 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from .dr_config import DRConfig from pyomo.common.config import ConfigValue diff --git a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py index f8cff8f4c9..2728c3dd3a 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/tests/test_linear_dr.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import _check_dependencies import unittest import pyomo.environ as pe diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 60e798db25..6f010caea2 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( BTXParameterBlock, diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index a1ac85a6c8..a82f280e6a 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from pyomo.core.base.block import _BlockData import idaes.apps.flexibility_analysis as flexibility diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index 925cb8580a..d732333409 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index 7674d358a4..78f4a97662 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import ( _check_dependencies, _check_relu_dependencies, diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index 919373a45c..4d72b6352b 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from pyomo.core.base.block import _BlockData import pyomo.environ as pe from typing import Sequence, Union, Mapping, MutableMapping, Optional diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 1ee991b2b6..c53fc9a6a7 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import numpy as np from .kkt import add_kkt_with_milp_complementarity_conditions from pyomo.core.base.block import _BlockData diff --git a/idaes/apps/flexibility_analysis/indices.py b/idaes/apps/flexibility_analysis/indices.py index 79e8639961..05a003f5a5 100644 --- a/idaes/apps/flexibility_analysis/indices.py +++ b/idaes/apps/flexibility_analysis/indices.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# class _ConIndex(object): def __init__(self, con, bound): self._con = con diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index 0afdd27d0e..d59c4236fb 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData, ScalarVar diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index 86c4222099..9faad15f1a 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 3a1cdfbcb9..cb75cd20a1 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from __future__ import annotations from pyomo.core.base.block import _BlockData import pyomo.environ as pe diff --git a/idaes/apps/flexibility_analysis/simplify.py b/idaes/apps/flexibility_analysis/simplify.py index 671cb4cf2f..6b77ec93e8 100644 --- a/idaes/apps/flexibility_analysis/simplify.py +++ b/idaes/apps/flexibility_analysis/simplify.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression from pyomo.core.expr import is_fixed, value diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 20d80aa2a2..fdd8db27d5 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe from idaes.apps.flexibility_analysis.flextest import ( diff --git a/idaes/apps/flexibility_analysis/tests/test_indices.py b/idaes/apps/flexibility_analysis/tests/test_indices.py index f0fe2c7f62..43deb5b3ae 100644 --- a/idaes/apps/flexibility_analysis/tests/test_indices.py +++ b/idaes/apps/flexibility_analysis/tests/test_indices.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import _check_dependencies import unittest import pyomo.environ as pe diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index bd732ffad5..abbe067b0c 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe from idaes.apps.flexibility_analysis.sampling import ( diff --git a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py index d91f059cc0..33b0edec21 100644 --- a/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py +++ b/idaes/apps/flexibility_analysis/tests/test_uncertain_params.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe import unittest diff --git a/idaes/apps/flexibility_analysis/tests/test_var_utils.py b/idaes/apps/flexibility_analysis/tests/test_var_utils.py index e09f82f60b..421cab3fb9 100644 --- a/idaes/apps/flexibility_analysis/tests/test_var_utils.py +++ b/idaes/apps/flexibility_analysis/tests/test_var_utils.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from idaes.apps.flexibility_analysis import _check_dependencies import pyomo.environ as pe import unittest diff --git a/idaes/apps/flexibility_analysis/uncertain_params.py b/idaes/apps/flexibility_analysis/uncertain_params.py index f4e31c8573..e2d05a479a 100644 --- a/idaes/apps/flexibility_analysis/uncertain_params.py +++ b/idaes/apps/flexibility_analysis/uncertain_params.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# from pyomo.core.base.block import _BlockData import pyomo.environ as pe from pyomo.core.expr.visitor import replace_expressions diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 38fef0f2d3..4ccb5a8b06 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -1,3 +1,15 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.common.collections import ComponentSet From 8c2b8e6575462fce438453b962f8443f81eebbef Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 21:42:08 -0600 Subject: [PATCH 38/60] update tests --- idaes/apps/flexibility_analysis/tests/test_flextest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index fdd8db27d5..c83f44199e 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -124,7 +124,7 @@ class TestFlexTest(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() var_bounds = pe.ComponentMap() - var_bounds[m.z] = (-20, 20) + var_bounds[m.z] = (-10, 10) build_active_constraint_flextest( m, uncertain_params=list(nominal_values.keys()), From 4a620566cd91d45fb610ae717488f35ba159e807 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 22:10:56 -0600 Subject: [PATCH 39/60] update flex tests --- .../tests/test_flextest.py | 2 +- .../tests/test_sampling.py | 88 +++++++++++-------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index c83f44199e..6da8732ae3 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -124,7 +124,7 @@ class TestFlexTest(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() var_bounds = pe.ComponentMap() - var_bounds[m.z] = (-10, 10) + var_bounds[m.z] = (-10, 10) # originally used (-20, 20), but the scip version was sensitive to this build_active_constraint_flextest( m, uncertain_params=list(nominal_values.keys()), diff --git a/idaes/apps/flexibility_analysis/tests/test_sampling.py b/idaes/apps/flexibility_analysis/tests/test_sampling.py index abbe067b0c..3dcefcb1b0 100644 --- a/idaes/apps/flexibility_analysis/tests/test_sampling.py +++ b/idaes/apps/flexibility_analysis/tests/test_sampling.py @@ -21,6 +21,22 @@ import numpy as np import pytest from pyomo.contrib import appsi +from idaes.core.util.testing import _enable_scip_solver_for_testing +from contextlib import contextmanager + + +@contextmanager +def scip_solver(): + solver = pe.SolverFactory("scip") + undo_changes = None + + if not solver.available(): + undo_changes = _enable_scip_solver_for_testing() + if not solver.available(): + pytest.skip(reason="SCIP solver not available") + yield solver + if undo_changes is not None: + undo_changes() def create_poly_model(): @@ -108,45 +124,45 @@ def create_hx_network_model(): class TestSampling(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() - opt = pe.SolverFactory("scip") - config = SamplingConfig() - config.solver = opt - config.num_points = 5 - config.strategy = SamplingStrategy.grid - tmp = perform_sampling( - m, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - controls=[m.z], - in_place=True, - config=config, - ) - sample_points, max_violation_values, control_values = tmp - max_viol_ndx = np.argmax(max_violation_values) - self.assertAlmostEqual(max_violation_values[max_viol_ndx], -1.0142, 4) - self.assertAlmostEqual(control_values[m.z][max_viol_ndx], 4.6319, 4) - self.assertAlmostEqual(sample_points[m.theta][max_viol_ndx], 22.5) + with scip_solver() as opt: + config = SamplingConfig() + config.solver = opt + config.num_points = 5 + config.strategy = SamplingStrategy.grid + tmp = perform_sampling( + m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.z], + in_place=True, + config=config, + ) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = np.argmax(max_violation_values) + self.assertAlmostEqual(max_violation_values[max_viol_ndx], -1.0142, 4) + self.assertAlmostEqual(control_values[m.z][max_viol_ndx], 4.6319, 4) + self.assertAlmostEqual(sample_points[m.theta][max_viol_ndx], 22.5) def test_hx_network(self): m, nominal_values, param_bounds = create_hx_network_model() - opt = pe.SolverFactory("scip") - config = SamplingConfig() - config.solver = opt - config.num_points = 2 - config.strategy = SamplingStrategy.grid - tmp = perform_sampling( - m, - uncertain_params=list(nominal_values.keys()), - param_nominal_values=nominal_values, - param_bounds=param_bounds, - controls=[m.qc], - in_place=True, - config=config, - ) - sample_points, max_violation_values, control_values = tmp - max_viol_ndx = np.argmax(max_violation_values) - self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) + with scip_solver() as opt: + config = SamplingConfig() + config.solver = opt + config.num_points = 2 + config.strategy = SamplingStrategy.grid + tmp = perform_sampling( + m, + uncertain_params=list(nominal_values.keys()), + param_nominal_values=nominal_values, + param_bounds=param_bounds, + controls=[m.qc], + in_place=True, + config=config, + ) + sample_points, max_violation_values, control_values = tmp + max_viol_ndx = np.argmax(max_violation_values) + self.assertAlmostEqual(max_violation_values[max_viol_ndx], 8.8) def test_hx_network3(self): m, nominal_values, param_bounds = create_hx_network_model() From 81a2f99d4aab15670695f0d1a0d4bfe7b316b309 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 22:15:45 -0600 Subject: [PATCH 40/60] flex docs --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index f2be8e17b7..4f7e7e8d3d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,6 +10,7 @@ sphinxcontrib-napoleon>=0.5.0 sphinx-argparse==0.4.0 sphinx-book-theme<=1.1.2,>=1.0.0 sphinx-copybutton==0.5.2 +enum_tools ### testing and linting # TODO/NOTE pytest is specified as a dependency in setup.py, but we might want to pin a specific version here From 129e4551e7560ca86767e179a94999f60ab75095 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 22:17:28 -0600 Subject: [PATCH 41/60] run black --- idaes/apps/flexibility_analysis/tests/test_flextest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/tests/test_flextest.py b/idaes/apps/flexibility_analysis/tests/test_flextest.py index 6da8732ae3..7db8bf2304 100644 --- a/idaes/apps/flexibility_analysis/tests/test_flextest.py +++ b/idaes/apps/flexibility_analysis/tests/test_flextest.py @@ -124,7 +124,10 @@ class TestFlexTest(unittest.TestCase): def test_poly(self): m, nominal_values, param_bounds = create_poly_model() var_bounds = pe.ComponentMap() - var_bounds[m.z] = (-10, 10) # originally used (-20, 20), but the scip version was sensitive to this + var_bounds[m.z] = ( + -10, + 10, + ) # originally used (-20, 20), but the scip version was sensitive to this build_active_constraint_flextest( m, uncertain_params=list(nominal_values.keys()), From 22d830b09c2efb549d78017cbd95f403c05a5ee7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 22:25:45 -0600 Subject: [PATCH 42/60] update docs dependencies --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4f7e7e8d3d..8ec2e1ca6c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ sphinxcontrib-napoleon>=0.5.0 sphinx-argparse==0.4.0 sphinx-book-theme<=1.1.2,>=1.0.0 sphinx-copybutton==0.5.2 -enum_tools +enum-tools[sphinx] ### testing and linting # TODO/NOTE pytest is specified as a dependency in setup.py, but we might want to pin a specific version here From e83afb552bafd63d2db41addb5e1e9aa829f0910 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 22:42:02 -0600 Subject: [PATCH 43/60] update imports --- .../flexibility_analysis/decision_rules/relu_dr.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index 4ffb44869e..df500d3626 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -11,9 +11,9 @@ # for full copyright and license information. ################################################################################# import numpy as np -import tensorflow as tf -from tensorflow import keras -from tensorflow.keras import layers +from pyomo.common.dependencies import attempt_import +tf, tensorflow_available = attempt_import("tensorflow") +keras, keras_available = attempt_import("tensorflow.keras") import pyomo.environ as pe from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.block import _BlockData @@ -71,13 +71,13 @@ def construct_relu_decision_rule( nn = keras.Sequential() nn.add( - layers.Dense( + keras.layers.Dense( units=config.n_nodes_per_layer, input_dim=len(inputs), activation="relu" ) ) for layer_ndx in range(config.n_layers - 1): - nn.add(layers.Dense(config.n_nodes_per_layer, activation="relu")) - nn.add(layers.Dense(len(outputs))) + nn.add(keras.layers.Dense(config.n_nodes_per_layer, activation="relu")) + nn.add(keras.layers.Dense(len(outputs))) if config.learning_rate is None: opt = keras.optimizers.Adam() else: From c1e6f3f359c9daa067c73b698ec3a6bf29b113e8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 22:48:07 -0600 Subject: [PATCH 44/60] update imports --- idaes/apps/flexibility_analysis/decision_rules/relu_dr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index df500d3626..15d8f14e07 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -12,8 +12,6 @@ ################################################################################# import numpy as np from pyomo.common.dependencies import attempt_import -tf, tensorflow_available = attempt_import("tensorflow") -keras, keras_available = attempt_import("tensorflow.keras") import pyomo.environ as pe from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.block import _BlockData @@ -24,6 +22,9 @@ from idaes.apps.flexibility_analysis.indices import _VarIndex from .relu_dr_config import ReluDRConfig +tf, tensorflow_available = attempt_import("tensorflow") +keras, keras_available = attempt_import("tensorflow.keras") + def construct_relu_decision_rule( input_vals: MutableMapping[_GeneralVarData, Sequence[float]], From 955b8fd80eb07f0262da172398d5de48d2dd8ce7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 08:37:53 -0600 Subject: [PATCH 45/60] fix pylint errors --- idaes/apps/flexibility_analysis/__init__.py | 4 ++ .../flexibility_analysis/check_optimal.py | 11 ++++ .../decision_rules/relu_dr.py | 14 ++--- idaes/apps/flexibility_analysis/flex_index.py | 10 ++- idaes/apps/flexibility_analysis/flextest.py | 62 ++++++++++++------- idaes/apps/flexibility_analysis/kkt.py | 39 +++++++++--- idaes/apps/flexibility_analysis/simplify.py | 19 +++++- idaes/apps/flexibility_analysis/var_utils.py | 55 ++++++++++++++-- 8 files changed, 170 insertions(+), 44 deletions(-) diff --git a/idaes/apps/flexibility_analysis/__init__.py b/idaes/apps/flexibility_analysis/__init__.py index 0d85ba5fbe..1e74568112 100644 --- a/idaes/apps/flexibility_analysis/__init__.py +++ b/idaes/apps/flexibility_analysis/__init__.py @@ -10,6 +10,10 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +A module for formulating flexibility analysis problems (feasibility test and +flexibility index). +""" from .flextest import ( solve_flextest, SamplingStrategy, diff --git a/idaes/apps/flexibility_analysis/check_optimal.py b/idaes/apps/flexibility_analysis/check_optimal.py index ca221083d8..f12c3812a1 100644 --- a/idaes/apps/flexibility_analysis/check_optimal.py +++ b/idaes/apps/flexibility_analysis/check_optimal.py @@ -10,11 +10,22 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module provides a function like Pyomo's assert_optimal_termination but that +works for both APPSI solver interfaces and non-appsi solver interfaces. +""" import pyomo.environ as pe from pyomo.contrib import appsi def assert_optimal_termination(results): + """ + Raise an exception if the termination condition was not optimal. + + Parameters + ---------- + results: pyomo results object from calling solve() + """ if hasattr(results, "termination_condition"): assert results.termination_condition == appsi.base.TerminationCondition.optimal else: diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index 15d8f14e07..3a8f814245 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -16,14 +16,14 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.block import _BlockData from typing import MutableMapping, Sequence -from omlt import OmltBlock, OffsetScaling -from omlt.neuralnet import ReluBigMFormulation -from omlt.io import load_keras_sequential from idaes.apps.flexibility_analysis.indices import _VarIndex from .relu_dr_config import ReluDRConfig tf, tensorflow_available = attempt_import("tensorflow") keras, keras_available = attempt_import("tensorflow.keras") +omlt, omlt_available = attempt_import("omlt") +omlt_nn, _ = attempt_import("omlt.neuralnet") +omlt_io, _ = attempt_import("omlt.io") def construct_relu_decision_rule( @@ -103,9 +103,9 @@ def construct_relu_decision_rule( plt.close() res = pe.Block(concrete=True) - res.nn = OmltBlock() + res.nn = omlt.OmltBlock() - scaler = OffsetScaling( + scaler = omlt.OffsetScaling( offset_inputs=[float(i) for i in input_mean], factor_inputs=[float(i) for i in input_std], offset_outputs=[float(i) for i in output_mean], @@ -118,8 +118,8 @@ def construct_relu_decision_rule( ) for ndx, v in enumerate(inputs) } - net = load_keras_sequential(nn, scaler, input_bounds) - formulation = ReluBigMFormulation(net) + net = omlt_io.load_keras_sequential(nn, scaler, input_bounds) + formulation = omlt_nn.ReluBigMFormulation(net) res.nn.build_formulation(formulation) res.input_set = pe.Set() diff --git a/idaes/apps/flexibility_analysis/flex_index.py b/idaes/apps/flexibility_analysis/flex_index.py index 4d72b6352b..5dd896b373 100644 --- a/idaes/apps/flexibility_analysis/flex_index.py +++ b/idaes/apps/flexibility_analysis/flex_index.py @@ -10,9 +10,15 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module contains a function for solving the flexibility index problem using +the bisection method. A flexibility test problem is solved at each iteration. +""" +import math +import logging +from typing import Sequence, Union, Mapping, MutableMapping, Optional from pyomo.core.base.block import _BlockData import pyomo.environ as pe -from typing import Sequence, Union, Mapping, MutableMapping, Optional from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from .sampling import SamplingStrategy @@ -23,8 +29,6 @@ FlexTestTermination, FlexTest, ) -import math -import logging logger = logging.getLogger(__name__) diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index c53fc9a6a7..4efe5111a1 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -10,12 +10,29 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +A module for formulating different versions of the flexibility/feasibility +test problem. +""" +import enum +import logging import numpy as np -from .kkt import add_kkt_with_milp_complementarity_conditions +from typing import Sequence, Union, Mapping, MutableMapping, Optional, Tuple from pyomo.core.base.block import _BlockData from pyomo.common.dependencies import attempt_import - +from pyomo.util.report_scaling import report_scaling +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData +from pyomo.contrib.solver.util import get_objective import pyomo.environ as pe +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + PositiveFloat, + InEnum, + MarkImmutable, + NonNegativeFloat, +) from .var_utils import ( get_used_unfixed_variables, BoundsManager, @@ -23,31 +40,16 @@ _apply_var_bounds, ) from .indices import _VarIndex, _ConIndex +from .kkt import add_kkt_with_milp_complementarity_conditions from .uncertain_params import _replace_uncertain_params from .inner_problem import _build_inner_problem -from pyomo.util.report_scaling import report_scaling -import logging -from typing import Sequence, Union, Mapping, MutableMapping, Optional, Tuple -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.param import _ParamData -from pyomo.contrib.solver.util import get_objective from .decision_rules.linear_dr import construct_linear_decision_rule -from pyomo.common.dependencies import attempt_import from .sampling import ( SamplingStrategy, perform_sampling, SamplingConfig, _perform_sampling, ) -import enum -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - PositiveFloat, - InEnum, - MarkImmutable, - NonNegativeFloat, -) relu_dr, relu_dr_available = attempt_import( "idaes.apps.flexibility_analysis.decision_rules.relu_dr", @@ -75,6 +77,11 @@ def _get_longest_name(comps): class FlexTestMethod(enum.Enum): + """ + Enum for specifying how to formulate the + flexibility test problem. + """ + active_constraint = enum.auto() linear_decision_rule = enum.auto() relu_decision_rule = enum.auto() @@ -223,6 +230,11 @@ def __init__( class FlexTestTermination(enum.Enum): + """ + An enum for communicating the results of a + flexibility test problem. + """ + found_infeasible_point = enum.auto() proven_feasible = enum.auto() uncertain = enum.auto() @@ -257,7 +269,7 @@ def __str__(self): s = f"Termination: {self.termination}\n" s += f"Maximum constraint violation: {self.max_constraint_violation}\n" if self.unc_param_values_at_max_violation is not None: - s += f"Uncertain parameter values at maximum constraint violation: \n" + s += "Uncertain parameter values at maximum constraint violation: \n" longest_param_name = _get_longest_name( self.unc_param_values_at_max_violation.keys() ) @@ -296,6 +308,9 @@ def build_flextest_with_dr( valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], config: FlexTestConfig, ): + """ + Build the flexibility test problem using a decision rule. + """ config.sampling_config.total_violation = config.total_violation # this has to be here in case tensorflow or omlt are not installed @@ -317,7 +332,7 @@ def build_flextest_with_dr( config=config.sampling_config, ) - _sample_points, max_violation_values, control_values = tmp + _sample_points, _, control_values = tmp # replace uncertain parameters with variables _replace_uncertain_params(m, uncertain_params, param_nominal_values, param_bounds) @@ -401,6 +416,9 @@ def build_active_constraint_flextest( valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], config: Optional[ActiveConstraintConfig] = None, ): + """ + Build the flexibility test problem using the active constraint method. + """ if config is None: config = ActiveConstraintConfig() @@ -573,7 +591,7 @@ def _solve_flextest_sampling( in_place=True, config=config.sampling_config, ) - sample_points, max_violation_values, control_values = tmp + sample_points, max_violation_values, _ = tmp max_viol_ndx = int(np.argmax(max_violation_values)) results = FlexTestResults() @@ -610,7 +628,7 @@ def _solve_flextest_vertex_enumeration( in_place=True, config=config.sampling_config, ) - sample_points, max_violation_values, control_values = tmp + sample_points, max_violation_values, _ = tmp max_viol_ndx = int(np.argmax(max_violation_values)) results = FlexTestResults() diff --git a/idaes/apps/flexibility_analysis/kkt.py b/idaes/apps/flexibility_analysis/kkt.py index 9faad15f1a..8c51a4da8f 100644 --- a/idaes/apps/flexibility_analysis/kkt.py +++ b/idaes/apps/flexibility_analysis/kkt.py @@ -10,20 +10,23 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module contains functions for generating the KKT system for the +inner problem of the flexibility/feasibility test problem. +""" +from typing import Sequence, Mapping import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd - from pyomo.core.base.block import _BlockData from pyomo.contrib.fbbt.fbbt import fbbt -from .var_utils import ( - get_used_unfixed_variables, - _apply_var_bounds, -) -from typing import Sequence, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression from pyomo.contrib.solver.util import get_objective from .indices import _VarIndex, _ConIndex +from .var_utils import ( + get_used_unfixed_variables, + _apply_var_bounds, +) def _simplify(expr): @@ -80,7 +83,9 @@ def _add_grad_lag_constraints(m: _BlockData) -> _BlockData: m.grad_lag_set = pe.Set() m.grad_lag = pe.Constraint(m.grad_lag_set) for v in primal_vars: - if v in grad_lag and (type(grad_lag[v]) != float or grad_lag[v] != 0): + if v in grad_lag and ( + type(grad_lag[v]) not in {float, int} or grad_lag[v] != 0 + ): key = _VarIndex(v, None) m.grad_lag_set.add(key) m.grad_lag[key] = _simplify(grad_lag[v]) == 0 @@ -144,6 +149,26 @@ def add_kkt_with_milp_complementarity_conditions( valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]], default_M=None, ) -> _BlockData: + """ + Generate the KKT conditions for the model m using a MIP + representation of the complementarity conditions. + + Parameters + ---------- + m: _BlockData + The model for which to build the KKT conditions + uncertain_params: Sequence[_GeneralVarData] + These variables are decisions for the outer problem + and should be considered parameters when generating + the KKT conditions + valid_var_bounds: Mapping[_GeneralVarData, Sequence[float]] + Variable bounds that are valid for any values of the + uncertain parameters. This is only used to make the + resulting problem more tractable. + default_M: Optional[float] + M value to use for the Big-M representation of the + complementarity conditions + """ for v in uncertain_params: v.fix() diff --git a/idaes/apps/flexibility_analysis/simplify.py b/idaes/apps/flexibility_analysis/simplify.py index 6b77ec93e8..bbb2acfa7a 100644 --- a/idaes/apps/flexibility_analysis/simplify.py +++ b/idaes/apps/flexibility_analysis/simplify.py @@ -10,12 +10,29 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module provides a function to simplify pyomo expressions with sympy +""" +from typing import Union from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression from pyomo.core.expr import is_fixed, value -def simplify_expr(expr: NumericExpression): +def simplify_expr(expr: NumericExpression) -> Union[float, NumericExpression]: + """ + Simplify a pyomo expression using sympy + + Parameters + ---------- + expr: NumericExpression + The pyomo expresssion to be simplified + + Returns + ------- + new_expr: Union[float, NumericExpression] + The simplified expression + """ om, se = sympyify_expression(expr) se = se.simplify() new_expr = sympy2pyomo_expression(se, om) diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 4ccb5a8b06..8273c565a3 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -10,17 +10,32 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +Some utility functions for working with pyomo variables. +""" +from typing import Mapping, Sequence, MutableSet import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.common.collections import ComponentSet from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.solver.util import get_objective - -from typing import Mapping, Sequence from pyomo.core.base.var import _GeneralVarData -def get_all_unfixed_variables(m: _BlockData): +def get_all_unfixed_variables(m: _BlockData) -> MutableSet[_GeneralVarData]: + """ + Returns a set containing all unfixed variables on the model m. + + Parameters + ---------- + m: _BlockData + The model for which to get the variables. + + Returns + ------- + vset: MutableSet[_GeneralVarData] + The set of all variables on m. + """ return ComponentSet( v for v in m.component_data_objects(pe.Var, descend_into=True, active=True) @@ -28,7 +43,21 @@ def get_all_unfixed_variables(m: _BlockData): ) -def get_used_unfixed_variables(m: _BlockData): +def get_used_unfixed_variables(m: _BlockData) -> MutableSet[_GeneralVarData]: + """ + Returns a set containing all unfixed variables in any active constraint + or objective on the model m. + + Parameters + ---------- + m: _BlockData + The model for which to get the variables. + + Returns + ------- + vset: MutableSet[_GeneralVarData] + The set of all variables in active constriants or objectives. + """ res = ComponentSet() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): res.update(v for v in identify_variables(c.body, include_fixed=False)) @@ -39,18 +68,36 @@ def get_used_unfixed_variables(m: _BlockData): class BoundsManager(object): + """ + A class for saving and restoring variable bounds. + """ + def __init__(self, m: _BlockData): # TODO: maybe use get_used_unfixed_variables here? self._vars = ComponentSet(m.component_data_objects(pe.Var, descend_into=True)) self._saved_bounds = list() def save_bounds(self): + """ + Save the variable bounds for later use. + """ bnds = pe.ComponentMap() for v in self._vars: bnds[v] = (v.lb, v.ub) self._saved_bounds.append(bnds) def pop_bounds(self, ndx=-1): + """ + Restore the variable bounds that were + previously saved. + + Parameters + ---------- + ndx: int + Indicates which set of bounds to restore. + By default, this will use the most recently + saved bounds. + """ bnds = self._saved_bounds.pop(ndx) for v, _bnds in bnds.items(): lb, ub = _bnds From bee33de8c95a5e3ce1fb03aad86ce760ef0f8425 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 09:13:02 -0600 Subject: [PATCH 46/60] fix pylint errors --- .../_check_dependencies.py | 5 +- idaes/apps/flexibility_analysis/flextest.py | 52 +++++++++++++++++-- idaes/apps/flexibility_analysis/sampling.py | 20 ++++++- idaes/apps/flexibility_analysis/simplify.py | 2 +- idaes/apps/flexibility_analysis/var_utils.py | 2 +- 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/idaes/apps/flexibility_analysis/_check_dependencies.py b/idaes/apps/flexibility_analysis/_check_dependencies.py index 1e36a72c65..974ef1bf2d 100644 --- a/idaes/apps/flexibility_analysis/_check_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_dependencies.py @@ -10,8 +10,11 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# -from pyomo.common.dependencies import attempt_import +""" +This module is only used to check dependencies for unit tests. +""" import unittest +from pyomo.common.dependencies import attempt_import np, nump_available = attempt_import("numpy") if not nump_available: diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 4efe5111a1..73827d47d2 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -16,8 +16,8 @@ """ import enum import logging -import numpy as np from typing import Sequence, Union, Mapping, MutableMapping, Optional, Tuple +import numpy as np from pyomo.core.base.block import _BlockData from pyomo.common.dependencies import attempt_import from pyomo.util.report_scaling import report_scaling @@ -742,6 +742,12 @@ def solve_flextest( class FlexTest(object): + """ + This class is mostly here for the flexibility index problem. + This class enables solving the same flexibility test problem + repeatedly with different uncertain parameter bounds. + """ + def __init__( self, m: _BlockData, @@ -749,9 +755,38 @@ def __init__( param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], max_param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], controls: Sequence[_GeneralVarData], - valid_var_bounds: MutableMapping[_GeneralVarData, Sequence[float]], + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], config: Optional[FlexTestConfig] = None, ): + r""" + Parameters + ---------- + m: _BlockData + The pyomo model to be used for the feasibility/flexibility test. + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]] + A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). + These can be pyomo variables (Var) or parameters (param). However, if parameters are used, + they must be mutable. + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + nominal values (:math:`\theta^{N}`). + max_param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to the + widest possible bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`) that will + be considered for any call to solve(). + controls: Sequence[_GeneralVarData] + A sequence (e.g., list) defining the set of control variables (:math:`z`). + valid_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]] + A mapping (e.g., ComponentMap) defining bounds for all variables (:math:`x` and :math:`z`) that + should be valid for any :math:`\theta` between :math:`\underline{\theta}` and + :math:`\overline{\theta}`. These are only used to make the resulting flexibility test problem + more computationally tractable. All variable bounds in the model `m` are treated as performance + constraints and relaxed (:math:`g_{j}(x, z, \theta) \leq u`). The bounds in `valid_var_bounds` + are applied to the single-level problem generated from the active constraint method or one of + the decision rules. This argument is not necessary for vertex enumeration or sampling. + config: Optional[FlexTestConfig] + An object defining options for how the flexibility test should be solved. + """ if config is None: self.config: FlexTestConfig = FlexTestConfig() else: @@ -878,7 +913,7 @@ def _solve_sampling( controls=self._controls, config=self.config.sampling_config, ) - sample_points, max_violation_values, control_values = tmp + sample_points, max_violation_values, _ = tmp sample_points = pe.ComponentMap( (self._clone_param_orig_param_map[p], vals) for p, vals in sample_points.items() @@ -897,8 +932,17 @@ def _solve_sampling( return results def solve( - self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]] + self, param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float]] ) -> FlexTestResults: + r""" + Solve the flexibility test problem for the specified uncertain parameter bounds. + + Parameters + ---------- + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`). + """ if self.config.method in { FlexTestMethod.active_constraint, FlexTestMethod.linear_decision_rule, diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index cb75cd20a1..5dc8b9da15 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -10,6 +10,10 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module provides functions for solving the inner problem of the flexibility +test at different samples of the uncertain parameters. +""" from __future__ import annotations from pyomo.core.base.block import _BlockData import pyomo.environ as pe @@ -31,11 +35,15 @@ from tqdm import tqdm except ImportError: - def tqdm(items, ncols, desc, disable): + def tqdm(items, ncols, desc, disable): # pylint: disable=missing-function-docstring return items class SamplingStrategy(enum.Enum): + """ + An enum for specifying the sampling method to use. + """ + grid = "grid" lhs = "lhs" @@ -45,6 +53,12 @@ class SamplingStrategy(enum.Enum): class SamplingInitStrategy(enum.Enum): + """ + An enum for specifying how to initialize the inner problem + of the flexibility test for each sample of the uncertain + parameters + """ + none = "none" square = "square" min_control_deviation = "min_control_deviation" @@ -82,14 +96,17 @@ def __init__( self.next_param = next_param def reset(self): + # pylint: disable=missing-function-docstring self.state = _GridSamplingState.increment self.ndx = 0 def get_value(self): + # pylint: disable=missing-function-docstring res = self.pts[self.ndx] return res def swap_state(self): + # pylint: disable=missing-function-docstring if self.state == _GridSamplingState.increment: self.state = _GridSamplingState.decrement else: @@ -97,6 +114,7 @@ def swap_state(self): self.state = _GridSamplingState.increment def step(self) -> bool: + # pylint: disable=missing-function-docstring if self.state == _GridSamplingState.increment: if self.ndx == len(self.pts) - 1: if self.next_param is None: diff --git a/idaes/apps/flexibility_analysis/simplify.py b/idaes/apps/flexibility_analysis/simplify.py index bbb2acfa7a..ad1629a16e 100644 --- a/idaes/apps/flexibility_analysis/simplify.py +++ b/idaes/apps/flexibility_analysis/simplify.py @@ -26,7 +26,7 @@ def simplify_expr(expr: NumericExpression) -> Union[float, NumericExpression]: Parameters ---------- expr: NumericExpression - The pyomo expresssion to be simplified + The pyomo expression to be simplified Returns ------- diff --git a/idaes/apps/flexibility_analysis/var_utils.py b/idaes/apps/flexibility_analysis/var_utils.py index 8273c565a3..ba2c67c4d9 100644 --- a/idaes/apps/flexibility_analysis/var_utils.py +++ b/idaes/apps/flexibility_analysis/var_utils.py @@ -56,7 +56,7 @@ def get_used_unfixed_variables(m: _BlockData) -> MutableSet[_GeneralVarData]: Returns ------- vset: MutableSet[_GeneralVarData] - The set of all variables in active constriants or objectives. + The set of all variables in active constraints or objectives. """ res = ComponentSet() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): From b451df1dcc3c40093f35a2b04a45945b2282fccc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 09:52:54 -0600 Subject: [PATCH 47/60] fix pylint errors --- .../_check_relu_dependencies.py | 5 +- .../examples/idaes_hx_network.py | 12 ++-- .../examples/linear_hx_network.py | 33 ++++++++-- idaes/apps/flexibility_analysis/flextest.py | 4 +- idaes/apps/flexibility_analysis/indices.py | 8 +++ .../flexibility_analysis/inner_problem.py | 9 ++- idaes/apps/flexibility_analysis/sampling.py | 61 +++++++++++++++---- .../flexibility_analysis/uncertain_params.py | 11 ++-- 8 files changed, 111 insertions(+), 32 deletions(-) diff --git a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py index 2860b057c0..4f07719817 100644 --- a/idaes/apps/flexibility_analysis/_check_relu_dependencies.py +++ b/idaes/apps/flexibility_analysis/_check_relu_dependencies.py @@ -10,8 +10,11 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# -from pyomo.common.dependencies import attempt_import +""" +This module is only used to check dependencies for unit tests. +""" import unittest +from pyomo.common.dependencies import attempt_import tensorflow, tensorflow_available = attempt_import("tensorflow") omlt, nump_available = attempt_import("omlt") diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 6f010caea2..e5861b53bd 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -10,24 +10,24 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +import logging import pyomo.environ as pe +from pyomo.contrib.fbbt.fbbt import fbbt +from pyomo.network import Arc +from pyomo.util.infeasible import log_infeasible_constraints, log_infeasible_bounds +from pyomo.core.base.block import _BlockData +from pyomo.contrib.solver.util import get_objective from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( BTXParameterBlock, ) from idaes.core import FlowsheetBlock from idaes.models.unit_models.heater import Heater -import logging -from pyomo.contrib.fbbt.fbbt import fbbt -from pyomo.network import Arc from idaes.core.util.initialization import propagate_state -from pyomo.util.infeasible import log_infeasible_constraints, log_infeasible_bounds from idaes.core.base.control_volume_base import ControlVolumeBlockData -from pyomo.core.base.block import _BlockData import idaes.apps.flexibility_analysis as flexibility from idaes.apps.flexibility_analysis.var_utils import BoundsManager from idaes.apps.flexibility_analysis.simplify import simplify_expr -from pyomo.contrib.solver.util import get_objective logging.basicConfig(level=logging.INFO) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index a82f280e6a..4c3a5ea65f 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -10,13 +10,20 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +A flexibility analysis example from + + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693. +""" +from typing import Tuple, Mapping +import random +import numpy as np import pyomo.environ as pe from pyomo.core.base.block import _BlockData -import idaes.apps.flexibility_analysis as flexibility -from typing import Tuple, Mapping from pyomo.contrib.fbbt import interval -import numpy as np -import random +import idaes.apps.flexibility_analysis as flexibility def create_model() -> Tuple[_BlockData, Mapping, Mapping]: @@ -77,6 +84,10 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: def get_var_bounds(m): + """ + Generate a map with valid variable bounds for + any possible realization of the uncertain parameters + """ res = pe.ComponentMap() for v in m.variable_temps.values(): res[v] = (100, 1000) @@ -89,6 +100,20 @@ def main( method: flexibility.FlexTestMethod = flexibility.FlexTestMethod.active_constraint, plot_history=True, ): + """ + Run the example + + Parameters + ---------- + flex_index: bool + If True, the flexibility index will be solved. Otherwise, the flexibility + test will be solved. + method: flexibility.FlexTestMethod + The method to use for the flexibility test + plot_history: bool + Only used if method is flexibility.FlexTestMethod.relu_decision_rule; + Plots the training history for the neural network + """ np.random.seed(0) random.seed(1) m, nominal_values, param_bounds = create_model() diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 73827d47d2..9ee6d89e37 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -940,8 +940,8 @@ def solve( Parameters ---------- param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]] - A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their - bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`). + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`). """ if self.config.method in { FlexTestMethod.active_constraint, diff --git a/idaes/apps/flexibility_analysis/indices.py b/idaes/apps/flexibility_analysis/indices.py index 05a003f5a5..515fd2e2ac 100644 --- a/idaes/apps/flexibility_analysis/indices.py +++ b/idaes/apps/flexibility_analysis/indices.py @@ -10,6 +10,10 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module contains some utility classes for creating meaningful indices +when formulating the flexibility test problem. +""" class _ConIndex(object): def __init__(self, con, bound): self._con = con @@ -17,10 +21,12 @@ def __init__(self, con, bound): @property def con(self): + # pylint: disable=missing-function-docstring return self._con @property def bound(self): + # pylint: disable=missing-function-docstring return self._bound def __repr__(self): @@ -48,10 +54,12 @@ def __init__(self, var, bound): @property def var(self): + # pylint: disable=missing-function-docstring return self._var @property def bound(self): + # pylint: disable=missing-function-docstring return self._bound def __repr__(self): diff --git a/idaes/apps/flexibility_analysis/inner_problem.py b/idaes/apps/flexibility_analysis/inner_problem.py index d59c4236fb..d64fb9468c 100644 --- a/idaes/apps/flexibility_analysis/inner_problem.py +++ b/idaes/apps/flexibility_analysis/inner_problem.py @@ -10,11 +10,16 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module contains functions for formulating the inner problem of the +flexibility test problem +""" +from typing import MutableMapping, Tuple, Optional, Mapping, Union +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData, ScalarVar from pyomo.core.expr.numeric_expr import ExpressionBase - from pyomo.contrib.solver.util import get_objective from .var_utils import ( BoundsManager, @@ -23,8 +28,6 @@ _remove_var_bounds, ) from .indices import _ConIndex, _VarIndex -from typing import MutableMapping, Tuple, Optional, Mapping, Union -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr def _get_g_bounds( diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 5dc8b9da15..03d362b800 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -15,20 +15,20 @@ test at different samples of the uncertain parameters. """ from __future__ import annotations +import enum +from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple, List +import numpy as np from pyomo.core.base.block import _BlockData import pyomo.environ as pe -import numpy as np -from typing import Sequence, Union, Mapping, Optional, MutableMapping, Tuple, List from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from .uncertain_params import _replace_uncertain_params -from .inner_problem import _build_inner_problem -import enum -from idaes.core.surrogate.pysmo.sampling import LatinHypercubeSampling -from .indices import _VarIndex from pyomo.common.config import ConfigDict, ConfigValue, InEnum from pyomo.contrib.solver.util import get_objective from pyomo.common.errors import ApplicationError +from idaes.core.surrogate.pysmo.sampling import LatinHypercubeSampling +from .uncertain_params import _replace_uncertain_params +from .inner_problem import _build_inner_problem +from .indices import _VarIndex from .check_optimal import assert_optimal_termination try: @@ -162,7 +162,8 @@ def __next__(self): return res def __iter__(self): - [i.reset() for i in self.param_iterators] + for i in self.param_iterators: + i.reset() self.done = False return self @@ -447,15 +448,51 @@ def perform_sampling( m: _BlockData, uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]], param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float], - param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]], controls: Optional[Sequence[_GeneralVarData]], in_place: bool, config: SamplingConfig, ) -> Tuple[ - MutableMapping[Union[_GeneralVarData, _ParamData], Sequence[float]], - Sequence[float], - MutableMapping[_GeneralVarData, Sequence[float]], + MutableMapping[Union[_GeneralVarData, _ParamData], List[float]], + List[float], + MutableMapping[_GeneralVarData, List[float]], ]: + r""" + Sample values of the uncertain parameters and solve the inner problem + of the flexibility test for each sample. + + Parameters + ---------- + m: _BlockData + The pyomo model to be used for the feasibility/flexibility test. + uncertain_params: Sequence[Union[_GeneralVarData, _ParamData]] + A sequence (e.g., list) defining the set of uncertain parameters (:math:`\theta`). + These can be pyomo variables (Var) or parameters (param). However, if parameters are used, + they must be mutable. + param_nominal_values: Mapping[Union[_GeneralVarData, _ParamData], float] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + nominal values (:math:`\theta^{N}`). + param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Tuple[float, float]] + A mapping (e.g., ComponentMap) from the uncertain parameters (:math:`\theta`) to their + bounds (:math:`\underline{\theta}`, :math:`\overline{\theta}`). + controls: Optional[Sequence[_GeneralVarData]] + A sequence (e.g., list) defining the set of control variables (:math:`z`). + in_place: bool + If True, m is modified in place to generate the model for solving the flexibility test. If False, + the model is cloned first. + config: SamplingConfig + An object defining options for how the flexibility test should be solved. + + Returns + ------- + sample_points: MutableMapping[Union[_GeneralVarData, _ParamData], List[float]] + The sampled values of the uncertain parameters + obj_values: List[float] + The value of the maximum (or total) constraint violation for each sample + of the uncertain parameters + control_values: MutableMapping[_GeneralVarData, List[float]] + The optimal values of the controls for each sample of the uncertain parameters + """ original_model = m if not in_place: m = m.clone() diff --git a/idaes/apps/flexibility_analysis/uncertain_params.py b/idaes/apps/flexibility_analysis/uncertain_params.py index e2d05a479a..e5540175e1 100644 --- a/idaes/apps/flexibility_analysis/uncertain_params.py +++ b/idaes/apps/flexibility_analysis/uncertain_params.py @@ -10,14 +10,17 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module defines a function for replacing uncertain parameters with variables +""" +from typing import Sequence, Union, Mapping from pyomo.core.base.block import _BlockData import pyomo.environ as pe from pyomo.core.expr.visitor import replace_expressions -from .indices import _VarIndex, _ConIndex -from typing import Sequence, Union, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from .var_utils import get_all_unfixed_variables +from .indices import _VarIndex, _ConIndex def _replace_uncertain_params( @@ -27,11 +30,11 @@ def _replace_uncertain_params( param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], ) -> _BlockData: for v in get_all_unfixed_variables(m): - if not pe.is_constant(v._lb): + if not pe.is_constant(v.lower): raise ValueError( f"The lower bound on {str(v)} is not constant. All variable bounds must be constant." ) - if not pe.is_constant(v._ub): + if not pe.is_constant(v.upper): raise ValueError( f"The upper bound on {str(v)} is not constant. All variable bounds must be constant." ) From 23061a1eaf46ddf409bd0094aa0c3c06cb34dde6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 12:27:45 -0600 Subject: [PATCH 48/60] fix pylint errors --- .../examples/idaes_hx_network.py | 66 +++++++++---------- .../examples/linear_hx_network.py | 2 +- idaes/apps/flexibility_analysis/indices.py | 2 + idaes/apps/flexibility_analysis/sampling.py | 4 +- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index e5861b53bd..2c3bcdfed5 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -10,11 +10,14 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This is a flexibility analysis example using an IDAES model of a +heat exchanger network. +""" import logging import pyomo.environ as pe from pyomo.contrib.fbbt.fbbt import fbbt from pyomo.network import Arc -from pyomo.util.infeasible import log_infeasible_constraints, log_infeasible_bounds from pyomo.core.base.block import _BlockData from pyomo.contrib.solver.util import get_objective from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( @@ -34,6 +37,10 @@ def create_model(): + """ + This function creates an IDAES model of a + heat exchanger network for flexibility analysis. + """ m = pe.ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) m.fs.properties = BTXParameterBlock( @@ -162,8 +169,7 @@ def create_model(): p.unfix() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): - lower, body, upper = c.lower, c.body, c.upper - body = simplify_expr(body) + body = simplify_expr(c.body) c.set_value((c.lower, body, c.upper)) for p in nominal_values.keys(): @@ -173,17 +179,10 @@ def create_model(): return m, nominal_values, param_bounds -def remove_inequalities(m): - for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): - if not c.equality: - c.deactivate() - - for v in m.component_data_objects(pe.Var, descend_into=True): - v.setlb(None) - v.setub(None) - - def initialize(m): + """ + Initialize the model + """ m.fs.heater1.heat_duty[0].fix(0.1000) m.fs.heater1.initialize() m.fs.heater1.heat_duty[0].unfix() @@ -211,6 +210,10 @@ def initialize(m): def get_var_bounds(m: _BlockData, param_bounds): + """ + Generate a map with valid variable bounds for + any possible realization of the uncertain parameters + """ for p, (p_lb, p_ub) in param_bounds.items(): p.unfix() p.setlb(p_lb) @@ -273,6 +276,15 @@ def get_var_bounds(m: _BlockData, param_bounds): def scale_model(m): + """ + This function scales the heat exchanger network model + prior to performing flexibility analysis + + Parameters + ---------- + m: _BlockData + The IDAES model of the heat exchanger network + """ m.scaling_factor = pe.Suffix(direction=pe.Suffix.EXPORT) for b in m.block_data_objects(active=True, descend_into=True): if isinstance(b, ControlVolumeBlockData): @@ -317,27 +329,15 @@ def scale_model(m): pe.TransformationFactory("core.scale_model").apply_to(m, rename=False) -def nominal_optimization(): - m, nominal_values, param_bounds = create_model() - initialize(m) - - log_infeasible_constraints(m, log_variables=False) - log_infeasible_bounds(m) - - opt = pe.SolverFactory("ipopt") - res = opt.solve(m, tee=True) - pe.assert_optimal_termination(res) - - m.fs.heater1.report() - m.fs.cooler1.report() - m.fs.heater2.report() - m.fs.cooler2.report() - m.fs.heater3.report() - m.fs.cooler3.report() - m.fs.cooler4.report() - +def main(method: flexibility.FlexTestMethod): + """ + Run the example -def main(method): + Parameters + ---------- + method: flexibility.FlexTestMethod + The method to use for the flexibility test + """ m, nominal_values, param_bounds = create_model() initialize(m) var_bounds = get_var_bounds(m, param_bounds) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 4c3a5ea65f..0247b0b433 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -85,7 +85,7 @@ def create_model() -> Tuple[_BlockData, Mapping, Mapping]: def get_var_bounds(m): """ - Generate a map with valid variable bounds for + Generate a map with valid variable bounds for any possible realization of the uncertain parameters """ res = pe.ComponentMap() diff --git a/idaes/apps/flexibility_analysis/indices.py b/idaes/apps/flexibility_analysis/indices.py index 515fd2e2ac..0e057c0b4e 100644 --- a/idaes/apps/flexibility_analysis/indices.py +++ b/idaes/apps/flexibility_analysis/indices.py @@ -14,6 +14,8 @@ This module contains some utility classes for creating meaningful indices when formulating the flexibility test problem. """ + + class _ConIndex(object): def __init__(self, con, bound): self._con = con diff --git a/idaes/apps/flexibility_analysis/sampling.py b/idaes/apps/flexibility_analysis/sampling.py index 03d362b800..f3f82daae3 100644 --- a/idaes/apps/flexibility_analysis/sampling.py +++ b/idaes/apps/flexibility_analysis/sampling.py @@ -458,7 +458,7 @@ def perform_sampling( MutableMapping[_GeneralVarData, List[float]], ]: r""" - Sample values of the uncertain parameters and solve the inner problem + Sample values of the uncertain parameters and solve the inner problem of the flexibility test for each sample. Parameters @@ -488,7 +488,7 @@ def perform_sampling( sample_points: MutableMapping[Union[_GeneralVarData, _ParamData], List[float]] The sampled values of the uncertain parameters obj_values: List[float] - The value of the maximum (or total) constraint violation for each sample + The value of the maximum (or total) constraint violation for each sample of the uncertain parameters control_values: MutableMapping[_GeneralVarData, List[float]] The optimal values of the controls for each sample of the uncertain parameters From 3d5650880a040fda00a5c8e0d5f03caa724effcf Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 12:44:47 -0600 Subject: [PATCH 49/60] fix pylint errors --- .../decision_rules/dr_config.py | 3 ++ .../decision_rules/dr_enum.py | 8 +++++ .../decision_rules/linear_dr.py | 34 +++++++++++++++---- .../decision_rules/relu_dr.py | 33 ++++++++++++++---- .../decision_rules/relu_dr_config.py | 6 +++- .../examples/nonlin_hx_network.py | 21 +++++++++++- 6 files changed, 91 insertions(+), 14 deletions(-) diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py index 443aafebec..dbd49586e5 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_config.py @@ -10,6 +10,9 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module defines a config for specifying options related to decision rules +""" from pyomo.common.config import ConfigDict diff --git a/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py b/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py index 2e06aa178c..1c1a4c6d7e 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py +++ b/idaes/apps/flexibility_analysis/decision_rules/dr_enum.py @@ -10,9 +10,17 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module defines an enum for specifying the type of decision rule when a +decision rule is used. +""" import enum class DecisionRuleTypes(enum.Enum): + """ + An enum to specify the type of decision rule to use. + """ + linear = enum.auto() relu_nn = enum.auto() diff --git a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py index d57d232e38..519bf336e1 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/linear_dr.py @@ -10,13 +10,17 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# -import pyomo.environ as pe +""" +This module contains a function for constructing a linear decision rule for +the inner problem of the flexibility test. +""" from typing import MutableMapping, Sequence -from pyomo.core.base.var import _GeneralVarData +import pyomo.environ as pe +from pyomo.core.base.var import _GeneralVarData, ScalarVar, IndexedVar from pyomo.core.base.block import _BlockData from pyomo.core.expr.numeric_expr import LinearExpression -from .dr_config import DRConfig from pyomo.common.config import ConfigValue +from .dr_config import DRConfig from ..check_optimal import assert_optimal_termination @@ -56,6 +60,24 @@ def construct_linear_decision_rule( output_vals: MutableMapping[_GeneralVarData, Sequence[float]], config: LinearDRConfig, ) -> _BlockData: + """ + Construct a linear decision rule from the data provided for the inputs + and outputs. + + Parameters + ---------- + input_vals: input_vals: MutableMapping[_GeneralVarData, Sequence[float]] + Data for the variables that are inputs to the decision rule + output_vals: input_vals: MutableMapping[_GeneralVarData, Sequence[float]] + Data for the variables that are outputs to the decision rule + config: LinearDRConfig + A config object to specify options for the decision rule + + Returns + ------- + res: _BlockData + A pyomo model containing the linear decision rule + """ n_inputs = len(input_vals) n_outputs = len(output_vals) @@ -70,9 +92,9 @@ def construct_linear_decision_rule( trainer.input_set = pe.Set(initialize=list(range(n_inputs))) trainer.sample_set = pe.Set(initialize=list(range(n_samples))) - trainer.const = pe.Var() - trainer.coefs = pe.Var(trainer.input_set) - trainer.out_est = pe.Var(trainer.sample_set) + trainer.const = ScalarVar() + trainer.coefs = IndexedVar(trainer.input_set) + trainer.out_est = IndexedVar(trainer.sample_set) obj_expr = sum( (trainer.out_est[i] - out_samples[i]) ** 2 for i in trainer.sample_set diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py index 3a8f814245..c27d988815 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr.py @@ -10,12 +10,16 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +This module contains a function for constructing a neural network-based decision +rule for the inner problem of the flexibility test. +""" +from typing import MutableMapping, Sequence import numpy as np from pyomo.common.dependencies import attempt_import import pyomo.environ as pe from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.block import _BlockData -from typing import MutableMapping, Sequence from idaes.apps.flexibility_analysis.indices import _VarIndex from .relu_dr_config import ReluDRConfig @@ -24,6 +28,7 @@ omlt, omlt_available = attempt_import("omlt") omlt_nn, _ = attempt_import("omlt.neuralnet") omlt_io, _ = attempt_import("omlt.io") +plt, _ = attempt_import("matplotlib.pyplot") def construct_relu_decision_rule( @@ -31,6 +36,24 @@ def construct_relu_decision_rule( output_vals: MutableMapping[_GeneralVarData, Sequence[float]], config: ReluDRConfig, ) -> _BlockData: + """ + Construct a neural network-based decision rule with ReLU activation functions + from the data provided for the inputs and outputs. + + Parameters + ---------- + input_vals: input_vals: MutableMapping[_GeneralVarData, Sequence[float]] + Data for the variables that are inputs to the decision rule + output_vals: input_vals: MutableMapping[_GeneralVarData, Sequence[float]] + Data for the variables that are outputs to the decision rule + config: ReluDRConfig + A config object to specify options for the decision rule + + Returns + ------- + res: _BlockData + A pyomo model containing the linear decision rule + """ tf.random.set_seed(config.tensorflow_seed) inputs = list(input_vals.keys()) outputs = list(output_vals.keys()) @@ -76,7 +99,7 @@ def construct_relu_decision_rule( units=config.n_nodes_per_layer, input_dim=len(inputs), activation="relu" ) ) - for layer_ndx in range(config.n_layers - 1): + for _ in range(config.n_layers - 1): nn.add(keras.layers.Dense(config.n_nodes_per_layer, activation="relu")) nn.add(keras.layers.Dense(len(outputs))) if config.learning_rate is None: @@ -93,8 +116,6 @@ def construct_relu_decision_rule( ) if config.plot_history: - import matplotlib.pyplot as plt - plt.scatter(history.epoch, history.history["loss"]) plt.xlabel("Epoch") plt.ylabel("Loss") @@ -126,14 +147,14 @@ def construct_relu_decision_rule( res.input_links = pe.Constraint(res.input_set) for ndx, v in enumerate(inputs): key = _VarIndex(v, None) - res.input_set.add(key) + res.input_set.add(key) # pylint: disable=no-member res.input_links[key] = v == res.nn.inputs[ndx] res.output_set = pe.Set() res.output_links = pe.Constraint(res.output_set) for ndx, v in enumerate(outputs): key = _VarIndex(v, None) - res.output_set.add(key) + res.output_set.add(key) # pylint: disable=no-member res.output_links[key] = v == res.nn.outputs[ndx] return res diff --git a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py index 0f6dc1a9f8..546e689969 100644 --- a/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py +++ b/idaes/apps/flexibility_analysis/decision_rules/relu_dr_config.py @@ -10,8 +10,12 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# -from .dr_config import DRConfig +""" +This module defines a config for specifying options related to neural network-based +decision rules +""" from pyomo.common.config import ConfigValue +from .dr_config import DRConfig class ReluDRConfig(DRConfig): diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index d732333409..652a6f8b04 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -10,11 +10,18 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +""" +A flexibility analysis example from + + Grossmann, I. E., & Floudas, C. A. (1987). Active constraint strategy for + flexibility analysis in chemical processes. Computers & Chemical Engineering, + 11(6), 675-693. +""" +from typing import Tuple, MutableMapping import pyomo.environ as pe from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData import idaes.apps.flexibility_analysis as flexibility -from typing import Tuple, MutableMapping def create_model() -> Tuple[ @@ -57,12 +64,24 @@ def create_model() -> Tuple[ def get_var_bounds(m): + """ + Generate a map with valid variable bounds for + any possible realization of the uncertain parameters + """ res = pe.ComponentMap() res[m.qc] = (-1000, 1000) return res def main(method): + """ + Run the example + + Parameters + ---------- + method: flexibility.FlexTestMethod + The method to use for the flexibility test + """ m, nominal_values, param_bounds = create_model() var_bounds = get_var_bounds(m) config = flexibility.FlexTestConfig() From a6aa18cd3b60fbdb4586a17996687c1d8c12d3da Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 14:28:54 -0600 Subject: [PATCH 50/60] bug fix --- idaes/apps/flexibility_analysis/uncertain_params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idaes/apps/flexibility_analysis/uncertain_params.py b/idaes/apps/flexibility_analysis/uncertain_params.py index e5540175e1..fb4e44cc1e 100644 --- a/idaes/apps/flexibility_analysis/uncertain_params.py +++ b/idaes/apps/flexibility_analysis/uncertain_params.py @@ -30,11 +30,11 @@ def _replace_uncertain_params( param_bounds: Mapping[Union[_GeneralVarData, _ParamData], Sequence[float]], ) -> _BlockData: for v in get_all_unfixed_variables(m): - if not pe.is_constant(v.lower): + if not pe.is_constant(v._lb): # pylint: disable=protected-access raise ValueError( f"The lower bound on {str(v)} is not constant. All variable bounds must be constant." ) - if not pe.is_constant(v.upper): + if not pe.is_constant(v._ub): # pylint: disable=protected-access raise ValueError( f"The upper bound on {str(v)} is not constant. All variable bounds must be constant." ) From 3abd59d87930695a6cc3b1bd6e9cc6a5add8e51b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 14:30:26 -0600 Subject: [PATCH 51/60] debugging ci --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index e46ff32c77..8f3e368533 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -130,7 +130,7 @@ jobs: || needs.precheck.outputs.workflow-trigger == 'approved_pr' ) strategy: - fail-fast: false + fail-fast: true matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: From fce9af413b0d69ea3cbd4ccbd6d0ebda1701f355 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 14:32:39 -0600 Subject: [PATCH 52/60] debugging ci --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 8f3e368533..f038d48659 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -154,7 +154,7 @@ jobs: with: install-target: -r requirements-dev.txt - name: Run pytest (integration) - run: pytest --pyargs idaes -m integration + run: pytest --pyargs idaes -x -m integration examples: name: Run examples (py${{ matrix.python-version }}/${{ matrix.os }}) From 79d4b6a03d4ef3feff03ade6070f8c079942e7fc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 14:35:41 -0600 Subject: [PATCH 53/60] revert some changes --- .github/workflows/integration.yml | 4 ++-- .../apps/flexibility_analysis/examples/tests/test_examples.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index f038d48659..e46ff32c77 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -130,7 +130,7 @@ jobs: || needs.precheck.outputs.workflow-trigger == 'approved_pr' ) strategy: - fail-fast: true + fail-fast: false matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: @@ -154,7 +154,7 @@ jobs: with: install-target: -r requirements-dev.txt - name: Run pytest (integration) - run: pytest --pyargs idaes -x -m integration + run: pytest --pyargs idaes -m integration examples: name: Run examples (py${{ matrix.python-version }}/${{ matrix.os }}) diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index 78f4a97662..c4afa01519 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -23,7 +23,7 @@ import pytest -@pytest.mark.integration +@pytest.mark.component class TestExamples(unittest.TestCase): def test_linear_hx_network(self): res = linear_hx_network.main( From 7f51740d471fefc5c851db6a23621701f08b499e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 14:51:28 -0600 Subject: [PATCH 54/60] speed up flexibility tests --- .../examples/linear_hx_network.py | 3 ++- .../examples/tests/test_examples.py | 13 +++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py index 0247b0b433..e0263ebcfc 100644 --- a/idaes/apps/flexibility_analysis/examples/linear_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/linear_hx_network.py @@ -99,6 +99,7 @@ def main( flex_index: bool = False, method: flexibility.FlexTestMethod = flexibility.FlexTestMethod.active_constraint, plot_history=True, + relu_epochs=3000, ): """ Run the example @@ -134,7 +135,7 @@ def main( config.decision_rule_config = flexibility.ReluDRConfig() config.decision_rule_config.n_layers = 1 config.decision_rule_config.n_nodes_per_layer = 15 - config.decision_rule_config.epochs = 3000 + config.decision_rule_config.epochs = relu_epochs config.decision_rule_config.batch_size = 150 config.decision_rule_config.scale_inputs = True config.decision_rule_config.scale_outputs = True diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index c4afa01519..8ce0da50f5 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -55,15 +55,6 @@ def test_linear_hx_network(self): res.termination, flex.FlexTestTermination.found_infeasible_point ) - res = linear_hx_network.main( - flex_index=False, - method=flex.FlexTestMethod.relu_decision_rule, - plot_history=False, - ) - self.assertEqual( - res.termination, flex.FlexTestTermination.found_infeasible_point - ) - res = linear_hx_network.main( flex_index=True, method=flex.FlexTestMethod.active_constraint, @@ -89,8 +80,10 @@ def test_linear_hx_network(self): flex_index=True, method=flex.FlexTestMethod.relu_decision_rule, plot_history=False, + relu_epochs=100, ) - self.assertAlmostEqual(res, 0.5, 2) + self.assertGreaterEqual(res, 0.1) + self.assertLessEqual(res, 0.5) def test_idaes_hx_network(self): res = idaes_hx_network.main(flex.FlexTestMethod.sampling) From 600426068b8f80e7772bdbba6a80e6a614b36da2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 14:57:32 -0600 Subject: [PATCH 55/60] fix tests --- .../examples/tests/test_examples.py | 141 ++++++++++-------- 1 file changed, 80 insertions(+), 61 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index 8ce0da50f5..badd6752f6 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -10,6 +10,10 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +from contextlib import contextmanager +import unittest +import pytest +import pyomo.environ as pe from idaes.apps.flexibility_analysis import ( _check_dependencies, _check_relu_dependencies, @@ -19,77 +23,92 @@ linear_hx_network, idaes_hx_network, ) -import unittest -import pytest +from idaes.core.util.testing import _enable_scip_solver_for_testing + + +@contextmanager +def scip_solver(): + solver = pe.SolverFactory("scip") + undo_changes = None + + if not solver.available(): + undo_changes = _enable_scip_solver_for_testing() + if not solver.available(): + pytest.skip(reason="SCIP solver not available") + yield solver + if undo_changes is not None: + undo_changes() @pytest.mark.component class TestExamples(unittest.TestCase): def test_linear_hx_network(self): - res = linear_hx_network.main( - flex_index=False, - method=flex.FlexTestMethod.active_constraint, - plot_history=False, - ) - self.assertEqual( - res.termination, flex.FlexTestTermination.found_infeasible_point - ) - self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) + with scip_solver(): + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.active_constraint, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) + self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) - res = linear_hx_network.main( - flex_index=False, - method=flex.FlexTestMethod.vertex_enumeration, - plot_history=False, - ) - self.assertEqual( - res.termination, flex.FlexTestTermination.found_infeasible_point - ) - self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.vertex_enumeration, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) + self.assertAlmostEqual(res.max_constraint_violation, 8.8, 5) - res = linear_hx_network.main( - flex_index=False, - method=flex.FlexTestMethod.linear_decision_rule, - plot_history=False, - ) - self.assertEqual( - res.termination, flex.FlexTestTermination.found_infeasible_point - ) + res = linear_hx_network.main( + flex_index=False, + method=flex.FlexTestMethod.linear_decision_rule, + plot_history=False, + ) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) - res = linear_hx_network.main( - flex_index=True, - method=flex.FlexTestMethod.active_constraint, - plot_history=False, - ) - self.assertAlmostEqual(res, 0.5, 5) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.active_constraint, + plot_history=False, + ) + self.assertAlmostEqual(res, 0.5, 5) - res = linear_hx_network.main( - flex_index=True, - method=flex.FlexTestMethod.vertex_enumeration, - plot_history=False, - ) - self.assertAlmostEqual(res, 0.5, 5) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.vertex_enumeration, + plot_history=False, + ) + self.assertAlmostEqual(res, 0.5, 5) - res = linear_hx_network.main( - flex_index=True, - method=flex.FlexTestMethod.linear_decision_rule, - plot_history=False, - ) - self.assertLessEqual(res, 0.5) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.linear_decision_rule, + plot_history=False, + ) + self.assertLessEqual(res, 0.5) - res = linear_hx_network.main( - flex_index=True, - method=flex.FlexTestMethod.relu_decision_rule, - plot_history=False, - relu_epochs=100, - ) - self.assertGreaterEqual(res, 0.1) - self.assertLessEqual(res, 0.5) + res = linear_hx_network.main( + flex_index=True, + method=flex.FlexTestMethod.relu_decision_rule, + plot_history=False, + relu_epochs=100, + ) + self.assertGreaterEqual(res, 0.1) + self.assertLessEqual(res, 0.5) def test_idaes_hx_network(self): - res = idaes_hx_network.main(flex.FlexTestMethod.sampling) - self.assertEqual( - res.termination, flex.FlexTestTermination.found_infeasible_point - ) - self.assertAlmostEqual( - res.max_constraint_violation, 0.375170890924453 - ) # regression + with scip_solver(): + res = idaes_hx_network.main(flex.FlexTestMethod.sampling) + self.assertEqual( + res.termination, flex.FlexTestTermination.found_infeasible_point + ) + self.assertAlmostEqual( + res.max_constraint_violation, 0.375170890924453 + ) # regression From 8cdbde91a8f6eb2400170705956765d81c6b0c16 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 15:02:41 -0600 Subject: [PATCH 56/60] debugging ci --- .github/workflows/core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 509f962829..701efe377d 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -119,7 +119,7 @@ jobs: echo PYTEST_ADDOPTS="$PYTEST_ADDOPTS --cov --cov-report=xml" >> "$GITHUB_ENV" - name: Run pytest (not integration) run: | - pytest --pyargs idaes -m "not integration" + pytest --pyargs idaes -x -m "not integration" - name: Upload coverage report as GHA workflow artifact if: matrix.cov-report uses: actions/upload-artifact@v4 From 4a78054329050ef32759700febf55092a7a5aa1e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 15:10:26 -0600 Subject: [PATCH 57/60] fix tests --- idaes/apps/flexibility_analysis/examples/idaes_hx_network.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py index 2c3bcdfed5..f998aeace3 100644 --- a/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/idaes_hx_network.py @@ -347,8 +347,7 @@ def main(method: flexibility.FlexTestMethod): config.method = method config.minlp_solver = pe.SolverFactory("scip") config.minlp_solver.options["limits/time"] = 300 - config.sampling_config.solver = pe.SolverFactory("appsi_ipopt") - config.sampling_config.solver.config.log_level = logging.DEBUG + config.sampling_config.solver = pe.SolverFactory("ipopt") config.sampling_config.strategy = flexibility.SamplingStrategy.grid config.sampling_config.num_points = 3 if method == flexibility.FlexTestMethod.linear_decision_rule: From 013fa662c029a4403135876d9a8b4b970e58852b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 15:15:35 -0600 Subject: [PATCH 58/60] revert some changes --- .github/workflows/core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 701efe377d..509f962829 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -119,7 +119,7 @@ jobs: echo PYTEST_ADDOPTS="$PYTEST_ADDOPTS --cov --cov-report=xml" >> "$GITHUB_ENV" - name: Run pytest (not integration) run: | - pytest --pyargs idaes -x -m "not integration" + pytest --pyargs idaes -m "not integration" - name: Upload coverage report as GHA workflow artifact if: matrix.cov-report uses: actions/upload-artifact@v4 From 6743fb16ab9d3f7b1544763ab475cc4df255209d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Apr 2024 16:14:40 -0600 Subject: [PATCH 59/60] fix docs --- .../modeling_extensions/flexibility_analysis/overview.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst index 961c2ad913..d6be478912 100644 --- a/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst +++ b/docs/explanations/modeling_extensions/flexibility_analysis/overview.rst @@ -131,7 +131,7 @@ functions. The first is :meth:`solve_flextest`. The :class:`FlexTestConfig` specifies how the flexibility test should be solved. The second is -:meth:`solve_flextest`. Examples +:meth:`solve_flex_index`. Examples can be found `here `_. From 7cb6ee8f164f90bfc67a693ff13a3b5beec2dd88 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 25 Apr 2024 07:32:04 -0600 Subject: [PATCH 60/60] more flexibility tests --- .../flexibility_analysis/examples/nonlin_hx_network.py | 1 + .../flexibility_analysis/examples/tests/test_examples.py | 8 ++++++++ idaes/apps/flexibility_analysis/flextest.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py index 652a6f8b04..40a044d23e 100644 --- a/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py +++ b/idaes/apps/flexibility_analysis/examples/nonlin_hx_network.py @@ -117,6 +117,7 @@ def main(method): config=config, ) print(results) + return results if __name__ == "__main__": diff --git a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py index badd6752f6..53d7355ebf 100644 --- a/idaes/apps/flexibility_analysis/examples/tests/test_examples.py +++ b/idaes/apps/flexibility_analysis/examples/tests/test_examples.py @@ -22,6 +22,7 @@ from idaes.apps.flexibility_analysis.examples import ( linear_hx_network, idaes_hx_network, + nonlin_hx_network, ) from idaes.core.util.testing import _enable_scip_solver_for_testing @@ -42,6 +43,13 @@ def scip_solver(): @pytest.mark.component class TestExamples(unittest.TestCase): + def test_nonlin_hx_network(self): + with scip_solver(): + res = nonlin_hx_network.main( + method=flex.FlexTestMethod.active_constraint, + ) + self.assertAlmostEqual(res, 0.1474609375, 3) + def test_linear_hx_network(self): with scip_solver(): res = linear_hx_network.main( diff --git a/idaes/apps/flexibility_analysis/flextest.py b/idaes/apps/flexibility_analysis/flextest.py index 9ee6d89e37..d599ee7e44 100644 --- a/idaes/apps/flexibility_analysis/flextest.py +++ b/idaes/apps/flexibility_analysis/flextest.py @@ -557,7 +557,7 @@ def _solve_flextest_decision_rule( config=config, ) opt = config.minlp_solver - res = opt.solve(m, tee=True) + res = opt.solve(m) pe.assert_optimal_termination(res) results = FlexTestResults()