# Path: bartpy/data.py
# class Data(object):
#     """
#     Encapsulates the data within a split of feature space.
#     Primarily used to cache computations on the data for better performance
# 
#     Parameters
#     ----------
#     X: np.ndarray
#         The subset of the covariate matrix that falls into the split
#     y: np.ndarray
#         The subset of the target array that falls into the split
#     normalize: bool
#         Whether to map the target into -0.5, 0.5
#     cache: bool
#         Whether to cache common values.
#         You really only want to turn this off if you're not going to the resulting object for anything (e.g. when testing)
#     """
# 
#     def __init__(self,
#                  X: np.ndarray,
#                  y: np.ndarray,
#                  mask: Optional[np.ndarray]=None,
#                  normalize: bool=False,
#                  unique_columns: List[int]=None,
#                  splittable_variables: Optional[List[Optional[bool]]]=None,
#                  y_sum: float=None,
#                  n_obsv: int=None):
# 
#         if mask is None:
#             mask = np.zeros_like(y).astype(bool)
#         self._mask: np.ndarray = mask
# 
#         if n_obsv is None:
#             n_obsv = (~self.mask).astype(int).sum()
# 
#         self._n_obsv = n_obsv
# 
#         self._X = CovariateMatrix(X, mask, n_obsv, unique_columns, splittable_variables)
#         self._y = Target(y, mask, n_obsv, normalize, y_sum)
# 
#     @property
#     def y(self) -> Target:
#         return self._y
# 
#     @property
#     def X(self) -> CovariateMatrix:
#         return self._X
# 
#     @property
#     def mask(self) -> np.ndarray:
#         return self._mask
# 
#     def update_y(self, y: np.ndarray) -> None:
#         self._y.update_y(y)
# 
#     def __add__(self, other: SplitCondition) -> 'Data':
#         updated_mask = self.X.update_mask(other)
# 
#         return Data(self.X.values,
#                     self.y.values,
#                     updated_mask,
#                     normalize=False,
#                     unique_columns=self._X._unique_columns,
#                     splittable_variables=self._X._splittable_variables,
#                     y_sum=other.carry_y_sum,
#                     n_obsv=other.carry_n_obsv)
# 
# Path: bartpy/split.py
# class Split:
#     def __init__(self, data: Data, combined_condition: Optional[CombinedCondition]=None):
#     def data(self):
#     def combined_condition(self):
#     def condition(self, X: np.ndarray=None) -> np.array:
#     def out_of_sample_condition(self, X: np.ndarray) -> np.ndarray:
#     def out_of_sample_conditioner(self) -> CombinedCondition:
#     def __add__(self, other: SplitCondition):
#     def most_recent_split_condition(self) -> Optional[SplitCondition]:


from typing import Union, Tuple
from bartpy.data import Data
from bartpy.split import Split, SplitCondition
    def __init__(self, split: Split, depth=0, value=0.0):
        self._value = value
        super().__init__(split, depth, None, None)

    def set_value(self, value: float) -> None:
        self._value = value

    @property
    def current_value(self):
        return self._value

    def predict(self) -> float:
        return self.current_value

    def is_splittable(self) -> bool:
        return self.data.X.is_at_least_one_splittable_variable()


class DecisionNode(TreeNode):
    """
    A `DecisionNode` encapsulates internal node in the tree
    Unlike a `LeafNode`, it contains very little actual logic beyond tying the tree together
    """

    def __init__(self, split: Split, left_child_node: TreeNode, right_child_node: TreeNode, depth=0):
        super().__init__(split, depth, left_child_node, right_child_node)

    def is_prunable(self) -> bool:
        return type(self.left_child) == LeafNode and type(self.right_child) == LeafNode


# Path: bartpy/data.py
# class Data(object):
#     """
#     Encapsulates the data within a split of feature space.
#     Primarily used to cache computations on the data for better performance
# 
#     Parameters
#     ----------
#     X: np.ndarray
#         The subset of the covariate matrix that falls into the split
#     y: np.ndarray
#         The subset of the target array that falls into the split
#     normalize: bool
#         Whether to map the target into -0.5, 0.5
#     cache: bool
#         Whether to cache common values.
#         You really only want to turn this off if you're not going to the resulting object for anything (e.g. when testing)
#     """
# 
#     def __init__(self,
#                  X: np.ndarray,
#                  y: np.ndarray,
#                  mask: Optional[np.ndarray]=None,
#                  normalize: bool=False,
#                  unique_columns: List[int]=None,
#                  splittable_variables: Optional[List[Optional[bool]]]=None,
#                  y_sum: float=None,
#                  n_obsv: int=None):
# 
#         if mask is None:
#             mask = np.zeros_like(y).astype(bool)
#         self._mask: np.ndarray = mask
# 
#         if n_obsv is None:
#             n_obsv = (~self.mask).astype(int).sum()
# 
#         self._n_obsv = n_obsv
# 
#         self._X = CovariateMatrix(X, mask, n_obsv, unique_columns, splittable_variables)
#         self._y = Target(y, mask, n_obsv, normalize, y_sum)
# 
#     @property
#     def y(self) -> Target:
#         return self._y
# 
#     @property
#     def X(self) -> CovariateMatrix:
#         return self._X
# 
#     @property
#     def mask(self) -> np.ndarray:
#         return self._mask
# 
#     def update_y(self, y: np.ndarray) -> None:
#         self._y.update_y(y)
# 
#     def __add__(self, other: SplitCondition) -> 'Data':
#         updated_mask = self.X.update_mask(other)
# 
#         return Data(self.X.values,
#                     self.y.values,
#                     updated_mask,
#                     normalize=False,
#                     unique_columns=self._X._unique_columns,
#                     splittable_variables=self._X._splittable_variables,
#                     y_sum=other.carry_y_sum,
#                     n_obsv=other.carry_n_obsv)
# 
# Path: bartpy/split.py
# class Split:
#     def __init__(self, data: Data, combined_condition: Optional[CombinedCondition]=None):
#     def data(self):
#     def combined_condition(self):
#     def condition(self, X: np.ndarray=None) -> np.array:
#     def out_of_sample_condition(self, X: np.ndarray) -> np.ndarray:
#     def out_of_sample_conditioner(self) -> CombinedCondition:
#     def __add__(self, other: SplitCondition):
#     def most_recent_split_condition(self) -> Optional[SplitCondition]:


# Path: bartpy/node.py
from typing import Union, Tuple
from bartpy.data import Data
from bartpy.split import Split, SplitCondition

    def __init__(self, split: Split, depth=0, value=0.0):
        self._value = value
        super().__init__(split, depth, None, None)

    def set_value(self, value: float) -> None:
        self._value = value

    @property
    def current_value(self):
        return self._value

    def predict(self) -> float:
        return self.current_value

    def is_splittable(self) -> bool:
        return self.data.X.is_at_least_one_splittable_variable()


class DecisionNode(TreeNode):
    """
    A `DecisionNode` encapsulates internal node in the tree
    Unlike a `LeafNode`, it contains very little actual logic beyond tying the tree together
    """

    def __init__(self, split: Split, left_child_node: TreeNode, right_child_node: TreeNode, depth=0):
        super().__init__(split, depth, left_child_node, right_child_node)

    def is_prunable(self) -> bool:
        return type(self.left_child) == LeafNode and type(self.right_child) == LeafNode


    def most_recent_split_condition(self) -> SplitCondition: