Repository: Pyomo/pyomo

Issue:
Issue Branching Priorities
Several commercial solvers allow for issuing branching directions and priorities for variables of MIP problems however Pyomo doesn't support this.

See [Gurobi](https://www.gurobi.com/documentation/9.0/refman/branchpriority.html), [CPLEX](https://www.ibm.com/support/knowledgecenter/SSSA5P_12.10.0/ilog.odms.cplex.help/CPLEX/FileFormats/topics/ORD.html), [GAMS](https://www.gams.com/latest/docs/UG_LanguageFeatures.html#UG_LanguageFeatures_SettingPrioritiesForBranching)

One should be able to apply a "direction" and "priority" for variables which are read and applied by the solver upon solve time.

Note, this is highly solver-specific and will require separate implementations for the Shell vs Persistent solvers. For example, the CPLEXSHELL solver can write a `.ord` file to disk in exactly the same manner as the warm start file which is then read by the CPLEX script. Meanwhile, the CPLEXDirect solver would utilise `self._solver_model.order.set()`

I would be happy to raise a PR given some design direction:
1. Should this be implemented using the `Suffix` interface? If not, would a simple `List[Tuple[PyomoVar, PriorityType, BranchDirectionType]]` passed in as a `solver.solve()` kwarg be sufficient? 
2. Is it acceptable to only implement this for CPLEXSHELL and CPLEXDirect for now?



Discussion:
Or perhaps this could just be a mutable attribute of the variables themselves (via `_GeneralVarData` or `_VarData`, I'm not sure which...) that is then read off within `_presolve()`?

Resolving patch:
diff --git a/pyomo/opt/base/problem.py b/pyomo/opt/base/problem.py
--- a/pyomo/opt/base/problem.py
+++ b/pyomo/opt/base/problem.py
@@ -8,7 +8,13 @@
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
 
-__all__ = [ 'AbstractProblemWriter', 'WriterFactory', 'ProblemConfigFactory', 'BaseProblemConfig' ]
+__all__ = [
+    "AbstractProblemWriter",
+    "WriterFactory",
+    "ProblemConfigFactory",
+    "BaseProblemConfig",
+    "BranchDirection",
+]
 
 from pyomo.common import Factory
 
@@ -44,3 +50,12 @@ def __enter__(self):
     def __exit__(self, t, v, traceback):
         pass
 
+
+class BranchDirection(object):
+    """ Allowed values for MIP variable branching directions in the `direction` Suffix of a model. """
+
+    default = 0
+    down = -1
+    up = 1
+
+    ALL = {default, down, up}
diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py
--- a/pyomo/solvers/plugins/solvers/CPLEX.py
+++ b/pyomo/solvers/plugins/solvers/CPLEX.py
@@ -23,7 +23,9 @@
 from pyomo.opt.results import *
 from pyomo.opt.solver import *
 from pyomo.solvers.mockmip import MockMIP
+from pyomo.core.base import Var, ComponentMap, Suffix, active_export_suffix_generator
 from pyomo.core.kernel.block import IBlock
+from pyomo.util.components import iter_component
 
 logger = logging.getLogger('pyomo.solvers')
 
@@ -112,6 +114,28 @@ def __new__(cls, *args, **kwds):
         return opt
 
 
+class ORDFileSchema(object):
+    HEADER = "* ENCODING=ISO-8859-1\nNAME             Priority Order\n"
+    FOOTER = "ENDATA\n"
+
+    @classmethod
+    def ROW(cls, name, priority, branch_direction=None):
+        return " %s %s %s\n" % (
+            cls._direction_to_str(branch_direction),
+            name,
+            priority,
+        )
+
+    @staticmethod
+    def _direction_to_str(branch_direction):
+        try:
+            return {BranchDirection.down: "DN", BranchDirection.up: "UP"}[
+                branch_direction
+            ]
+        except KeyError:
+            return ""
+
+
 @SolverFactory.register('_cplex_shell', doc='Shell interface to the CPLEX LP/MIP solver')
 class CPLEXSHELL(ILMLicensedSystemCallSolver):
     """Shell interface to the CPLEX LP/MIP solver
@@ -162,9 +186,6 @@ def warm_start_capable(self):
     # write a warm-start file in the CPLEX MST format.
     #
     def _warm_start(self, instance):
-
-        from pyomo.core.base import Var
-
         # for each variable in the symbol_map, add a child to the
         # variables element.  Both continuous and discrete are accepted
         # (and required, depending on other options), according to the
@@ -201,6 +222,72 @@ def _warm_start(self, instance):
             mst_file.write("</variables>\n")
             mst_file.write("</CPLEXSolution>\n")
 
+    # Expected names of `Suffix` components for branching priorities and directions respectively
+    SUFFIX_PRIORITY_NAME = "priority"
+    SUFFIX_DIRECTION_NAME = "direction"
+
+    def _write_priorities_file(self, instance):
+        """ Write a variable priorities file in the CPLEX ORD format. """
+        priorities, directions = self._get_suffixes(instance)
+        rows = self._convert_priorities_to_rows(instance, priorities, directions)
+        self._write_priority_rows(rows)
+
+    def _get_suffixes(self, instance):
+        if isinstance(instance, IBlock):
+            suffixes = (
+                (suf.name, suf)
+                for suf in pyomo.core.kernel.suffix.export_suffix_generator(
+                    instance, datatype=Suffix.INT, active=True, descend_into=False
+                )
+            )
+        else:
+            suffixes = active_export_suffix_generator(instance, datatype=Suffix.INT)
+        suffixes = dict(suffixes)
+
+        if self.SUFFIX_PRIORITY_NAME not in suffixes:
+            raise ValueError(
+                "Cannot write branching priorities file as `model.%s` Suffix has not been declared."
+                % (self.SUFFIX_PRIORITY_NAME,)
+            )
+
+        return (
+            suffixes[self.SUFFIX_PRIORITY_NAME],
+            suffixes.get(self.SUFFIX_DIRECTION_NAME, ComponentMap()),
+        )
+
+    def _convert_priorities_to_rows(self, instance, priorities, directions):
+        if isinstance(instance, IBlock):
+            smap = getattr(instance, "._symbol_maps")[self._smap_id]
+        else:
+            smap = instance.solutions.symbol_map[self._smap_id]
+        byObject = smap.byObject
+
+        rows = []
+        for var, priority in priorities.items():
+            if priority is None or not var.active:
+                continue
+
+            if not (0 <= priority == int(priority)):
+                raise ValueError("`priority` must be a non-negative integer")
+
+            var_direction = directions.get(var, BranchDirection.default)
+
+            for child_var in iter_component(var):
+                if id(child_var) not in byObject:
+                    continue
+
+                child_var_direction = directions.get(child_var, var_direction)
+
+                rows.append((byObject[id(child_var)], priority, child_var_direction))
+        return rows
+
+    def _write_priority_rows(self, rows):
+        with open(self._priorities_file_name, "w") as ord_file:
+            ord_file.write(ORDFileSchema.HEADER)
+            for var_name, priority, direction in rows:
+                ord_file.write(ORDFileSchema.ROW(var_name, priority, direction))
+            ord_file.write(ORDFileSchema.FOOTER)
+
     # over-ride presolve to extract the warm-start keyword, if specified.
     def _presolve(self, *args, **kwds):
 
@@ -234,6 +321,21 @@ def _presolve(self, *args, **kwds):
                 self._warm_start_file_name = pyutilib.services.TempfileManager.\
                                              create_tempfile(suffix = '.cplex.mst')
 
+        self._priorities_solve = kwds.pop("priorities", False)
+        self._priorities_file_name = _validate_file_name(
+            self, kwds.pop("priorities_file", None), "branching priorities"
+        )
+        user_priorities = self._priorities_file_name is not None
+
+        if (
+            self._priorities_solve
+            and not isinstance(args[0], basestring)
+            and not user_priorities
+        ):
+            self._priorities_file_name = pyutilib.services.TempfileManager.create_tempfile(
+                suffix=".cplex.ord"
+            )
+
         # let the base class handle any remaining keywords/actions.
         ILMLicensedSystemCallSolver._presolve(self, *args, **kwds)
 
@@ -259,6 +361,16 @@ def _presolve(self, *args, **kwds):
                     print("Warm start write time= %.2f seconds"
                           % (end_time-start_time))
 
+            if self._priorities_solve and (not user_priorities):
+                start_time = time.time()
+                self._write_priorities_file(args[0])
+                end_time = time.time()
+                if self._report_timing:
+                    print(
+                        "Branching priorities write time= %.2f seconds"
+                        % (end_time - start_time)
+                    )
+
     def _default_executable(self):
         executable = pyomo.common.Executable("cplex")
         if not executable:
@@ -328,6 +440,9 @@ def create_command_line(self, executable, problem_files):
            (self._warm_start_file_name is not None):
             script += 'read %s\n' % (self._warm_start_file_name,)
 
+        if self._priorities_solve and self._priorities_file_name is not None:
+            script += "read %s\n" % (self._priorities_file_name,)
+
         if 'relax_integrality' in self.options:
             script += 'change problem lp\n'
 
@@ -351,6 +466,9 @@ def create_command_line(self, executable, problem_files):
                 print("Solver warm-start file="
                       +self._warm_start_file_name)
 
+            if self._priorities_solve and self._priorities_file_name is not None:
+                print("Solver priorities file=" + self._priorities_file_name)
+
         #
         # Define command line
         #
diff --git a/pyomo/util/components.py b/pyomo/util/components.py
--- a/pyomo/util/components.py
+++ b/pyomo/util/components.py
@@ -8,8 +8,9 @@
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
 
-from pyomo.core.kernel.component_map import ComponentMap
 from pyomo.common.modeling import unique_component_name
+from pyomo.core.kernel.component_map import ComponentMap
+
 
 def rename_components(model, component_list, prefix):
     """
@@ -52,3 +53,30 @@ def rename_components(model, component_list, prefix):
         name_map[c] = old_name
 
     return name_map
+
+
+def iter_component(obj):
+    """
+    Yield "child" objects from a component that is defined with either the `base` or `kernel` APIs.
+    If the component is not indexed, it returns itself.
+
+    Parameters
+    ----------
+    obj : ComponentType
+        eg. `TupleContainer`, `ListContainer`, `DictContainer`, `IndexedComponent`, or `Component`
+
+    Returns
+    -------
+    Iterator[ComponentType] : Iterator of the component data objects.
+    """
+    try:
+        # catches `IndexedComponent`, and kernel's `_dict`
+        return iter(obj.values())
+    except AttributeError:
+        pass
+
+    try:
+        # catches list and kernel's `_list` and `_tuple`
+        return iter(obj)
+    except TypeError:
+        return iter((obj,))


Test patch:
diff --git a/pyomo/solvers/tests/checks/test_cplex.py b/pyomo/solvers/tests/checks/test_cplex.py
--- a/pyomo/solvers/tests/checks/test_cplex.py
+++ b/pyomo/solvers/tests/checks/test_cplex.py
@@ -9,9 +9,16 @@
 #  ___________________________________________________________________________
 
 import os
+
+import pyutilib
 import pyutilib.th as unittest
 
-from pyomo.solvers.plugins.solvers.CPLEX import _validate_file_name
+import pyomo.kernel as pmo
+from pyomo.core import Binary, ConcreteModel, Constraint, Objective, Var, Integers, RangeSet, minimize, quicksum, Suffix
+from pyomo.environ import *
+from pyomo.opt import ProblemFormat, convert_problem, SolverFactory, BranchDirection
+from pyomo.solvers.plugins.solvers.CPLEX import CPLEXSHELL, MockCPLEX, _validate_file_name
+
 
 class _mock_cplex_128(object):
     def version(self):
@@ -54,5 +61,243 @@ def test_validate_file_name(self):
         with self.assertRaisesRegexp(ValueError, msg):
             _validate_file_name(_128, fname, 'xxx')
 
+
+class CPLEXShellWritePrioritiesFile(unittest.TestCase):
+    """ Unit test on writing of priorities via `CPLEXSHELL._write_priorities_file()` """
+    suffix_cls = Suffix
+
+    def setUp(self):
+        self.mock_model = self.get_mock_model()
+        self.mock_cplex_shell = self.get_mock_cplex_shell(self.mock_model)
+        self.mock_cplex_shell._priorities_file_name = pyutilib.services.TempfileManager.create_tempfile(
+            suffix=".cplex.ord"
+        )
+
+    def tearDown(self):
+        pyutilib.services.TempfileManager.clear_tempfiles()
+
+    def get_mock_model(self):
+        model = ConcreteModel()
+        model.x = Var(within=Binary)
+        model.con = Constraint(expr=model.x >= 1)
+        model.obj = Objective(expr=model.x)
+        return model
+
+    def get_mock_cplex_shell(self, mock_model):
+        solver = MockCPLEX()
+        solver._problem_files, solver._problem_format, solver._smap_id = convert_problem(
+            (mock_model,),
+            ProblemFormat.cpxlp,
+            [ProblemFormat.cpxlp],
+            has_capability=lambda x: True,
+        )
+        return solver
+
+    def get_priorities_file_as_string(self, mock_cplex_shell):
+        with open(mock_cplex_shell._priorities_file_name, "r") as ord_file:
+            priorities_file = ord_file.read()
+        return priorities_file
+
+    @staticmethod
+    def _set_suffix_value(suffix, variable, value):
+        suffix.set_value(variable, value)
+
+    def test_write_without_priority_suffix(self):
+        with self.assertRaises(ValueError):
+            CPLEXSHELL._write_priorities_file(self.mock_cplex_shell, self.mock_model)
+
+    def test_write_priority_to_priorities_file(self):
+        self.mock_model.priority = self.suffix_cls(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        priority_val = 10
+        self._set_suffix_value(self.mock_model.priority, self.mock_model.x, priority_val)
+
+        CPLEXSHELL._write_priorities_file(self.mock_cplex_shell, self.mock_model)
+        priorities_file = self.get_priorities_file_as_string(self.mock_cplex_shell)
+
+        self.assertEqual(
+            priorities_file,
+            "* ENCODING=ISO-8859-1\n"
+            "NAME             Priority Order\n"
+            "  x1 10\n"
+            "ENDATA\n"
+        )
+
+    def test_write_priority_and_direction_to_priorities_file(self):
+        self.mock_model.priority = self.suffix_cls(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        priority_val = 10
+        self._set_suffix_value(self.mock_model.priority, self.mock_model.x, priority_val)
+
+        self.mock_model.direction = self.suffix_cls(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        direction_val = BranchDirection.down
+        self._set_suffix_value(self.mock_model.direction, self.mock_model.x, direction_val)
+
+        CPLEXSHELL._write_priorities_file(self.mock_cplex_shell, self.mock_model)
+        priorities_file = self.get_priorities_file_as_string(self.mock_cplex_shell)
+
+        self.assertEqual(
+            priorities_file,
+            "* ENCODING=ISO-8859-1\n"
+            "NAME             Priority Order\n"
+            " DN x1 10\n"
+            "ENDATA\n"
+        )
+
+    def test_raise_due_to_invalid_priority(self):
+        self.mock_model.priority = self.suffix_cls(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        self._set_suffix_value(self.mock_model.priority, self.mock_model.x, -1)
+        with self.assertRaises(ValueError):
+            CPLEXSHELL._write_priorities_file(self.mock_cplex_shell, self.mock_model)
+
+        self._set_suffix_value(self.mock_model.priority, self.mock_model.x, 1.1)
+        with self.assertRaises(ValueError):
+            CPLEXSHELL._write_priorities_file(self.mock_cplex_shell, self.mock_model)
+
+    def test_use_default_due_to_invalid_direction(self):
+        self.mock_model.priority = self.suffix_cls(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        priority_val = 10
+        self._set_suffix_value(self.mock_model.priority, self.mock_model.x, priority_val)
+
+        self.mock_model.direction = self.suffix_cls(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        self._set_suffix_value(
+            self.mock_model.direction, self.mock_model.x, "invalid_branching_direction"
+        )
+
+        CPLEXSHELL._write_priorities_file(self.mock_cplex_shell, self.mock_model)
+        priorities_file = self.get_priorities_file_as_string(self.mock_cplex_shell)
+
+        self.assertEqual(
+            priorities_file,
+            "* ENCODING=ISO-8859-1\n"
+            "NAME             Priority Order\n"
+            "  x1 10\n"
+            "ENDATA\n"
+        )
+
+
+class CPLEXShellWritePrioritiesFileKernel(CPLEXShellWritePrioritiesFile):
+    suffix_cls = pmo.suffix
+
+    @staticmethod
+    def _set_suffix_value(suffix, variable, value):
+        suffix[variable] = value
+
+    def get_mock_model(self):
+        model = pmo.block()
+        model.x = pmo.variable(domain=Binary)
+        model.con = pmo.constraint(expr=model.x >= 1)
+        model.obj = pmo.objective(expr=model.x)
+        return model
+
+
+class CPLEXShellSolvePrioritiesFile(unittest.TestCase):
+    """ Integration test on the end-to-end application of priorities via the `Suffix` through a `solve()` """
+    def get_mock_model_with_priorities(self):
+        m = ConcreteModel()
+        m.x = Var(domain=Integers)
+        m.s = RangeSet(10)
+        m.y = Var(m.s, domain=Integers)
+        m.o = Objective(expr=m.x + sum(m.y), sense=minimize)
+        m.c = Constraint(expr=m.x >= 1)
+        m.c2 = Constraint(expr=quicksum(m.y[i] for i in m.s) >= 10)
+
+        m.priority = Suffix(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        m.direction = Suffix(direction=Suffix.EXPORT, datatype=Suffix.INT)
+
+        m.priority.set_value(m.x, 1)
+
+        # Ensure tests work for both options of `expand`
+        m.priority.set_value(m.y, 2, expand=False)
+        m.direction.set_value(m.y, BranchDirection.down, expand=True)
+
+        m.direction.set_value(m.y[10], BranchDirection.up)
+        return m
+
+    def test_use_variable_priorities(self):
+        model = self.get_mock_model_with_priorities()
+        with SolverFactory("_mock_cplex") as opt:
+            opt._presolve(model, priorities=True, keepfiles=True)
+
+            with open(opt._priorities_file_name, "r") as ord_file:
+                priorities_file = ord_file.read()
+
+        self.assertEqual(
+            priorities_file,
+            (
+                "* ENCODING=ISO-8859-1\n"
+                "NAME             Priority Order\n"
+                "  x1 1\n"
+                " DN x2 2\n"
+                " DN x3 2\n"
+                " DN x4 2\n"
+                " DN x5 2\n"
+                " DN x6 2\n"
+                " DN x7 2\n"
+                " DN x8 2\n"
+                " DN x9 2\n"
+                " DN x10 2\n"
+                " UP x11 2\n"
+                "ENDATA\n"
+            ),
+        )
+        self.assertIn("read %s\n" % (opt._priorities_file_name,), opt._command.script)
+
+    def test_ignore_variable_priorities(self):
+        model = self.get_mock_model_with_priorities()
+        with SolverFactory("_mock_cplex") as opt:
+            opt._presolve(model, priorities=False, keepfiles=True)
+
+            self.assertIsNone(opt._priorities_file_name)
+            self.assertNotIn(".ord", opt._command.script)
+
+    def test_can_use_manual_priorities_file_with_lp_solve(self):
+        """ Test that we can pass an LP file (not a pyomo model) along with a priorities file to `.solve()` """
+        model = self.get_mock_model_with_priorities()
+
+        with SolverFactory("_mock_cplex") as pre_opt:
+            pre_opt._presolve(model, priorities=True, keepfiles=True)
+            lp_file = pre_opt._problem_files[0]
+            priorities_file_name = pre_opt._priorities_file_name
+
+            with open(priorities_file_name, "r") as ord_file:
+                provided_priorities_file = ord_file.read()
+
+        with SolverFactory("_mock_cplex") as opt:
+            opt._presolve(
+                lp_file,
+                priorities=True,
+                priorities_file=priorities_file_name,
+                keepfiles=True,
+            )
+
+            self.assertIn(".ord", opt._command.script)
+
+            with open(opt._priorities_file_name, "r") as ord_file:
+                priorities_file = ord_file.read()
+
+        self.assertEqual(priorities_file, provided_priorities_file)
+
+
+class CPLEXShellSolvePrioritiesFileKernel(CPLEXShellSolvePrioritiesFile):
+    def get_mock_model_with_priorities(self):
+        m = pmo.block()
+        m.x = pmo.variable(domain=Integers)
+        m.s = range(10)
+
+        m.y = pmo.variable_list(pmo.variable(domain=Integers) for _ in m.s)
+
+        m.o = pmo.objective(expr=m.x + sum(m.y), sense=minimize)
+        m.c = pmo.constraint(expr=m.x >= 1)
+        m.c2 = pmo.constraint(expr=quicksum(m.y[i] for i in m.s) >= 10)
+
+        m.priority = pmo.suffix(direction=Suffix.EXPORT, datatype=Suffix.INT)
+        m.direction = pmo.suffix(direction=Suffix.EXPORT, datatype=Suffix.INT)
+
+        m.priority[m.x] = 1
+        m.priority[m.y] = 2
+        m.direction[m.y] = BranchDirection.down
+        m.direction[m.y[-1]] = BranchDirection.up
+        return m
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/pyomo/util/tests/test_components.py b/pyomo/util/tests/test_components.py
--- a/pyomo/util/tests/test_components.py
+++ b/pyomo/util/tests/test_components.py
@@ -7,10 +7,13 @@
 #  rights in this software.
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
+from six.moves import zip_longest
 
 import pyutilib.th as unittest
+
 import pyomo.environ as pe
-from pyomo.util.components import rename_components
+import pyomo.kernel as pmo
+from pyomo.util.components import iter_component, rename_components
 
 class TestUtilComponents(unittest.TestCase):
 
@@ -46,6 +49,53 @@ def con_rule(m, i):
         self.assertEquals(model.b.name, 'b')
         self.assertEquals(model.b.scaled_bz.name, 'b.scaled_bz')
 
+    def assertSameComponents(self, obj, other_obj):
+        for i, j in zip_longest(obj, other_obj):
+            self.assertEqual(id(i), id(j))
+
+    def test_iter_component_base(self):
+        model = pe.ConcreteModel()
+        model.x = pe.Var([1, 2, 3], initialize=0)
+        model.z = pe.Var(initialize=0)
+
+        def con_rule(m, i):
+            return m.x[i] + m.z == i
+
+        model.con = pe.Constraint([1, 2, 3], rule=con_rule)
+        model.zcon = pe.Constraint(expr=model.z >= model.x[2])
+
+        self.assertSameComponents(list(iter_component(model.x)), list(model.x.values()))
+        self.assertSameComponents(list(iter_component(model.z)), [model.z[None]])
+        self.assertSameComponents(
+            list(iter_component(model.con)), list(model.con.values())
+        )
+        self.assertSameComponents(list(iter_component(model.zcon)), [model.zcon[None]])
+
+    def test_iter_component_kernel(self):
+        model = pmo.block()
+        model.x = pmo.variable_list(pmo.variable(value=0) for _ in [1, 2, 3])
+        model.z = pmo.variable(value=0)
+
+        model.con = pmo.constraint_dict(
+            (i, pmo.constraint(expr=model.x[i - 1] + model.z == i)) for i in [1, 2, 3]
+        )
+        model.zcon = pmo.constraint(expr=model.z >= model.x[2])
+
+        model.param_t = pmo.parameter_tuple(pmo.parameter(value=36) for _ in [1, 2, 3])
+        model.param = pmo.parameter(value=42)
+
+        self.assertSameComponents(list(iter_component(model.x)), list(model.x))
+        self.assertSameComponents(list(iter_component(model.z)), [model.z])
+        self.assertSameComponents(
+            list(iter_component(model.con)), list(model.con.values())
+        )
+        self.assertSameComponents(list(iter_component(model.zcon)), [model.zcon])
+        self.assertSameComponents(
+            list(iter_component(model.param_t)), list(model.param_t)
+        )
+        self.assertSameComponents(list(iter_component(model.param)), [model.param])
+
+
 if __name__ == '__main__':
     # t = TestUtilComponents()
     # t.test_rename_components()