Returns True iff Other 
        (a) has the same domain,
        (b) the same or more specific 'upper' bound; and 
        (c) the same or more general 'lower' bound. 
        
        Approach: Looks for ways to rule out entailment.  If none are found, 
        returns True

def is_entailed_by(self, other):
        """
        Returns True iff Other 
        (a) has the same domain,
        (b) the same or more specific 'upper' bound; and 
        (c) the same or more general 'lower' bound. 
        
        Approach: Looks for ways to rule out entailment.  If none are found, 
        returns True
        """

        
        if not self.is_domain_equal(other):
            return False
        
        """        
        First we compare the similarity between upper bounds, 
         (1) if Self's upper bound is empty, Other's upper bound will always
            be as or more specific.
         (2) if Self's upper bound is a subset of Other's, Other will always
            be as or more specific.
            
         To prove the other has a more specific upper bound, we eliminate
            cases where it does not.  The first condition ensures the above
            two cases are not true.   Then, for each element in Other's upper
            bound (that is not in Self's),
            
         (3) if, for all elements in other's upper bound, we cannot find a member
           in the complete upper bound that is more general (has path from self[i]
           to other[j]), we return False, indicating Other does not entail
           Self.
        """
        domain = self.get_domain()
        self_full_upper = self.compute_upper_bound()
        if not (len(self.upper) == 0 \
             or other.upper.issuperset(self.upper)):
            for self_up in self_full_upper - other.upper:
                found = False
                # find a more specific member for each member
                # of self.upper that's not in other.upper
                for other_up in other.upper - self_full_upper:
                    if has_path(domain, self_up, other_up):
                        found = True
                        break
                if found:
                    break
                else:
                    return False  # none was found     
        """        
        Other must also share the same or more general 'lower' bound. 

         (3) if, for all elements in other's lower bound, we cannot find a member
           in the complete upper bound that is more general (has path from self[i]
           to other[j]), we stop.

        """
        if not (len(self.lower) == 0 \
            or self.lower.issubset(other.lower)):
            for self_lo in self.lower - other.lower:
                found = False
                for other_lo in other.lower - self.lower:
                    if has_path(domain, self_lo, other_lo):
                        found =True
                        break
                if found:
                    break
                else:
                    return False  # none was found
                    
        # if we got this far, we should have an entailment!
        return True