def integrate(self, node_id: int, var: FNode) -> int:
        self.ub_cache = dict()
        self.lb_cache = dict()

        if self.reduce_strategy[0]:
            node_id = self.pool.diagram(node_id).reduce(
                method=self.method).root_id

        if logger.isEnabledFor(logging.DEBUG):
            self.pool.diagram(node_id).export_png(
                "log/integrate_{}_d_{}".format(node_id, var), pretty=True)

        if var.symbol_type() != BOOL and self.symbolic_integration_enabled:
            integrator = partial(self.symbolic_integrator, var)
            integrated = leaf_transform.transform_leaves(
                integrator, self.pool.diagram(node_id))
        else:
            integrated = node_id

        # self.export(self.pool.diagram(integrated), "integrated")
        result_id = self.resolve_lb_ub(integrated, var)
        # result_id = order.order(self.pool.diagram(self.resolve_lb_ub(integrated, var))).root_id

        self.ub_cache = None
        self.lb_cache = None

        if all(self.reduce_strategy):
            result_id = self.pool.diagram(result_id).reduce(
                method=self.method).root_id
        return result_id
    def resolve_lb_ub(
        self,
        node_id: int,
        var: FNode,
        ub: Optional[FNode] = None,
        lb: Optional[FNode] = None,
        prefix="",
    ) -> int:
        new_prefix = "  " + prefix
        method = self.method  # "fast_smt"
        # prefix = rl * "." + "({})({})({})".format(node_id, ub, lb)
        # print(prefix + " enter")

        logger.debug("%s resolve %s var=%s lb=%s ub=%s", prefix, node_id, var,
                     lb, ub)

        if self.cache_result:
            key = (var.symbol_name(), node_id, ub, lb)
            self.cache_calls += 1
            # print key, self.cache_calls, self.cache_hits
            if key in self.resolve_cache:
                # print("cache hit", self.cache_hits)
                self.cache_hits += 1
                # print("Cache hit for key={}".format(key))
                return self.resolve_cache[key]

            cache_result = partial(self.add_to_cache, key)
        else:
            cache_result = lambda r: r

        node = self.pool.get_node(node_id)
        # print "ub_lb_resolve node: {}, ub: {}, lb: {}, {} : {}".format(node, ub, lb, hash(str(ub)), hash(str(lb)))
        # leaf
        algebra = self.pool.algebra
        if node.is_terminal():
            if node.node_id == self.pool.zero_id:
                return self.pool.zero_id
            if var.symbol_type() == BOOL:
                return self.pool.terminal(
                    algebra.times(algebra.real(2), node.expression))

            if ub is None or lb is None:
                # TODO: to deal with unbounded constraints, we should either return 0 if we've seen bounds
                # or f(inf) if we haven't seen bounds
                return cache_result(self.pool.zero_id)
            else:
                # ub_sub = self.operator_to_bound(ub, var)
                # lb_sub = self.operator_to_bound(lb, var)

                if self.symbolic_integration_enabled:
                    raise NotImplementedError()
                else:
                    expression = self.concrete_integrate(
                        node.expression, var, lb, ub, prefix)

                # print "->", self.pool.get_node(res)
                return cache_result(self.pool.terminal(expression))
                # not leaf

        assert isinstance(node, InternalNode)
        if var in node.decision.variables:
            # Variable occurs in test

            if var.symbol_type() == BOOL:
                return self.pool.apply(Summation, node.child_true,
                                       node.child_false)

            var_coefficient = node.decision.inequality.coefficient(str(var))
            if var_coefficient > 0:
                # True branch is upper-bound
                ub_inequality = node.decision.inequality
                ub_branch = node.child_true
                lb_branch = node.child_false
            else:
                # False branch is upper-bound
                ub_inequality = node.decision.inequality.inverted()
                ub_branch = node.child_false
                lb_branch = node.child_true
            # ub_at_node = self.operator_to_bound(operator, var)
            # lb_at_node = self.operator_to_bound((~operator).to_canonical(), var)

            lb_inequality = ub_inequality.inverted()

            var_name = var.symbol_name()
            new_bound = self.operator_to_bound(ub_inequality, var_name)
            # lb_expr = self.operator_to_bound(lb_inequality, var_name)
            # consistency_test = simplify(lb_expr <= ub_expr)

            pass_ub = False
            if lb is not None:
                consistency_test = simplify(lb < new_bound)
                if consistency_test == FALSE():
                    # this branch is infeasible
                    ub_consistency = self.pool.zero_id
                    some_or_best_ub = self.pool.zero_id
                    pass_ub = True
                elif consistency_test == TRUE():
                    ub_consistency = self.pool.one_id
                else:
                    ub_consistency = self.pool.bool_test(
                        Decision(consistency_test))
            else:
                ub_consistency = self.pool.one_id

            if ub is not None and not pass_ub:
                tighter_ub_test = simplify(new_bound < ub)
                if tighter_ub_test == TRUE():
                    some_ub = self.pool.zero_id
                else:
                    some_ub = self.resolve_lb_ub(ub_branch,
                                                 var,
                                                 ub=ub,
                                                 lb=lb,
                                                 prefix=new_prefix)

                if tighter_ub_test == FALSE():
                    best_ub = self.pool.zero_id
                else:
                    best_ub = self.resolve_lb_ub(ub_branch,
                                                 var,
                                                 ub=new_bound,
                                                 lb=lb,
                                                 prefix=new_prefix)

                best_ub = (
                    self.pool.diagram(best_ub).reduce(method=method).root_id
                )  # RED
                some_ub = (
                    self.pool.diagram(some_ub).reduce(method=method).root_id
                )  # RED

                some_or_best_ub = self.pool.internal(Decision(tighter_ub_test),
                                                     best_ub, some_ub)
            elif not pass_ub:
                some_or_best_ub = self.resolve_lb_ub(ub_branch,
                                                     var,
                                                     ub=new_bound,
                                                     lb=lb,
                                                     prefix=new_prefix)

            pass_lb = False
            if ub is not None:
                consistency_test = simplify(new_bound < ub)
                if consistency_test == FALSE():
                    # this branch is infeasible
                    lb_consistency = self.pool.zero_id
                    some_or_best_lb = self.pool.zero_id
                    pass_lb = True
                    if consistency_test == TRUE():
                        lb_consistency = self.pool.one_id
                else:
                    lb_consistency = self.pool.bool_test(
                        Decision(consistency_test))
            else:
                lb_consistency = self.pool.one_id

            if lb is not None and not pass_lb:
                tighter_lb_test = simplify(new_bound > lb)
                if tighter_lb_test == TRUE():
                    some_lb = self.pool.zero_id
                else:
                    some_lb = self.resolve_lb_ub(lb_branch,
                                                 var,
                                                 ub=ub,
                                                 lb=lb,
                                                 prefix=new_prefix)

                if tighter_lb_test == FALSE():
                    best_lb = self.pool.zero_id
                else:
                    best_lb = self.resolve_lb_ub(lb_branch,
                                                 var,
                                                 ub=ub,
                                                 lb=new_bound,
                                                 prefix=new_prefix)

                best_lb = (
                    self.pool.diagram(best_lb).reduce(method=method).root_id
                )  # RED
                some_lb = (
                    self.pool.diagram(some_lb).reduce(method=method).root_id
                )  # RED

                some_or_best_lb = self.pool.internal(Decision(tighter_lb_test),
                                                     best_lb, some_lb)
            elif not pass_lb:
                some_or_best_lb = self.resolve_lb_ub(lb_branch,
                                                     var,
                                                     ub=ub,
                                                     lb=new_bound,
                                                     prefix=new_prefix)

            lb_branch = self.pool.apply(Multiplication, some_or_best_lb,
                                        lb_consistency)
            ub_branch = self.pool.apply(Multiplication, some_or_best_ub,
                                        ub_consistency)

            # print(prefix + " lb done")
            if self.reduce_strategy[1]:
                lb_branch = (
                    self.pool.diagram(lb_branch).reduce(method=method).root_id
                )  # RED
                ub_branch = (
                    self.pool.diagram(ub_branch).reduce(method=method).root_id
                )  # RED
            # self.export(res, "res{}_{}_{}".format(node_id, hash(str(ub)), hash(str(lb))))
            result = self.pool.apply(Summation, lb_branch, ub_branch)
            if self.reduce_strategy[2]:
                result = self.pool.diagram(result).reduce(
                    method=method).root_id
            return cache_result(result)
        else:
            true_branch_id = self.resolve_lb_ub(node.child_true,
                                                var,
                                                ub=ub,
                                                lb=lb)
            false_branch_id = self.resolve_lb_ub(node.child_false,
                                                 var,
                                                 ub=ub,
                                                 lb=lb)
            return cache_result(
                self.pool.internal(node.decision, true_branch_id,
                                   false_branch_id))