Repository: Pyomo/pyomo

Issue:
core.logical_to_linear misplaces transformed components on hierarchical models
## Summary

`core.logical_to_linear` puts transformed components on the parent model rather than the parent block, resulting in incorrect solutions if you solve subblocks of a model.

### Steps to reproduce the issue

```
from pyomo.environ import *
from pyomo.gdp import *

m = ConcreteModel()
m.b = Block()
m.b.x = Var(bounds=(0,23))
m.b.Y = BooleanVar()
m.b.disj1 = Disjunct()
m.b.disj1.c = Constraint(expr=m.b.x >= 21)
m.b.disj2 = Disjunct()
m.b.disj2.c = Constraint(expr=m.b.x >= 19)
m.b.disjunction = Disjunction(expr=[m.b.disj1, m.b.disj2])
m.b.obj = Objective(expr=m.b.x)

m.b.lc = LogicalConstraint(expr=m.b.Y.implies(m.b.disj1.indicator_var))
m.b.Y.fix(True)

TransformationFactory('gdp.bigm').apply_to(m)
TransformationFactory('core.logical_to_linear').apply_to(m)

print("Solve block:")
SolverFactory('gurobi').solve(m.b)
m.b.x.display()
print("Solve model:")
SolverFactory('gurobi').solve(m)
m.b.x.display()
```

### Error Message

The logical constraint has implicitly been moved to `m` rather than `m.b`, so solving the model is correct, but solving `m.b` is better than optimal because it's actually a relaxation (without the logical implication):
```
Solve block:
x : Size=1, Index=None
    Key  : Lower : Value : Upper : Fixed : Stale : Domain
    None :     0 :  19.0 :    23 : False : False :  Reals
Solve model:
x : Size=1, Index=None
    Key  : Lower : Value : Upper : Fixed : Stale : Domain
    None :     0 :  21.0 :    23 : False : False :  Reals
```

### Information on your system

Pyomo version: main
Python version: 3.6
Operating system: linux
How Pyomo was installed (PyPI, conda, source): source
Solver (if applicable): Gurobi, but it doesn't matter


Discussion:

Resolving patch:
diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py
--- a/pyomo/core/base/boolean_var.py
+++ b/pyomo/core/base/boolean_var.py
@@ -17,7 +17,8 @@
 from pyomo.core.expr.boolean_value import BooleanValue
 from pyomo.core.expr.numvalue import value
 from pyomo.core.base.component import ComponentData, ModelComponentFactory
-from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set
+from pyomo.core.base.indexed_component import (IndexedComponent,
+                                               UnindexedComponent_set)
 from pyomo.core.base.misc import apply_indexed_rule
 from pyomo.core.base.set import Set, BooleanSet
 from pyomo.core.base.util import is_functor
@@ -126,7 +127,8 @@ def unfix(self):
 
     free=unfix
 
-    def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False):
+    def to_string(self, verbose=None, labeler=None, smap=None,
+                  compute_values=False):
         """Return the component name"""
         if self.fixed and compute_values:
             try:
@@ -229,7 +231,8 @@ def fix(self, *val):
         if len(val) == 1:
             self.value = val[0]
         elif len(val) > 1:
-            raise TypeError("fix expected at most 1 arguments, got %d" % (len(val)))
+            raise TypeError("fix expected at most 1 arguments, got %d" %
+                            (len(val)))
 
     def unfix(self):
         """Sets the fixed indicator to False."""
@@ -239,7 +242,8 @@ def unfix(self):
 
     def get_associated_binary(self):
         """Get the binary _VarData associated with this _GeneralBooleanVarData"""
-        return self._associated_binary() if self._associated_binary is not None else None
+        return self._associated_binary() if self._associated_binary \
+            is not None else None
 
     def associate_binary_var(self, binary_var):
         """Associate a binary _VarData to this _GeneralBooleanVarData"""
diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py
--- a/pyomo/core/plugins/transform/logical_to_linear.py
+++ b/pyomo/core/plugins/transform/logical_to_linear.py
@@ -1,20 +1,27 @@
-"""Transformation from BooleanVar and LogicalConstraint to Binary and Constraints."""
+"""Transformation from BooleanVar and LogicalConstraint to Binary and
+Constraints."""
 from pyomo.common.collections import ComponentMap
 from pyomo.common.modeling import unique_component_name
 from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr
-from pyomo.core import TransformationFactory, BooleanVar, VarList, Binary, LogicalConstraint, Block, ConstraintList, \
-    native_types, BooleanVarList
+from pyomo.core import (TransformationFactory, BooleanVar, VarList, Binary,
+                        LogicalConstraint, Block, ConstraintList, native_types,
+                        BooleanVarList)
 from pyomo.core.expr.cnf_walker import to_cnf
-from pyomo.core.expr.logical_expr import AndExpression, OrExpression, NotExpression, AtLeastExpression, \
-    AtMostExpression, ExactlyExpression, special_boolean_atom_types, EqualityExpression, InequalityExpression, \
-    RangedExpression
+from pyomo.core.expr.logical_expr import (AndExpression, OrExpression,
+                                          NotExpression, AtLeastExpression,
+                                          AtMostExpression, ExactlyExpression,
+                                          special_boolean_atom_types,
+                                          EqualityExpression,
+                                          InequalityExpression,
+                                          RangedExpression)
 from pyomo.core.expr.numvalue import native_logical_types, value
 from pyomo.core.expr.visitor import StreamBasedExpressionVisitor
 from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation
 from pyomo.gdp import Disjunct
 
 
-@TransformationFactory.register("core.logical_to_linear", doc="Convert logic to linear constraints")
+@TransformationFactory.register("core.logical_to_linear", 
+                                doc="Convert logic to linear constraints")
 class LogicalToLinear(IsomorphicTransformation):
     """
     Re-encode logical constraints as linear constraints,
@@ -22,15 +29,26 @@ class LogicalToLinear(IsomorphicTransformation):
     """
 
     def _apply_to(self, model, **kwds):
-        for boolean_var in model.component_objects(ctype=BooleanVar, descend_into=(Block, Disjunct)):
-            new_varlist = None
+        for boolean_var in model.component_objects(ctype=BooleanVar,
+                                                   descend_into=(Block,
+                                                                 Disjunct)):
+            new_varlists = {}
             for bool_vardata in boolean_var.values():
-                if new_varlist is None and bool_vardata.get_associated_binary() is None:
-                    new_var_list_name = unique_component_name(model, boolean_var.local_name + '_asbinary')
+                # we have neither the list nor an associated binary
+                parent_block = bool_vardata.parent_block()
+                new_varlist = new_varlists.get(parent_block)
+                if new_varlist is None and \
+                   bool_vardata.get_associated_binary() is None:
+                    new_var_list_name = unique_component_name(
+                        parent_block,
+                        boolean_var.local_name + '_asbinary')
                     new_varlist = VarList(domain=Binary)
-                    setattr(model, new_var_list_name, new_varlist)
+                    setattr(parent_block, new_var_list_name, new_varlist)
+                    new_varlists[parent_block] = new_varlist
 
                 if bool_vardata.get_associated_binary() is None:
+                    # we already have a list, but need to create the associated
+                    # binary
                     new_binary_vardata = new_varlist.add()
                     bool_vardata.associate_binary_var(new_binary_vardata)
                     if bool_vardata.value is not None:
@@ -40,14 +58,25 @@ def _apply_to(self, model, **kwds):
 
         # Process statements in global (entire model) context
         _process_logical_constraints_in_logical_context(model)
+        # Now go do the ones on Blocks
+        for block in model.component_data_objects(Block,
+                                                  descend_into=(Block, Disjunct),
+                                                  active=True):
+            _process_logical_constraints_in_logical_context(block)
         # Process statements that appear in disjuncts
-        for disjunct in model.component_data_objects(Disjunct, descend_into=(Block, Disjunct), active=True):
+        for disjunct in model.component_data_objects(Disjunct,
+                                                     descend_into=(Block,
+                                                                   Disjunct), 
+                                                     active=True):
             _process_logical_constraints_in_logical_context(disjunct)
 
 
 def update_boolean_vars_from_binary(model, integer_tolerance=1e-5):
-    """Updates all Boolean variables based on the value of their linked binary variables."""
-    for boolean_var in model.component_data_objects(BooleanVar, descend_into=(Block, Disjunct)):
+    """Updates all Boolean variables based on the value of their linked binary
+    variables."""
+    for boolean_var in model.component_data_objects(BooleanVar,
+                                                    descend_into=(Block,
+                                                                  Disjunct)):
         binary_var = boolean_var.get_associated_binary()
         if binary_var is not None and binary_var.value is not None:
             if abs(binary_var.value - 1) <= integer_tolerance:
@@ -55,10 +84,10 @@ def update_boolean_vars_from_binary(model, integer_tolerance=1e-5):
             elif abs(binary_var.value) <= integer_tolerance:
                 boolean_var.value = False
             else:
-                raise ValueError("Binary variable has non-{0,1} value: %s = %s" % (binary_var.name, binary_var.value))
+                raise ValueError("Binary variable has non-{0,1} value: "
+                                 "%s = %s" % (binary_var.name, binary_var.value))
             boolean_var.stale = binary_var.stale
 
-
 def _process_logical_constraints_in_logical_context(context):
     new_xfrm_block_name = unique_component_name(context, 'logic_to_linear')
     new_xfrm_block = Block(doc="Transformation objects for logic_to_linear")
@@ -71,8 +100,10 @@ def _process_logical_constraints_in_logical_context(context):
     indicator_map = ComponentMap()
     cnf_statements = []
     # Convert all logical constraints to CNF
-    for logical_constraint in context.component_data_objects(ctype=LogicalConstraint, active=True):
-        cnf_statements.extend(to_cnf(logical_constraint.body, new_boolvarlist, indicator_map))
+    for logical_constraint in context.component_data_objects(
+            ctype=LogicalConstraint, active=True, descend_into=False):
+        cnf_statements.extend(to_cnf(logical_constraint.body, new_boolvarlist,
+                                     indicator_map))
         logical_constraint.deactivate()
 
     # Associate new Boolean vars to new binary variables
@@ -85,14 +116,18 @@ def _process_logical_constraints_in_logical_context(context):
         for linear_constraint in _cnf_to_linear_constraint_list(cnf_statement):
             new_constrlist.add(expr=linear_constraint)
 
-    # Add bigM associated with special atoms
-    # Note: this ad-hoc reformulation may be revisited for tightness in the future.
+    # Add bigM associated with special atoms 
+    # Note: this ad-hoc reformulation may be revisited for tightness in the 
+    # future.
     old_varlist_length = len(new_varlist)
     for indicator_var, special_atom in indicator_map.items():
-        for linear_constraint in _cnf_to_linear_constraint_list(special_atom, indicator_var, new_varlist):
+        for linear_constraint in _cnf_to_linear_constraint_list(special_atom,
+                                                                indicator_var,
+                                                                new_varlist):
             new_constrlist.add(expr=linear_constraint)
 
-    # Previous step may have added auxiliary binaries. Associate augmented Booleans to them.
+    # Previous step may have added auxiliary binaries. Associate augmented
+    # Booleans to them.
     num_new = len(new_varlist) - old_varlist_length
     list_o_vars = list(new_varlist.values())
     if num_new:
@@ -100,9 +135,9 @@ def _process_logical_constraints_in_logical_context(context):
             new_bool_vardata = new_boolvarlist.add()
             new_bool_vardata.associate_binary_var(binary_vardata)
 
-    # If added components were not used, remove them.
-    # Note: it is ok to simply delete the index_set for these components, because by
-    # default, a new set object is generated for each [Thing]List.
+    # If added components were not used, remove them.  
+    # Note: it is ok to simply delete the index_set for these components,
+    # because by default, a new set object is generated for each [Thing]List.
     if len(new_constrlist) == 0:
         new_xfrm_block.del_component(new_constrlist.index_set())
         new_xfrm_block.del_component(new_constrlist)
@@ -118,22 +153,27 @@ def _process_logical_constraints_in_logical_context(context):
         context.del_component(new_xfrm_block)
 
 
-def _cnf_to_linear_constraint_list(cnf_expr, indicator_var=None, binary_varlist=None):
+def _cnf_to_linear_constraint_list(cnf_expr, indicator_var=None,
+                                   binary_varlist=None):
     # Screen for constants
     if type(cnf_expr) in native_types or cnf_expr.is_constant():
         if value(cnf_expr) is True:
             return []
         else:
             raise ValueError(
-                "Cannot build linear constraint for logical expression with constant value False: %s"
+                "Cannot build linear constraint for logical expression with "
+                "constant value False: %s"
                 % cnf_expr)
     if cnf_expr.is_expression_type():
-        return CnfToLinearVisitor(indicator_var, binary_varlist).walk_expression(cnf_expr)
+        return CnfToLinearVisitor(indicator_var, binary_varlist).walk_expression(
+            cnf_expr)
     else:
-        return [cnf_expr.get_associated_binary() == 1]  # Assume that cnf_expr is a BooleanVar
+        return [cnf_expr.get_associated_binary() == 1]  # Assume that cnf_expr
+                                                        # is a BooleanVar
 
 
-_numeric_relational_types = {InequalityExpression, EqualityExpression, RangedExpression}
+_numeric_relational_types = {InequalityExpression, EqualityExpression,
+                             RangedExpression}
 
 
 class CnfToLinearVisitor(StreamBasedExpressionVisitor):
@@ -150,13 +190,14 @@ def __init__(self, indicator_var, binary_varlist):
 
     def exitNode(self, node, values):
         if type(node) == AndExpression:
-            return list((v if type(v) in _numeric_relational_types else v == 1) for v in values)
+            return list((v if type(v) in _numeric_relational_types else v == 1)
+                        for v in values)
         elif type(node) == OrExpression:
             return sum(values) >= 1
         elif type(node) == NotExpression:
             return 1 - values[0]
-        # Note: the following special atoms should only be encountered as root nodes.
-        # If they are encountered otherwise, something went wrong.
+        # Note: the following special atoms should only be encountered as root
+        # nodes.  If they are encountered otherwise, something went wrong.
         sum_values = sum(values[1:])
         num_args = node.nargs() - 1  # number of logical arguments
         if self._indicator is None:
@@ -169,30 +210,36 @@ def exitNode(self, node, values):
         else:
             rhs_lb, rhs_ub = compute_bounds_on_expr(values[0])
             if rhs_lb == float('-inf') or rhs_ub == float('inf'):
-                raise ValueError(
-                    "Cannnot generate linear constraints for %s([N, *logical_args]) with unbounded N. "
-                    "Detected %s <= N <= %s." % (type(node).__name__, rhs_lb, rhs_ub)
-                )
+                raise ValueError( "Cannnot generate linear constraints for %s"
+                                  "([N, *logical_args]) with unbounded N. "
+                                  "Detected %s <= N <= %s." %
+                                  (type(node).__name__, rhs_lb, rhs_ub) )
             indicator_binary = self._indicator.get_associated_binary()
             if type(node) == AtLeastExpression:
                 return [
                     sum_values >= values[0] - rhs_ub * (1 - indicator_binary),
-                    sum_values <= values[0] - 1 + (-(rhs_lb - 1) + num_args) * indicator_binary
+                    sum_values <= values[0] - 1 + (-(rhs_lb - 1) + num_args) * \
+                    indicator_binary
                 ]
             elif type(node) == AtMostExpression:
                 return [
-                    sum_values <= values[0] + (-rhs_lb + num_args) * (1 - indicator_binary),
-                    sum_values >= (values[0] + 1) - (rhs_ub + 1) * indicator_binary
+                    sum_values <= values[0] + (-rhs_lb + num_args) * \
+                    (1 - indicator_binary),
+                    sum_values >= (values[0] + 1) - (rhs_ub + 1) * \
+                    indicator_binary
                 ]
             elif type(node) == ExactlyExpression:
                 less_than_binary = self._binary_varlist.add()
                 more_than_binary = self._binary_varlist.add()
                 return [
-                    sum_values <= values[0] + (-rhs_lb + num_args) * (1 - indicator_binary),
+                    sum_values <= values[0] + (-rhs_lb + num_args) * \
+                    (1 - indicator_binary),
                     sum_values >= values[0] - rhs_ub * (1 - indicator_binary),
                     indicator_binary + less_than_binary + more_than_binary >= 1,
-                    sum_values <= values[0] - 1 + (-(rhs_lb - 1) + num_args) * (1 - less_than_binary),
-                    sum_values >= values[0] + 1 - (rhs_ub + 1) * (1 - more_than_binary),
+                    sum_values <= values[0] - 1 + (-(rhs_lb - 1) + num_args) * \
+                    (1 - less_than_binary),
+                    sum_values >= values[0] + 1 - (rhs_ub + 1) * \
+                    (1 - more_than_binary),
                 ]
             pass
 


Test patch:
diff --git a/pyomo/core/tests/unit/test_logical_to_linear.py b/pyomo/core/tests/unit/test_logical_to_linear.py
--- a/pyomo/core/tests/unit/test_logical_to_linear.py
+++ b/pyomo/core/tests/unit/test_logical_to_linear.py
@@ -11,10 +11,12 @@
 import pyomo.common.unittest as unittest
 
 from pyomo.core.expr.sympy_tools import sympy_available
-from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary
-from pyomo.environ import (
-    ConcreteModel, BooleanVar, LogicalConstraint, lor, TransformationFactory, RangeSet,
-    Var, Constraint, ComponentMap, value, BooleanSet, atleast, atmost, exactly)
+from pyomo.core.plugins.transform.logical_to_linear import \
+    update_boolean_vars_from_binary
+from pyomo.environ import ( ConcreteModel, BooleanVar, LogicalConstraint, lor,
+                            TransformationFactory, RangeSet, Var, Constraint,
+                            ComponentMap, value, BooleanSet, atleast, atmost,
+                            exactly, Block)
 from pyomo.gdp import Disjunct, Disjunction
 from pyomo.repn import generate_standard_repn
 
@@ -27,13 +29,15 @@ def _generate_boolean_model(nvars):
 
 
 def _constrs_contained_within(test_case, test_constr_tuples, constraint_list):
-    """Checks to see if constraints defined by test_constr_tuples are in the constraint list.
+    """Checks to see if constraints defined by test_constr_tuples are in the
+    constraint list.
 
     Parameters
     ----------
     constraint_list : Constraint
     test_constr_tuples : list of tuple
     test_case : unittest.TestCase
+
     """
     # Move const term from body
     def _move_const_from_body(lower, repn, upper):
@@ -47,26 +51,34 @@ def _move_const_from_body(lower, repn, upper):
     def _repns_match(repn, test_repn):
         if not len(repn.linear_vars) == len(test_repn.linear_vars):
             return False
-        coef_map = ComponentMap((var, coef) for var, coef in zip(repn.linear_vars, repn.linear_coefs))
+        coef_map = ComponentMap((var, coef) for var, coef in
+                                zip(repn.linear_vars, repn.linear_coefs))
         for var, coef in zip(test_repn.linear_vars, test_repn.linear_coefs):
             if not coef_map.get(var, 0) == coef:
                 return False
         return True
 
-    constr_list_tuples = [
-        _move_const_from_body(constr.lower, generate_standard_repn(constr.body), constr.upper)
-        for constr in constraint_list.values()]
+    constr_list_tuples = [ _move_const_from_body(
+        constr.lower,
+        generate_standard_repn(constr.body),
+        constr.upper) for constr in
+                           constraint_list.values()]
     for test_lower, test_body, test_upper in test_constr_tuples:
         test_repn = generate_standard_repn(test_body)
-        test_lower, test_repn, test_upper = _move_const_from_body(test_lower, test_repn, test_upper)
+        test_lower, test_repn, test_upper = _move_const_from_body(test_lower,
+                                                                  test_repn,
+                                                                  test_upper)
         found_match = False
         # Make sure one of the list tuples matches
         for lower, repn, upper in constr_list_tuples:
-            if lower == test_lower and upper == test_upper and _repns_match(repn, test_repn):
+            if lower == test_lower and upper == test_upper and \
+               _repns_match(repn, test_repn):
                 found_match = True
                 break
-        test_case.assertTrue(found_match, "{} <= {} <= {} was not found in constraint list.".format(
-            test_lower, test_body, test_upper))
+        test_case.assertTrue(
+            found_match, 
+            "{} <= {} <= {} was not found in constraint list.".format(
+                test_lower, test_body, test_upper))
 
 
 @unittest.skipUnless(sympy_available, "Sympy not available")
@@ -78,21 +90,23 @@ def test_implies(self):
         m.y = BooleanVar()
         m.p = LogicalConstraint(expr=m.x.implies(m.y))
         TransformationFactory('core.logical_to_linear').apply_to(m)
-        _constrs_contained_within(
-            self, [(1, (1 - m.x.get_associated_binary()) + m.y.get_associated_binary(), None)],
-            m.logic_to_linear.transformed_constraints)
+        _constrs_contained_within( self, [(1, (1 - m.x.get_associated_binary())
+                                           + m.y.get_associated_binary(),
+                                           None)],
+                                   m.logic_to_linear.transformed_constraints)
 
     def test_literal(self):
         m = ConcreteModel()
         m.Y = BooleanVar()
         m.p = LogicalConstraint(expr=m.Y)
         TransformationFactory('core.logical_to_linear').apply_to(m)
-        _constrs_contained_within(
-            self, [(1, m.Y.get_associated_binary(), 1)], m.logic_to_linear.transformed_constraints)
+        _constrs_contained_within( self, [(1, m.Y.get_associated_binary(), 1)],
+                                   m.logic_to_linear.transformed_constraints)
 
     def test_constant_True(self):
         m = ConcreteModel()
-        with self.assertRaisesRegex(ValueError, "LogicalConstraint 'p' is always True."):
+        with self.assertRaisesRegex(ValueError, 
+                                    "LogicalConstraint 'p' is always True."):
             m.p = LogicalConstraint(expr=True)
             TransformationFactory('core.logical_to_linear').apply_to(m)
         self.assertIsNone(m.component('logic_to_linear'))
@@ -129,7 +143,9 @@ def test_xfrm_atleast_statement(self):
         _constrs_contained_within(
             self, [
                 (2,
-                 m.Y[1].get_associated_binary() + m.Y[2].get_associated_binary() + m.Y[3].get_associated_binary(),
+                 m.Y[1].get_associated_binary() + \
+                 m.Y[2].get_associated_binary() + \
+                 m.Y[3].get_associated_binary(),
                  None)
             ], m.logic_to_linear.transformed_constraints)
 
@@ -142,7 +158,9 @@ def test_xfrm_atmost_statement(self):
         _constrs_contained_within(
             self, [
                 (None,
-                 m.Y[1].get_associated_binary() + m.Y[2].get_associated_binary() + m.Y[3].get_associated_binary(),
+                 m.Y[1].get_associated_binary() + \
+                 m.Y[2].get_associated_binary() + \
+                 m.Y[3].get_associated_binary(),
                  2)
             ], m.logic_to_linear.transformed_constraints)
 
@@ -154,56 +172,72 @@ def test_xfrm_exactly_statement(self):
         TransformationFactory('core.logical_to_linear').apply_to(m)
         _constrs_contained_within(
             self, [
-                (2, m.Y[1].get_associated_binary() + m.Y[2].get_associated_binary() + m.Y[3].get_associated_binary(), 2)
+                (2, m.Y[1].get_associated_binary() + \
+                 m.Y[2].get_associated_binary() + \
+                 m.Y[3].get_associated_binary(), 2)
             ], m.logic_to_linear.transformed_constraints)
 
     def test_xfrm_special_atoms_nonroot(self):
         m = ConcreteModel()
         m.s = RangeSet(3)
         m.Y = BooleanVar(m.s)
-        m.p = LogicalConstraint(expr=m.Y[1].implies(atleast(2, m.Y[1], m.Y[2], m.Y[3])))
+        m.p = LogicalConstraint(expr=m.Y[1].implies(atleast(2, m.Y[1], m.Y[2],
+                                                            m.Y[3])))
         TransformationFactory('core.logical_to_linear').apply_to(m)
         Y_aug = m.logic_to_linear.augmented_vars
         self.assertEqual(len(Y_aug), 1)
         self.assertEqual(Y_aug[1].domain, BooleanSet)
         _constrs_contained_within(
             self, [
-                (None, sum(m.Y[:].get_associated_binary()) - (1 + 2 * Y_aug[1].get_associated_binary()), 0),
-                (1, (1 - m.Y[1].get_associated_binary()) + Y_aug[1].get_associated_binary(), None),
-                (None, 2 - 2 * (1 - Y_aug[1].get_associated_binary()) - sum(m.Y[:].get_associated_binary()), 0)
+                (None, sum(m.Y[:].get_associated_binary()) - \
+                 (1 + 2 * Y_aug[1].get_associated_binary()), 0),
+                (1, (1 - m.Y[1].get_associated_binary()) + \
+                 Y_aug[1].get_associated_binary(), None),
+                (None, 2 - 2 * (1 - Y_aug[1].get_associated_binary()) - \
+                 sum(m.Y[:].get_associated_binary()), 0)
             ], m.logic_to_linear.transformed_constraints)
 
         m = ConcreteModel()
         m.s = RangeSet(3)
         m.Y = BooleanVar(m.s)
-        m.p = LogicalConstraint(expr=m.Y[1].implies(atmost(2, m.Y[1], m.Y[2], m.Y[3])))
+        m.p = LogicalConstraint(expr=m.Y[1].implies(atmost(2, m.Y[1], m.Y[2],
+                                                           m.Y[3])))
         TransformationFactory('core.logical_to_linear').apply_to(m)
         Y_aug = m.logic_to_linear.augmented_vars
         self.assertEqual(len(Y_aug), 1)
         self.assertEqual(Y_aug[1].domain, BooleanSet)
         _constrs_contained_within(
             self, [
-                (None, sum(m.Y[:].get_associated_binary()) - (1 - Y_aug[1].get_associated_binary() + 2), 0),
-                (1, (1 - m.Y[1].get_associated_binary()) + Y_aug[1].get_associated_binary(), None),
-                (None, 3 - 3 * Y_aug[1].get_associated_binary() - sum(m.Y[:].get_associated_binary()), 0)
+                (None, sum(m.Y[:].get_associated_binary()) - \
+                 (1 - Y_aug[1].get_associated_binary() + 2), 0),
+                (1, (1 - m.Y[1].get_associated_binary()) + \
+                 Y_aug[1].get_associated_binary(), None),
+                (None, 3 - 3 * Y_aug[1].get_associated_binary() - \
+                 sum(m.Y[:].get_associated_binary()), 0)
             ], m.logic_to_linear.transformed_constraints)
 
         m = ConcreteModel()
         m.s = RangeSet(3)
         m.Y = BooleanVar(m.s)
-        m.p = LogicalConstraint(expr=m.Y[1].implies(exactly(2, m.Y[1], m.Y[2], m.Y[3])))
+        m.p = LogicalConstraint(expr=m.Y[1].implies(exactly(2, m.Y[1], m.Y[2],
+                                                            m.Y[3])))
         TransformationFactory('core.logical_to_linear').apply_to(m)
         Y_aug = m.logic_to_linear.augmented_vars
         self.assertEqual(len(Y_aug), 3)
         self.assertEqual(Y_aug[1].domain, BooleanSet)
         _constrs_contained_within(
             self, [
-                (1, (1 - m.Y[1].get_associated_binary()) + Y_aug[1].get_associated_binary(), None),
-                (None, sum(m.Y[:].get_associated_binary()) - (1 - Y_aug[1].get_associated_binary() + 2), 0),
-                (None, 2 - 2 * (1 - Y_aug[1].get_associated_binary()) - sum(m.Y[:].get_associated_binary()), 0),
+                (1, (1 - m.Y[1].get_associated_binary()) + \
+                 Y_aug[1].get_associated_binary(), None),
+                (None, sum(m.Y[:].get_associated_binary()) - \
+                 (1 - Y_aug[1].get_associated_binary() + 2), 0),
+                (None, 2 - 2 * (1 - Y_aug[1].get_associated_binary()) - \
+                 sum(m.Y[:].get_associated_binary()), 0),
                 (1, sum(Y_aug[:].get_associated_binary()), None),
-                (None, sum(m.Y[:].get_associated_binary()) - (1 + 2 * (1 - Y_aug[2].get_associated_binary())), 0),
-                (None, 3 - 3 * (1 - Y_aug[3].get_associated_binary()) - sum(m.Y[:].get_associated_binary()), 0),
+                (None, sum(m.Y[:].get_associated_binary()) - \
+                 (1 + 2 * (1 - Y_aug[2].get_associated_binary())), 0),
+                (None, 3 - 3 * (1 - Y_aug[3].get_associated_binary()) - \
+                 sum(m.Y[:].get_associated_binary()), 0),
             ], m.logic_to_linear.transformed_constraints)
 
         # Note: x is now a variable
@@ -211,47 +245,68 @@ def test_xfrm_special_atoms_nonroot(self):
         m.s = RangeSet(3)
         m.Y = BooleanVar(m.s)
         m.x = Var(bounds=(1, 3))
-        m.p = LogicalConstraint(expr=m.Y[1].implies(exactly(m.x, m.Y[1], m.Y[2], m.Y[3])))
+        m.p = LogicalConstraint(expr=m.Y[1].implies(exactly(m.x, m.Y[1], m.Y[2],
+                                                            m.Y[3])))
         TransformationFactory('core.logical_to_linear').apply_to(m)
         Y_aug = m.logic_to_linear.augmented_vars
         self.assertEqual(len(Y_aug), 3)
         self.assertEqual(Y_aug[1].domain, BooleanSet)
         _constrs_contained_within(
             self, [
-                (1, (1 - m.Y[1].get_associated_binary()) + Y_aug[1].get_associated_binary(), None),
-                (None, sum(m.Y[:].get_associated_binary()) - (m.x + 2 * (1 - Y_aug[1].get_associated_binary())), 0),
-                (None, m.x - 3 * (1 - Y_aug[1].get_associated_binary()) - sum(m.Y[:].get_associated_binary()), 0),
+                (1, (1 - m.Y[1].get_associated_binary()) + \
+                 Y_aug[1].get_associated_binary(), None),
+                (None, sum(m.Y[:].get_associated_binary()) - \
+                 (m.x + 2 * (1 - Y_aug[1].get_associated_binary())), 0),
+                (None, m.x - 3 * (1 - Y_aug[1].get_associated_binary()) - \
+                 sum(m.Y[:].get_associated_binary()), 0),
                 (1, sum(Y_aug[:].get_associated_binary()), None),
-                (None, sum(m.Y[:].get_associated_binary()) - (m.x - 1 + 3 * (1 - Y_aug[2].get_associated_binary())), 0),
-                (None, m.x + 1 - 4 * (1 - Y_aug[3].get_associated_binary()) - sum(m.Y[:].get_associated_binary()), 0),
+                (None, sum(m.Y[:].get_associated_binary()) - \
+                 (m.x - 1 + 3 * (1 - Y_aug[2].get_associated_binary())), 0),
+                (None, m.x + 1 - 4 * (1 - Y_aug[3].get_associated_binary()) - \
+                 sum(m.Y[:].get_associated_binary()), 0),
             ], m.logic_to_linear.transformed_constraints)
 
     def test_xfrm_atleast_nested(self):
         m = _generate_boolean_model(4)
-        m.p = LogicalConstraint(expr=atleast(1, atleast(2, m.Y[1], m.Y[1].lor(m.Y[2]), m.Y[2]).lor(m.Y[3]), m.Y[4]))
+        m.p = LogicalConstraint(expr=atleast(1, atleast(2, m.Y[1],
+                                                        m.Y[1].lor(m.Y[2]),
+                                                        m.Y[2]).lor(m.Y[3]),
+                                             m.Y[4]))
         TransformationFactory('core.logical_to_linear').apply_to(m)
         Y_aug = m.logic_to_linear.augmented_vars
         self.assertEqual(len(Y_aug), 3)
         _constrs_contained_within(
             self, [
-                (1, Y_aug[1].get_associated_binary() + m.Y[4].get_associated_binary(), None),
-                (1, 1 - Y_aug[2].get_associated_binary() + Y_aug[1].get_associated_binary(), None),
-                (1, 1 - m.Y[3].get_associated_binary() + Y_aug[1].get_associated_binary(), None),
+                (1, Y_aug[1].get_associated_binary() + \
+                 m.Y[4].get_associated_binary(), None),
+                (1, 1 - Y_aug[2].get_associated_binary() + \
+                 Y_aug[1].get_associated_binary(), None),
+                (1, 1 - m.Y[3].get_associated_binary() + \
+                 Y_aug[1].get_associated_binary(), None),
                 (1,
-                 Y_aug[2].get_associated_binary() + m.Y[3].get_associated_binary()
+                 Y_aug[2].get_associated_binary() + \
+                 m.Y[3].get_associated_binary()
                  + 1 - Y_aug[1].get_associated_binary(),
                  None),
-                (1, 1 - m.Y[1].get_associated_binary() + Y_aug[3].get_associated_binary(), None),
-                (1, 1 - m.Y[2].get_associated_binary() + Y_aug[3].get_associated_binary(), None),
+                (1, 1 - m.Y[1].get_associated_binary() + \
+                 Y_aug[3].get_associated_binary(), None),
+                (1, 1 - m.Y[2].get_associated_binary() + \
+                 Y_aug[3].get_associated_binary(), None),
                 (1,
-                 m.Y[1].get_associated_binary() + m.Y[2].get_associated_binary() + 1 - Y_aug[3].get_associated_binary(),
+                 m.Y[1].get_associated_binary() + \
+                 m.Y[2].get_associated_binary() + 1 - \
+                 Y_aug[3].get_associated_binary(),
                  None),
                 (None,
                  2 - 2 * (1 - Y_aug[2].get_associated_binary())
-                 - (m.Y[1].get_associated_binary() + Y_aug[3].get_associated_binary() + m.Y[2].get_associated_binary()),
+                 - (m.Y[1].get_associated_binary() + \
+                    Y_aug[3].get_associated_binary() + \
+                    m.Y[2].get_associated_binary()),
                  0),
                 (None,
-                 m.Y[1].get_associated_binary() + Y_aug[3].get_associated_binary() + m.Y[2].get_associated_binary()
+                 m.Y[1].get_associated_binary() + \
+                 Y_aug[3].get_associated_binary() + \
+                 m.Y[2].get_associated_binary()
                  - (1 + 2 * Y_aug[2].get_associated_binary()),
                  0)
             ], m.logic_to_linear.transformed_constraints)
@@ -275,8 +330,10 @@ def test_link_with_gdp_indicators(self):
         TransformationFactory('core.logical_to_linear').apply_to(m)
         _constrs_contained_within(
             self, [
-                (1, m.dd[1].indicator_var + m.dd[2].indicator_var + 1 - m.d1.indicator_var, None),
-                (None, m.d1.indicator_var + m.d2.indicator_var + m.dd[1].indicator_var + m.dd[2].indicator_var, 2)
+                (1, m.dd[1].indicator_var + m.dd[2].indicator_var + \
+                 1 - m.d1.indicator_var, None),
+                (None, m.d1.indicator_var + m.d2.indicator_var + \
+                 m.dd[1].indicator_var + m.dd[2].indicator_var, 2)
             ], m.logic_to_linear.transformed_constraints)
 
     def test_gdp_nesting(self):
@@ -288,13 +345,42 @@ def test_gdp_nesting(self):
         TransformationFactory('core.logical_to_linear').apply_to(m)
         _constrs_contained_within(
             self, [
-                (1, 1 - m.Y[1].get_associated_binary() + m.Y[2].get_associated_binary(), None),
+                (1, 1 - m.Y[1].get_associated_binary() + \
+                 m.Y[2].get_associated_binary(), None),
             ], m.disj_disjuncts[0].logic_to_linear.transformed_constraints)
         _constrs_contained_within(
             self, [
                 (1, 1 - m.Y[2].get_associated_binary(), 1),
             ], m.disj_disjuncts[1].logic_to_linear.transformed_constraints)
 
+    def test_transformed_components_on_parent_block(self):
+        m = ConcreteModel()
+        m.b = Block()
+        m.b.s = RangeSet(3)
+        m.b.Y = BooleanVar(m.b.s)
+        m.b.p = LogicalConstraint(expr=m.b.Y[1].implies(lor(m.b.Y[2], 
+                                                            m.b.Y[3])))
+        TransformationFactory('core.logical_to_linear').apply_to(m)
+
+        boolean_var = m.b.component("Y_asbinary")
+        self.assertIsInstance(boolean_var, Var)
+        notAVar = m.component("Y_asbinary")
+        self.assertIsNone(notAVar)
+
+        transBlock = m.b.component("logic_to_linear")
+        self.assertIsInstance(transBlock, Block)
+        notAThing = m.component("logic_to_linear")
+        self.assertIsNone(notAThing)
+
+        # check the constraints on the transBlock
+        _constrs_contained_within(
+            self, [
+                (1,
+                 m.b.Y[2].get_associated_binary() + \
+                 m.b.Y[3].get_associated_binary()
+                 + (1 - m.b.Y[1].get_associated_binary()),
+                 None)
+            ], m.b.logic_to_linear.transformed_constraints)        
 
 @unittest.skipUnless(sympy_available, "Sympy not available")
 class TestLogicalToLinearBackmap(unittest.TestCase):
@@ -308,6 +394,20 @@ def test_backmap(self):
         self.assertFalse(m.Y[2].value)
         self.assertIsNone(m.Y[3].value)
 
+    def test_backmap_hierarchical_model(self):
+        m = _generate_boolean_model(3)
+        m.b = Block()
+        m.b.Y = BooleanVar()
+        TransformationFactory('core.logical_to_linear').apply_to(m)
+        m.Y_asbinary[1].value = 1
+        m.Y_asbinary[2].value = 0
+        m.b.Y.get_associated_binary().value = 1
+        update_boolean_vars_from_binary(m)
+        self.assertTrue(m.Y[1].value)
+        self.assertFalse(m.Y[2].value)
+        self.assertIsNone(m.Y[3].value)
+        self.assertTrue(m.b.Y.value)
+
     def test_backmap_noninteger(self):
         m = _generate_boolean_model(2)
         TransformationFactory('core.logical_to_linear').apply_to(m)
@@ -315,7 +415,8 @@ def test_backmap_noninteger(self):
         update_boolean_vars_from_binary(m, integer_tolerance=0.1)
         self.assertTrue(m.Y[1].value)
         # Now try it without the tolerance set
-        with self.assertRaisesRegex(ValueError, r"Binary variable has non-\{0,1\} value"):
+        with self.assertRaisesRegex(ValueError, 
+                                    r"Binary variable has non-\{0,1\} value"):
             update_boolean_vars_from_binary(m)