# 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/splitcondition.py
# class CombinedCondition(object):
# 
#     def __init__(self, variables: List[int], conditions: List[SplitCondition]):
#         self.variables = {v: CombinedVariableCondition(v, -np.inf, np.inf) for v in variables}
#         self.conditions = conditions
#         for condition in conditions:
#             self.variables[condition.splitting_variable] = self.variables[condition.splitting_variable].add_condition(condition)
#         if len(conditions) > 0:
#             self.splitting_variable = conditions[-1].splitting_variable
#         else:
#             self.splitting_variable = None
# 
#     def condition(self, X: np.ndarray) -> np.ndarray:
#         c = np.array([True] * len(X))
#         for variable in self.variables.keys():
#             c = c & (X[:, variable] > self.variables[variable].min_value) & (X[:, variable] <= self.variables[variable].max_value)
#         return c
# 
#     def __add__(self, other: SplitCondition):
#         return CombinedCondition(list(self.variables.keys()), self.conditions + [other])
# 
#     def most_recent_split_condition(self):
#         if len(self.conditions) == 0:
#             return None
#         else:
#             return self.conditions[-1]
# 
# class SplitCondition(object):
#     """
#     A representation of a split in feature space.
#     The two main components are:
# 
#         - splitting_variable: which variable is being split on
#         - splitting_value: the value being split on
#                            all values less than or equal to this go left, all values greater go right
# 
#     """
# 
#     def __init__(self, 
#                  splitting_variable: int, 
#                  splitting_value: float, 
#                  operator: Callable[[float, float], bool], 
#                  condition=None,
#                  carry_y_sum=None,
#                  carry_n_obsv=None):
#         self.splitting_variable = splitting_variable
#         self.splitting_value = splitting_value
#         self._condition = condition
#         self.operator = operator
# 
#         self.carry_y_sum = carry_y_sum
#         self.carry_n_obsv = carry_n_obsv
# 
#     def __str__(self):
#         return str(self.splitting_variable) + ": " + str(self.splitting_value)
# 
#     def __eq__(self, other: 'SplitCondition'):
#         return self.splitting_variable == other.splitting_variable and self.splitting_value == other.splitting_value and self.operator == other.operator


from copy import deepcopy
from typing import List, Optional
from bartpy.data import Data
from bartpy.splitcondition import CombinedCondition, SplitCondition
import numpy as np
     - Maintaining a record of which rows of the covariate matrix are in the split
     - Being able to easily access a `Data` object with the relevant rows
     - Applying `SplitConditions` to further break up the data
    """

    def __init__(self, data: Data, combined_condition: Optional[CombinedCondition]=None):
        self._data = data
        if combined_condition is None:
            combined_condition = CombinedCondition(self._data.X.variables, [])
        self._combined_condition = combined_condition

    @property
    def data(self):
        return self._data

    def combined_condition(self):
        return self._combined_condition

    def condition(self, X: np.ndarray=None) -> np.array:
        if X is None:
            return ~self._data.mask
        else:
            return self.out_of_sample_condition(X)

    def out_of_sample_condition(self, X: np.ndarray) -> np.ndarray:
        return self._combined_condition.condition(X)

    def out_of_sample_conditioner(self) -> CombinedCondition:
        return self._combined_condition


# 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/splitcondition.py
# class CombinedCondition(object):
# 
#     def __init__(self, variables: List[int], conditions: List[SplitCondition]):
#         self.variables = {v: CombinedVariableCondition(v, -np.inf, np.inf) for v in variables}
#         self.conditions = conditions
#         for condition in conditions:
#             self.variables[condition.splitting_variable] = self.variables[condition.splitting_variable].add_condition(condition)
#         if len(conditions) > 0:
#             self.splitting_variable = conditions[-1].splitting_variable
#         else:
#             self.splitting_variable = None
# 
#     def condition(self, X: np.ndarray) -> np.ndarray:
#         c = np.array([True] * len(X))
#         for variable in self.variables.keys():
#             c = c & (X[:, variable] > self.variables[variable].min_value) & (X[:, variable] <= self.variables[variable].max_value)
#         return c
# 
#     def __add__(self, other: SplitCondition):
#         return CombinedCondition(list(self.variables.keys()), self.conditions + [other])
# 
#     def most_recent_split_condition(self):
#         if len(self.conditions) == 0:
#             return None
#         else:
#             return self.conditions[-1]
# 
# class SplitCondition(object):
#     """
#     A representation of a split in feature space.
#     The two main components are:
# 
#         - splitting_variable: which variable is being split on
#         - splitting_value: the value being split on
#                            all values less than or equal to this go left, all values greater go right
# 
#     """
# 
#     def __init__(self, 
#                  splitting_variable: int, 
#                  splitting_value: float, 
#                  operator: Callable[[float, float], bool], 
#                  condition=None,
#                  carry_y_sum=None,
#                  carry_n_obsv=None):
#         self.splitting_variable = splitting_variable
#         self.splitting_value = splitting_value
#         self._condition = condition
#         self.operator = operator
# 
#         self.carry_y_sum = carry_y_sum
#         self.carry_n_obsv = carry_n_obsv
# 
#     def __str__(self):
#         return str(self.splitting_variable) + ": " + str(self.splitting_value)
# 
#     def __eq__(self, other: 'SplitCondition'):
#         return self.splitting_variable == other.splitting_variable and self.splitting_value == other.splitting_value and self.operator == other.operator


# Path: bartpy/split.py
from copy import deepcopy
from typing import List, Optional
from bartpy.data import Data
from bartpy.splitcondition import CombinedCondition, SplitCondition
import numpy as np

     - Maintaining a record of which rows of the covariate matrix are in the split
     - Being able to easily access a `Data` object with the relevant rows
     - Applying `SplitConditions` to further break up the data
    """

    def __init__(self, data: Data, combined_condition: Optional[CombinedCondition]=None):
        self._data = data
        if combined_condition is None:
            combined_condition = CombinedCondition(self._data.X.variables, [])
        self._combined_condition = combined_condition

    @property
    def data(self):
        return self._data

    def combined_condition(self):
        return self._combined_condition

    def condition(self, X: np.ndarray=None) -> np.array:
        if X is None:
            return ~self._data.mask
        else:
            return self.out_of_sample_condition(X)

    def out_of_sample_condition(self, X: np.ndarray) -> np.ndarray:
        return self._combined_condition.condition(X)

    def out_of_sample_conditioner(self) -> CombinedCondition:
        return self._combined_condition


    def __add__(self, other: SplitCondition):