diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index f701677d47..ab401f29aa 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -100,6 +100,13 @@ deactivated_objectives_set, variables_in_activated_constraints_set, variables_not_in_activated_constraints_set, + number_activated_greybox_equalities, + number_deactivated_greybox_equalities, + activated_greybox_block_set, + deactivated_greybox_block_set, + greybox_block_set, + unfixed_greybox_variables, + greybox_variables, degrees_of_freedom, large_residuals_set, variables_near_bounds_set, @@ -486,7 +493,10 @@ def __init__(self, model: BlockData, **kwargs): "model argument must be an instance of a Pyomo BlockData object " "(either a scalar Block or an element of an indexed Block)." ) - + if len(greybox_block_set(model)) != 0: + raise NotImplementedError( + "Model contains Greybox models, which are not supported by Diagnostics toolbox at the moment" + ) self._model = model self.config = CONFIG(kwargs) @@ -1746,7 +1756,12 @@ def report_structural_issues(self, stream=None): # Potential evaluation errors # TODO: High Index? + if len(greybox_block_set(self._model)) != 0: + raise NotImplementedError( + "Model contains Greybox models, which are not supported by Diagnostics toolbox at the moment" + ) stats = _collect_model_statistics(self._model) + warnings, next_steps = self._collect_structural_warnings() cautions = self._collect_structural_cautions() @@ -1790,7 +1805,6 @@ def report_numerical_issues(self, stream=None): """ if stream is None: stream = sys.stdout - jac, nlp = get_jacobian(self._model, scaled=False) warnings, next_steps = self._collect_numerical_warnings(jac=jac, nlp=nlp) @@ -1935,7 +1949,10 @@ def __init__(self, model: BlockData, **kwargs): "model argument must be an instance of a Pyomo BlockData object " "(either a scalar Block or an element of an indexed Block)." ) - + if len(greybox_block_set(model)) != 0: + raise NotImplementedError( + "Model contains Greybox models, which are not supported by Diagnostics toolbox at the moment" + ) self._model = model self.config = SVDCONFIG(kwargs) @@ -2377,7 +2394,10 @@ def __init__(self, model, **kwargs): "model argument must be an instance of a Pyomo BlockData object " "(either a scalar Block or an element of an indexed Block)." ) - + if len(greybox_block_set(model)) != 0: + raise NotImplementedError( + "Model contains Greybox models, which are not supported by Diagnostics toolbox at the moment" + ) self._model = model self.config = DHCONFIG(kwargs) @@ -3475,7 +3495,10 @@ def __init__(self, model, **kwargs): "model argument must be an instance of a Pyomo BlockData object " "(either a scalar Block or an element of an indexed Block)." ) - + if len(greybox_block_set(model)) != 0: + raise NotImplementedError( + "Model contains Greybox models, which are not supported by Diagnostics toolbox at the moment" + ) self.config = self.CONFIG(kwargs) self._model = model @@ -4402,8 +4425,8 @@ def _collect_model_statistics(model): f"(External: {len(ext_fixed_vars_in_constraints)})" ) stats.append( - f"{TAB}Activated Equality Constraints: {len(activated_equalities_set(model))} " - f"(Deactivated: {len(deactivated_equalities_set(model))})" + f"{TAB}Activated Equality Constraints: {len(activated_equalities_set(model))+number_activated_greybox_equalities(model)} " + f"(Deactivated: {len(deactivated_equalities_set(model))+number_deactivated_greybox_equalities(model)})" ) stats.append( f"{TAB}Activated Inequality Constraints: {len(activated_inequalities_set(model))} " @@ -4414,6 +4437,21 @@ def _collect_model_statistics(model): f"(Deactivated: {len(deactivated_objectives_set(model))})" ) + # Only show graybox info if they are present + if len(greybox_block_set(model)) != 0: + stats.append(f"{TAB}GreyBox Statistics") + stats.append( + f"{TAB* 2}Activated GreyBox models: {len(activated_greybox_block_set(model))} " + f"(Deactivated: {len(deactivated_greybox_block_set(model))})" + ) + stats.append( + f"{TAB* 2}Activated GreyBox Equalities: {number_activated_greybox_equalities(model)} " + f"(Deactivated: {number_deactivated_greybox_equalities(model)})" + ) + stats.append( + f"{TAB* 2}Free Variables in Activated GreyBox Equalities: {len(unfixed_greybox_variables(model))} (Fixed: {len(greybox_variables(model)-unfixed_greybox_variables(model))})" + ) + return stats diff --git a/idaes/core/util/model_statistics.py b/idaes/core/util/model_statistics.py index d237be3a52..97a269cad5 100644 --- a/idaes/core/util/model_statistics.py +++ b/idaes/core/util/model_statistics.py @@ -25,6 +25,7 @@ from pyomo.core.expr import identify_variables from pyomo.common.collections import ComponentSet from pyomo.common.deprecation import deprecation_warning +from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock import idaes.logger as idaeslog @@ -115,6 +116,104 @@ def activated_blocks_set(block): return block_set +def greybox_block_set(block): + """ + Function to return ComponentSet of all Greybox Blocks components in a + model. + + Args: + block : model to be studied + + Returns: + A ComponentSet including all GreyBox Block components in block + (including block itself) + """ + block_set = ComponentSet() + for grey_box in activated_block_component_generator( + block, ctype=ExternalGreyBoxBlock + ): + block_set.add(grey_box) + + return block_set + + +def activated_greybox_block_set(block): + """ + Function to return ComponentSet of activated Greybox Blocks components in a + model. + + Args: + block : model to be studied + + Returns: + A ComponentSet including all GreyBox Block components in block + (including block itself) + """ + block_set = ComponentSet() + for grey_box in greybox_block_set(block): + if grey_box.active: + block_set.add(grey_box) + + return block_set + + +def deactivated_greybox_block_set(block): + """ + Function to return ComponentSet of deactivated Greybox Blocks components in a + model. + + Args: + block : model to be studied + + Returns: + A ComponentSet including all GreyBox Block components in block + (including block itself) + """ + return greybox_block_set(block) - activated_greybox_block_set(block) + + +def number_deactivated_greybox_block(block): + """ + Function to return a Number of deactivated Greybox Blocks components in a + model. + + Args: + block : model to be studied + + Returns: + number of deactivated greybox blocks + """ + return len(deactivated_greybox_block_set(block)) + + +def number_greybox_blocks(block): + """ + Function to return a Number of activated Greybox Blocks components in a + model. + + Args: + block : model to be studied + + Returns: + number of activated greybox blocks + """ + return len(greybox_block_set(block)) + + +def number_activated_greybox_blocks(block): + """ + Function to return a Number of activated Greybox Blocks components in a + model. + + Args: + block : model to be studied + + Returns: + number of activated greybox blocks + """ + return len(activated_greybox_block_set(block)) + + def number_activated_blocks(block): """ Method to return the number of activated Block components in a model. @@ -188,6 +287,8 @@ def total_constraints_set(block): def number_total_constraints(block): """ Method to return the total number of Constraint components in a model. + This will include the number of constraints provided by Greybox models using + the number_activated_greybox_equalities function. Args: block : model to be studied @@ -195,7 +296,11 @@ def number_total_constraints(block): Returns: Number of Constraint components in block """ - return sum(1 for _ in activated_block_component_generator(block, ctype=Constraint)) + number_standard_constraints = sum( + 1 for _ in activated_block_component_generator(block, ctype=Constraint) + ) + number_greybox_constraints = number_activated_greybox_equalities(block) + return number_standard_constraints + number_greybox_constraints def activated_constraints_generator(block): @@ -272,7 +377,8 @@ def deactivated_constraints_set(block): def number_deactivated_constraints(block): """ Method to return the number of deactivated Constraint components in a - model. + model. This will include number of deactivated equalities in a Greybox models + using number_deactivated_greybox_equalities function. Args: block : model to be studied @@ -280,7 +386,9 @@ def number_deactivated_constraints(block): Returns: Number of deactivated Constraint components in block """ - return sum(1 for _ in deactivated_constraints_generator(block)) + standard_equalities = sum(1 for _ in deactivated_constraints_generator(block)) + greybox_equalities = number_deactivated_greybox_equalities(block) + return standard_equalities + greybox_equalities # ------------------------------------------------------------------------- @@ -317,7 +425,7 @@ def total_equalities_set(block): def number_total_equalities(block): """ Method to return the total number of equality Constraint components in a - model. + model. This will include the number of activated equalities Greybox using the number_activated_greybox_equalities function. Args: block : model to be studied @@ -325,7 +433,9 @@ def number_total_equalities(block): Returns: Number of equality Constraint components in block """ - return sum(1 for _ in total_equalities_generator(block)) + standard_equalities = sum(1 for _ in total_equalities_generator(block)) + greybox_equalities = number_activated_greybox_equalities(block) + return standard_equalities + greybox_equalities def activated_equalities_generator(block): @@ -369,7 +479,7 @@ def activated_equalities_set(block): def number_activated_equalities(block): """ Method to return the number of activated equality Constraint components in - a model. + a model. This will include number of equalities in Greybox model using number_activated_greybox_equalities function. Args: block : model to be studied @@ -377,7 +487,53 @@ def number_activated_equalities(block): Returns: Number of activated equality Constraint components in block """ - return sum(1 for _ in activated_equalities_generator(block)) + return sum( + 1 for _ in activated_equalities_generator(block) + ) + number_activated_greybox_equalities(block) + + +def number_activated_greybox_equalities(block) -> int: + """ + Function to compute total number of equality constraints for all GreyBox objects in this block. + + A GreyBox model is always assumed to be 0DOFs where each output[i]==f(inputs) + where f is GreyBox model, this should be true regardless if + GreyBox model is doing internal optimization or not, as every output + is calculated through the GreyBox internal model using provided inputs. + + Args: + block : pyomo concrete model or pyomo block + + Returns: + Number of equality constraints in all GreyBox objects on the provided block + """ + equalities = 0 + for grey_box in activated_greybox_block_set(block): + equalities += len(grey_box.outputs) + equalities += grey_box.get_external_model().n_equality_constraints() + return equalities + + +def number_deactivated_greybox_equalities(block) -> int: + """ + Function to compute total number of equality constraints for all GreyBox objects in this block. + + A GreyBox model is always assumed to be 0DOFs where each output[i]==f(inputs) + where f is GreyBox model, this should be true regardless if + GreyBox model is doing internal optimization or not, as every output + is calculated through a the GreyBox internal model using provided inputs. + + Args: + block : pyomo concrete model or pyomo block + + Returns: + Number of equality constraints in all GreyBox objects on the provided block + """ + equalities = 0 + for grey_box in deactivated_greybox_block_set(block): + equalities += len(grey_box.outputs) + equalities += grey_box.get_external_model().n_equality_constraints() + return equalities def deactivated_equalities_generator(block): @@ -415,7 +571,7 @@ def deactivated_equalities_set(block): def number_deactivated_equalities(block): """ Method to return the number of deactivated equality Constraint components - in a model. + in a model. This will include the number of deactivated equality constraints in Greybox models. Args: block : model to be studied @@ -423,7 +579,9 @@ def number_deactivated_equalities(block): Returns: Number of deactivated equality Constraint components in block """ - return sum(1 for _ in deactivated_equalities_generator(block)) + standard_equalities = sum(1 for _ in deactivated_equalities_generator(block)) + greybox_equalities = number_deactivated_greybox_equalities(block) + return standard_equalities + greybox_equalities # ------------------------------------------------------------------------- @@ -529,7 +687,7 @@ def deactivated_inequalities_generator(block): block : model to be studied Returns: - A generator which returns all indeactivated equality Constraint + A generator which returns all deactivated equality Constraint components block """ for c in total_inequalities_generator(block): @@ -580,11 +738,14 @@ def variables_set(block): Returns: A ComponentSet including all Var components in block """ - return ComponentSet( - _iter_indexed_block_data_objects( - block, ctype=Var, active=True, descend_into=True - ) - ) + var_set = ComponentSet() + for var in _iter_indexed_block_data_objects( + block, ctype=Var, active=True, descend_into=True + ): + var_set.add(var) + for var in greybox_variables(block): + var_set.add(var) + return var_set def number_variables(block): @@ -615,6 +776,10 @@ def fixed_variables_generator(block): ): if v.fixed: yield v + # include greybox variables in set + for v in greybox_variables(block): + if v.fixed: + yield v def fixed_variables_set(block): @@ -825,6 +990,9 @@ def variables_in_activated_constraints_set(block): ): for v in identify_variables(c.body): var_set.add(v) + # include any vars in greyboxes + for v in greybox_variables(block): + var_set.add(v) return var_set @@ -898,6 +1066,9 @@ def variables_in_activated_equalities_set(block): for c in activated_equalities_generator(block): for v in identify_variables(c.body): var_set.add(v) + # include any vars in greyboxes + for v in greybox_variables(block): + var_set.add(v) return var_set @@ -1027,7 +1198,7 @@ def unfixed_variables_in_activated_equalities_set(block): block : model to be studied Returns: - A ComponentSet including all unfixed Var components which appear within + A ComponentSet of all unfixed Var components which appear within activated equality Constraints in block """ var_set = ComponentSet() @@ -1037,6 +1208,69 @@ def unfixed_variables_in_activated_equalities_set(block): return var_set +def unfixed_greybox_variables(block): + """ + Function to return a ComponentSet of all unfixed Var in GreyBoxModels + + Args: + block : model to be studied + + Returns: + A ComponentSet of all unfixed Var components which appear in Greybox models + """ + var_set = ComponentSet() + for var in greybox_variables(block): + if not var.fixed: + var_set.add(var) + return var_set + + +def greybox_variables(block): + """ + Function to return a ComponentSet of all Var in GreyBoxModels + + Args: + block : model to be studied + + Returns: + A ComponentSet of all Var components which appear within + activated Greybox model blocks + """ + var_set = ComponentSet() + for grey_box in activated_greybox_block_set(block): + for in_var in grey_box.inputs: + var_set.add(grey_box.inputs[in_var]) + for out_var in grey_box.outputs: + var_set.add(grey_box.outputs[out_var]) + return var_set + + +def number_of_unfixed_greybox_variables(block): + """ + Function to return a number of unfixed variables in grey box + Args: + block : model to be studied + + Returns: + number of unfixed greybox variables + """ + + return len(unfixed_greybox_variables(block)) + + +def number_of_greybox_variables(block): + """ + Function to return a number of variables in grey box + Args: + block : model to be studied + + Returns: + number of greybox variables + """ + + return len(greybox_variables(block)) + + def number_unfixed_variables_in_activated_equalities(block): """ Method to return the number of unfixed Var components which appear within @@ -1112,7 +1346,7 @@ def number_unused_variables(block): block : model to be studied Returns: - Number of Var components which do not appear within any activagted + Number of Var components which do not appear within any activated Constraints in block """ return len(unused_variables_set(block)) @@ -1574,6 +1808,17 @@ def report_statistics(block, ostream=None): f"{number_deactivated_blocks(block)}) \n" ) ostream.write(f"No. Expressions: " f"{number_expressions(block)} \n") + if number_activated_greybox_blocks(block) != 0: + ostream.write( + f"No. Activated GreyBox Blocks: {number_activated_greybox_blocks(block)} \n" + ) + ostream.write(f"No. GreyBox Variables: {number_of_greybox_variables(block)} \n") + ostream.write( + f"No. Fixed GreyBox Variables: {number_of_greybox_variables(block)-number_of_unfixed_greybox_variables(block)} \n" + ) + ostream.write( + f"No. GreyBox Equalities: {number_activated_greybox_equalities(block)} \n" + ) ostream.write(header + "\n") ostream.write("\n") diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index e10f8a7c57..d5d4c21d60 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -17,6 +17,11 @@ from io import StringIO import math import os +from copy import deepcopy +from pyomo.contrib.pynumero.interfaces.external_grey_box import ( + ExternalGreyBoxBlock, + ExternalGreyBoxModel, +) import re from unittest import TestCase @@ -468,6 +473,70 @@ def test_fixed_variables(self): assert stats[7] == " Activated Inequality Constraints: 0 (Deactivated: 0)" assert stats[8] == " Activated Objectives: 0 (Deactivated: 0)" + def test_with_greybox_variables(self): + """non functional graybox model added to m fixture, to test DOFs + + GreyBoxModel has 3 inputs and 2 outputs calculated an unknown function, + input a1 and a2 are bound by equality constraint through internal graybox model + """ + + class BasicGrayBox(ExternalGreyBoxModel): + def input_names(self): + return ["a1", "a2", "a3"] + + def output_names(self): + return ["o1", "o2"] + + def equality_constraint_names(self): + return ["a_sum"] + + def evaluate_equality_constraints(self): + a1 = self._input_values[0] + a2 = self._input_values[1] + return [a1 * 0.5 + a2] + + m = ConcreteModel() + + m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + m.gb_inactive = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + m.gb_inactive.deactivate() + m.a1 = Var(initialize=1) + m.a1.fix() + m.gb.inputs["a2"].unfix() + m.gb.inputs["a3"].fix() + m.a1_eq = Constraint(expr=m.a1 == m.gb.inputs["a1"]) + m.o1 = Var(initialize=1) + m.o1_eq = Constraint(expr=m.o1 == m.gb.outputs["o1"]) + m.o1.fix() + stats = _collect_model_statistics(m) + for k in stats: + print(k) + print(stats, len(stats)) + m.display() + tab = " " * 4 + assert len(stats) == 13 + assert stats[0] == f"{tab}Activated Blocks: 1 (Deactivated: 0)" + assert ( + stats[1] == f"{tab}Free Variables in Activated Constraints: 4 (External: 0)" + ) + assert stats[2] == f"{tab*2}Free Variables with only lower bounds: 0" + assert stats[3] == f"{tab*2}Free Variables with only upper bounds: 0" + assert stats[4] == f"{tab*2}Free Variables with upper and lower bounds: 0" + assert ( + stats[5] + == f"{tab}Fixed Variables in Activated Constraints: 3 (External: 0)" + ) + assert stats[6] == f"{tab}Activated Equality Constraints: 5 (Deactivated: 3)" + assert stats[7] == f"{tab}Activated Inequality Constraints: 0 (Deactivated: 0)" + assert stats[8] == f"{tab}Activated Objectives: 0 (Deactivated: 0)" + assert stats[9] == f"{tab}GreyBox Statistics" + assert stats[10] == f"{tab*2}Activated GreyBox models: 1 (Deactivated: 1)" + assert stats[11] == f"{tab*2}Activated GreyBox Equalities: 3 (Deactivated: 3)" + assert ( + stats[12] + == f"{tab*2}Free Variables in Activated GreyBox Equalities: 4 (Fixed: 1)" + ) + @pytest.mark.solver class TestDiagnosticsToolbox: @@ -527,6 +596,30 @@ def model(self): return m + @pytest.mark.component + def test_with_grey_box(self): + + class BasicGrayBox(ExternalGreyBoxModel): + def input_names(self): + return ["a1", "a2", "a3"] + + def output_names(self): + return ["o1", "o2"] + + def equality_constraint_names(self): + return ["a_sum"] + + def evaluate_equality_constraints(self): + a1 = self._input_values[0] + a2 = self._input_values[1] + return [a1 * 0.5 + a2] + + m = ConcreteModel() + + m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + with pytest.raises(NotImplementedError): + DiagnosticsToolbox(model=m) + @pytest.mark.component def test_display_external_variables(self, model): dt = DiagnosticsToolbox(model=model.b) @@ -1842,6 +1935,30 @@ def test_svd_callback_domain(self, dummy_problem): svd = SVDToolbox(dummy_problem, svd_callback=dummy_callback2) assert svd.config.svd_callback is dummy_callback2 + @pytest.mark.component + def test_with_grey_box(self): + + class BasicGrayBox(ExternalGreyBoxModel): + def input_names(self): + return ["a1", "a2", "a3"] + + def output_names(self): + return ["o1", "o2"] + + def equality_constraint_names(self): + return ["a_sum"] + + def evaluate_equality_constraints(self): + a1 = self._input_values[0] + a2 = self._input_values[1] + return [a1 * 0.5 + a2] + + m = ConcreteModel() + + m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + with pytest.raises(NotImplementedError): + SVDToolbox(model=m) + @pytest.mark.unit def test_init(self, dummy_problem): svd = SVDToolbox(dummy_problem) @@ -2291,6 +2408,30 @@ def test_init(self, model): assert dh.degenerate_set == {} assert dh.irreducible_degenerate_sets == [] + @pytest.mark.component + def test_with_grey_box(self): + + class BasicGrayBox(ExternalGreyBoxModel): + def input_names(self): + return ["a1", "a2", "a3"] + + def output_names(self): + return ["o1", "o2"] + + def equality_constraint_names(self): + return ["a_sum"] + + def evaluate_equality_constraints(self): + a1 = self._input_values[0] + a2 = self._input_values[1] + return [a1 * 0.5 + a2] + + m = ConcreteModel() + + m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + with pytest.raises(NotImplementedError): + DegeneracyHunter2(model=m) + @pytest.mark.unit def test_get_solver(self, model): dh = DegeneracyHunter2(model, solver="ipopt", solver_options={"maxiter": 50}) @@ -2548,6 +2689,30 @@ def model(self): return m + @pytest.mark.component + def test_with_grey_box(self): + + class BasicGrayBox(ExternalGreyBoxModel): + def input_names(self): + return ["a1", "a2", "a3"] + + def output_names(self): + return ["o1", "o2"] + + def equality_constraint_names(self): + return ["a_sum"] + + def evaluate_equality_constraints(self): + a1 = self._input_values[0] + a2 = self._input_values[1] + return [a1 * 0.5 + a2] + + m = ConcreteModel() + + m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + with pytest.raises(NotImplementedError): + IpoptConvergenceAnalysis(model=m) + @pytest.mark.unit def test_init(self, model): ca = IpoptConvergenceAnalysis(model) diff --git a/idaes/core/util/tests/test_model_statistics.py b/idaes/core/util/tests/test_model_statistics.py index 209ac4af1c..618e9666a6 100644 --- a/idaes/core/util/tests/test_model_statistics.py +++ b/idaes/core/util/tests/test_model_statistics.py @@ -31,6 +31,10 @@ from idaes.core.util.model_statistics import * from idaes.core.util.model_statistics import _iter_indexed_block_data_objects +from pyomo.contrib.pynumero.interfaces.external_grey_box import ( + ExternalGreyBoxBlock, + ExternalGreyBoxModel, +) @pytest.mark.unit @@ -685,6 +689,68 @@ def test_degrees_of_freedom(m): assert degrees_of_freedom(m.b2) == -1 +@pytest.mark.unit +def test_degrees_of_freedom_with_graybox(): + """non functional graybox model added to m fixture, to test DOFs + + GreyBoxModel has 3 inputs and 2 outputs calculated an unknown function, + input a1 and a2 are bound by equality constraint through internal graybox model""" + + class BasicGrayBox(ExternalGreyBoxModel): + def input_names(self): + return ["a1", "a2", "a3"] + + def output_names(self): + return ["o1", "o2"] + + def equality_constraint_names(self): + return ["a_sum"] + + def evaluate_equality_constraints(self): + a1 = self._input_values[0] + a2 = self._input_values[1] + return [a1 * 0.5 + a2] + + m = ConcreteModel() + + m.gb = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + m.gb_inactive = ExternalGreyBoxBlock(external_model=BasicGrayBox()) + m.gb_inactive.deactivate() + # test counting functions + assert number_greybox_blocks(m) == 2 + assert number_deactivated_greybox_block(m) == 1 + assert number_activated_greybox_blocks(m) == 1 + assert number_of_greybox_variables(m) == 5 + assert number_of_unfixed_greybox_variables(m) == 5 + assert number_activated_greybox_equalities(m) == 3 + assert number_variables_in_activated_constraints(m) == 5 + # verify DOFS works on stand alone greybox + assert degrees_of_freedom(m) == 2 + m.gb.inputs.fix() + m.gb.inputs["a1"].unfix() + assert number_of_unfixed_greybox_variables(m) == 3 + assert degrees_of_freedom(m) == 0 + m.gb.outputs.fix() + assert degrees_of_freedom(m) == -2 + m.gb.outputs.unfix() + + # verify DOFs works on greybox connected to other vars on a model via constraints + m.a1 = Var(initialize=1) + m.a1.fix() + m.gb.inputs["a2"].unfix() + m.a1_eq = Constraint(expr=m.a1 == m.gb.inputs["a1"]) + assert degrees_of_freedom(m) == 0 + m.o1 = Var(initialize=1) + m.o1_eq = Constraint(expr=m.o1 == m.gb.outputs["o1"]) + m.o1.fix() + assert degrees_of_freedom(m) == -1 + assert number_variables_in_activated_constraints(m) == 7 + assert number_total_constraints(m) == 5 + assert number_total_equalities(m) == 5 + assert number_deactivated_equalities(m) == 3 + assert number_deactivated_constraints(m) == 3 + + @pytest.mark.unit def test_large_residuals_set(m): # Initialize derivative var values so no errors occur