MULTIPROCESS_FLAG = multiprocessing_flag_reader() def config_path(flag=None):
def magic_num_reader():
def multiprocessing_flag_reader():
def envvar_warning_flag_reader():
def custom_model_path_reader():
def cpu_gpu_reader():
def cpu_gpu_check():
def tf_patch():
def tf_unpatch():
def __tf_patches(method='patch'):
    def __master_patch(path, diffpatch):
MAGIC_NUMBER = magic_num_reader()
MULTIPROCESS_FLAG = multiprocessing_flag_reader()
ENVVAR_WARN_FLAG = envvar_warning_flag_reader()
CUSTOM_MODEL_PATH = custom_model_path_reader() class NeuralNetMaster(ABC):
    """
    Top-level class for an astroNN neural network

    :ivar name: Full English name
    :ivar _model_type: Type of model
    :ivar _model_identifier: Unique model identifier, by default using class name as ID
    :ivar _implementation_version: Version of the model
    :ivar _python_info: Placeholder to store python version used for debugging purpose
    :ivar _astronn_ver: astroNN version detected
    :ivar _keras_ver: Keras version detected
    :ivar _tf_ver: Tensorflow version detected
    :ivar currentdir: Current directory of the terminal
    :ivar folder_name: Folder name to be saved
    :ivar fullfilepath: Full file path
    :ivar batch_size: Batch size for training, by default 64
    :ivar autosave: Boolean to flag whether autosave model or not

    :ivar task: Task
    :ivar lr: Learning rate
    :ivar max_epochs: Maximum epochs
    :ivar val_size: Validation set size in percentage
    :ivar val_num: Validation set autual number

    :ivar beta_1: Exponential decay rate for the 1st moment estimates for optimization algorithm
    :ivar beta_2: Exponential decay rate for the 2nd moment estimates for optimization algorithm
    :ivar optimizer_epsilon: A small constant for numerical stability for optimization algorithm
    :ivar optimizer: Placeholder for optimizer

    :ivar targetname: Full name for every output neurones

    :History:
        | 2017-Dec-23 - Written - Henry Leung (University of Toronto)
        | 2018-Jan-05 - Updated - Henry Leung (University of Toronto)
    """

    def __init__(self):
        self.name = None
        self._model_type = None
        self._model_identifier = self.__class__.__name__  # No effect, will do when save
        self._implementation_version = None
        self._python_info = sys.version
        self._astronn_ver = astroNN.__version__
        self._keras_ver = tfk.__version__  # tensorflow.keras version
        self._tf_ver = tf.__version__
        self.currentdir = os.getcwd()
        self.folder_name = None
        self.fullfilepath = None
        self.batch_size = 64
        self.autosave = False

        # Hyperparameter
        self.task = None
        self.lr = None
        self.max_epochs = None
        self.val_size = None
        self.val_num = None

        # optimizer parameter
        self.beta_1 = 0.9  # exponential decay rate for the 1st moment estimates for optimization algorithm
        self.beta_2 = 0.999  # exponential decay rate for the 2nd moment estimates for optimization algorithm
        self.optimizer_epsilon = epsilon()  # a small constant for numerical stability for optimization algorithm
        self.optimizer = None

        # Keras API
        self.verbose = 2
        self.keras_model = None
        self.keras_model_predict = None
        self.history = None
        self.metrics = None
        self.callbacks = None
        self.__callbacks = None  # for internal default callbacks usage only
        self._output_loss = None

        self.input_normalizer = None
        self.labels_normalizer = None
        self.training_generator = None
        self.validation_generator = None

        self.input_norm_mode = None
        self.labels_norm_mode = None
        self.input_mean = None
        self.input_std = None
        self.labels_mean = None
        self.labels_std = None
        self.input_names = None
        self.output_names = None

        self._input_shape = None
        self._labels_shape = None

        self.num_train = None
        self.train_idx = None
        self.val_idx = None
        self.targetname = None
        self.history = None
        self.virtual_cvslogger = None
        self.hyper_txt = None

        cpu_gpu_check()

    def __str__(self):
        return f"Name: {self.name}\nModel Type: {self._model_type}\nModel ID: {self._model_identifier}"

    @property
    def has_model(self):
        """
        Get whether the instance has a model, usually a model is created after you called train(), the instance
        will has no model if you did not call train()

        :return: bool
        :History: 2018-May-21 - Written - Henry Leung (University of Toronto)
        """
        if self.keras_model is None:
            return False
        else:
            return True

    def has_model_check(self):
        if self.has_model is False:
            raise AttributeError("No model found in this instance, the common problem is you did not train a model")

    def custom_train_step(self, *args):
        raise NotImplementedError
    
    def custom_test_step(self, *args):
        raise NotImplementedError
    
    @abstractmethod
    def train(self, *args):
        raise NotImplementedError

    @abstractmethod
    def train_on_batch(self, *args):
        raise NotImplementedError

    @abstractmethod
    def test(self, *args):
        raise NotImplementedError

    @abstractmethod
    def evaluate(self, *args):
        raise NotImplementedError

    @abstractmethod
    def model(self):
        raise NotImplementedError

    @abstractmethod
    def post_training_checklist_child(self):
        raise NotImplementedError
    
    def _tensor_dict_sanitize(self, tensor_dict, names_list):
        """
        Remove extra tensors
        
        :param tensor_dict: Dictionary of array or tensors
        :type tensor_dict: dict
        :param names_list: List of names
        :type names_list: list
        :return: Sanitized dict
        """
        for tensor_name in [n for n in tensor_dict.keys() if n not in names_list]:
                tensor_dict.pop(tensor_name)
        
        return tensor_dict


    def pre_training_checklist_master(self, input_data, labels):

        # handle named inputs/outputs first
        try:
            self.input_names = list(input_data.keys())
        except AttributeError:
            self.input_names = ["input"]  # default input name in all astroNN models
            input_data = {"input": input_data}
        try:
            self.output_names = list(labels.keys())
        except AttributeError:
            self.output_names = ["output"]  # default input name in all astroNN models
            labels = {"output": labels}

        # assert all named input has the same number of data points
        # TODO: add detail error msg, add test
        if not all(input_data["input"].shape[0] == input_data[name].shape[0] for name in self.input_names):
            raise IndexError("all inputs should contain same number of data point")
        if not all(labels["output"].shape[0] == labels[name].shape[0] for name in self.output_names):
            raise IndexError("all outputs should contain same number of data point")

        if self.val_size is None:
            self.val_size = 0

        self.val_num = int(input_data["input"].shape[0] * self.val_size)
        self.num_train = input_data["input"].shape[0] - self.val_num

        # Assuming the convolutional layer immediately after input layer
        # only require if it is new, no need for fine-tuning
        # in case you read this for dense network, use Flattener as first layer in your network to flatten it
        if self._input_shape is None:
            self._input_shape = {}
            for name in self.input_names:
                data_ndim = input_data[name].ndim
                if data_ndim == 1:
                    self._input_shape.update({name: (1, 1,)})
                elif data_ndim == 2:
                    self._input_shape.update({name: (input_data[name].shape[1], 1,)})
                elif data_ndim == 3:
                    self._input_shape.update({name: (input_data[name].shape[1], input_data[name].shape[2], 1,)})
                elif data_ndim == 4:
                    self._input_shape.update({name: (input_data[name].shape[1], input_data[name].shape[2],
                                                     input_data[name].shape[3],)})

            # zeroth dim should always be number of data
            self._labels_shape = {}
            for name in self.output_names:
                data_ndim = labels[name].ndim
                if data_ndim == 1:
                    self._labels_shape.update({name: 1})
                elif data_ndim == 2:
                    self._labels_shape.update({name: (labels[name].shape[1])})
                elif data_ndim == 3:
                    self._labels_shape.update({name: (labels[name].shape[1], labels[name].shape[2])})
                elif data_ndim == 4:
                    self._labels_shape.update({name: (labels[name].shape[1], labels[name].shape[2], labels[name].shape[3])})

        print(f'Number of Training Data: {self.num_train}, Number of Validation Data: {self.val_num}')

        return input_data, labels

    def pre_testing_checklist_master(self, input_data):
        if type(input_data) is not dict:
            input_data = {self.input_names[0]: np.atleast_2d(input_data)}
        else:
            for name in input_data.keys():
                input_data.update({name: np.atleast_2d(input_data[name])})
        return input_data

    def post_training_checklist_master(self):
        pass

    def save(self, name=None, model_plot=False):
        """
        Save the model to disk

        :param name: Folder name to be saved
        :type name: string
        :param model_plot: True to plot model too
        :type model_plot: boolean
        :return: A saved folder on disk
        """
        self.has_model_check()
        # Only generate a folder automatically if no name provided
        if self.folder_name is None and name is None:
            self.folder_name = folder_runnum()
        elif name is not None:
            self.folder_name = name

        # if foldername provided, then create a directory, if exist append something to avoid overwrite
        if not os.path.exists(os.path.join(self.currentdir, self.folder_name)):
            os.makedirs(os.path.join(self.currentdir, self.folder_name))
        else:
            i_back = 2
            while True:
                if not os.path.exists(os.path.join(self.currentdir, self.folder_name + f'_{i_back}')):
                    break
                i_back += 1
            new_folder_name_temp = self.folder_name + f'_{i_back}'
            warnings.warn(f'To prevent your model being overwritten, your folder name changed from {self.folder_name} '
                          f'to {new_folder_name_temp}', UserWarning)
            self.folder_name = new_folder_name_temp
            os.makedirs(os.path.join(self.currentdir, self.folder_name))

        self.fullfilepath = os.path.join(self.currentdir, self.folder_name + os.sep)

        txt_file_path = self.fullfilepath + 'hyperparameter.txt'
        if os.path.isfile(txt_file_path):
            self.hyper_txt = open(txt_file_path, 'a')
            self.hyper_txt.write("\n")
            self.hyper_txt.write("======Another Run======")
        else:
            self.hyper_txt = open(txt_file_path, 'w')
        self.hyper_txt.write(f"Model: {self.name} \n")
        self.hyper_txt.write(f"Model Type: {self._model_type} \n")
        self.hyper_txt.write(f"astroNN identifier: {self._model_identifier} \n")
        self.hyper_txt.write(f"Python Version: {self._python_info} \n")
        self.hyper_txt.write(f"astroNN Version: {self._astronn_ver} \n")
        self.hyper_txt.write(f"Keras Version: {self._keras_ver} \n")
        self.hyper_txt.write(f"Tensorflow Version: {self._tf_ver} \n")
        self.hyper_txt.write(f"Folder Name: {self.folder_name} \n")
        self.hyper_txt.write(f"Batch size: {self.batch_size} \n")
        self.hyper_txt.write(f"Optimizer: {self.optimizer.__class__.__name__} \n")
        self.hyper_txt.write(f"Maximum Epochs: {self.max_epochs} \n")
        self.hyper_txt.write(f"Learning Rate: {self.lr} \n")
        self.hyper_txt.write(f"Validation Size: {self.val_size} \n")
        self.hyper_txt.write(f"Input Shape: {self._input_shape} \n")
        self.hyper_txt.write(f"Label Shape: {self._labels_shape} \n")
        self.hyper_txt.write(f"Number of Training Data: {self.num_train} \n")
        self.hyper_txt.write(f"Number of Validation Data: {self.val_num} \n")

        if model_plot is True:
            self.plot_model()

        self.post_training_checklist_child()

        if self.virtual_cvslogger is not None:  # in case you save without training, so cvslogger is None
            self.virtual_cvslogger.savefile(folder_name=self.folder_name)

    def plot_model(self, name='model.png', show_shapes=True, show_layer_names=True, rankdir='TB'):
        """
        Plot model architecture with pydot and graphviz

        :param name: file name to be saved with extension, .png is recommended
        :type name: str
        :param show_shapes: whether show shape in model plot
        :type show_shapes: bool
        :param show_layer_names: whether to display layer names
        :type show_layer_names: bool
        :param rankdir: a string specifying the format of the plot, 'TB' for vertical or 'LR' for horizontal plot
        :type rankdir: bool
        :return: No return but will save the model architecture as png to disk
        """
        self.has_model_check()

        try:
            if self.fullfilepath is not None:
                plot_model(self.keras_model, show_shapes=show_shapes, to_file=os.path.join(self.fullfilepath, name),
                           show_layer_names=show_layer_names, rankdir=rankdir)
            else:
                plot_model(self.keras_model, show_shapes=show_shapes, to_file=name, show_layer_names=show_layer_names,
                           rankdir=rankdir)
        except ImportError or ModuleNotFoundError:
            warnings.warn('Skipped plot_model! graphviz and pydot_ng are required to plot the model architecture',
                          UserWarning)
            pass

    def hessian(self, x=None, mean_output=False, mc_num=1, denormalize=False):
        """
        | Calculate the hessian of output to input
        |
        | Please notice that the de-normalize (if True) assumes the output depends on the input data first orderly
        | in which the hessians does not depends on input scaling and only depends on output scaling
        |
        | The hessians can be all zeros and the common cause is you did not use any activation or
        | activation that is still too linear in some sense like ReLU.

        :param x: Input Data
        :type x: ndarray
        :param mean_output: False to get all hessian, True to get the mean
        :type mean_output: boolean
        :param mc_num: Number of monte carlo integration
        :type mc_num: int
        :param denormalize: De-normalize diagonal part of Hessian
        :type denormalize: bool
        :return: An array of Hessian
        :rtype: ndarray
        :History: 2018-Jun-14 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()

        if x is None:
            raise ValueError('Please provide data to calculate the jacobian')

        if mc_num < 1 or isinstance(mc_num, float):
            raise ValueError('mc_num must be a positive integer')

        if self.input_normalizer is not None:
            x_data = self.input_normalizer.normalize({"input": x}, calc=False)
            x_data = x_data['input']
        else:
            # Prevent shallow copy issue
            x_data = np.array(x)
            x_data -= self.input_mean
            x_data /= self.input_std

        _model = None
        try:
            input_tens = self.keras_model_predict.get_layer("input").input
            output_tens = self.keras_model_predict.get_layer("output").output
            input_shape_expectation = self.keras_model_predict.get_layer("input").input_shape
            output_shape_expectation = self.keras_model_predict.get_layer("output").output_shape
            _model = self.keras_model_predict
        except AttributeError:
            input_tens = self.keras_model.get_layer("input").input
            output_tens = self.keras_model.get_layer("output").output
            input_shape_expectation = self.keras_model.get_layer("input").input_shape
            output_shape_expectation = self.keras_model.get_layer("output").output_shape
            _model = self.keras_model
        except ValueError:
            raise ValueError("astroNN expects input layer is named as 'input' and output layer is named as 'output', "
                             "but None is found.")

        if len(input_shape_expectation) == 1:
            input_shape_expectation = input_shape_expectation[0]

        # just in case only 1 data point is provided and mess up the shape issue
        if len(input_shape_expectation) == 3:
            x_data = np.atleast_3d(x_data)
        elif len(input_shape_expectation) == 4:
            if len(x_data.shape) < 4:
                x_data = x_data[:, :, :, np.newaxis]
        else:
            raise ValueError('Input data shape do not match neural network expectation')

        total_num = x_data.shape[0]

        input_dim = len(np.squeeze(np.ones(input_shape_expectation[1:])).shape)
        output_dim = len(np.squeeze(np.ones(output_shape_expectation[1:])).shape)
        if input_dim > 3 or output_dim > 3:
            raise ValueError("Unsupported data dimension")

        xtensor = tf.Variable(x_data)

        with tf.GradientTape(watch_accessed_variables=False) as tape:
            tape.watch(xtensor)
            with tf.GradientTape() as dtape:
                dtape.watch(xtensor)
                temp = _model(xtensor)
            jacobian = tf.squeeze(dtape.batch_jacobian(temp, xtensor))

        start_time = time.time()

        hessian = tf.squeeze(tape.batch_jacobian(jacobian, xtensor))

        if np.all(hessian == 0.):  # warn user about not so linear activation like ReLU will get all zeros
            warnings.warn(
                'The hessians is detected to be all zeros. The common cause is you did not use any activation or '
                'activation that is still too linear in some sense like ReLU.', UserWarning)

        if mean_output is True:
            hessians_master = tf.reduce_mean(hessian, axis=0).numpy()
        else:
            hessians_master = hessian.numpy()

        if denormalize:  # no need to denorm input scaling because of we assume first order dependence
            if self.labels_std is not None:
                try:
                    hessians_master = hessians_master * self.labels_std
                except ValueError:
                    hessians_master = hessians_master * self.labels_std.reshape(-1, 1)

        print(f'Finished hessian calculation, {(time.time() - start_time):.{2}f} seconds elapsed')
        return hessians_master


    def jacobian(self, x=None, mean_output=False, mc_num=1, denormalize=False):
        """
        | Calculate jacobian of gradient of output to input high performance calculation update on 15 April 2018
        |
        | Please notice that the de-normalize (if True) assumes the output depends on the input data first orderly
        | in which the equation is simply jacobian divided the input scaling, usually a good approx. if you use ReLU all the way

        :param x: Input Data
        :type x: ndarray
        :param mean_output: False to get all jacobian, True to get the mean
        :type mean_output: boolean
        :param mc_num: Number of monte carlo integration
        :type mc_num: int
        :param denormalize: De-normalize Jacobian
        :type denormalize: bool
        :return: An array of Jacobian
        :rtype: ndarray
        :History:
            | 2017-Nov-20 - Written - Henry Leung (University of Toronto)
            | 2018-Apr-15 - Updated - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        if x is None:
            raise ValueError('Please provide data to calculate the jacobian')

        if mc_num < 1 or isinstance(mc_num, float):
            raise ValueError('mc_num must be a positive integer')

        if self.input_normalizer is not None:
            x_data = self.input_normalizer.normalize({"input": x}, calc=False)
            x_data = x_data['input']
        else:
            # Prevent shallow copy issue
            x_data = np.array(x)
            x_data -= self.input_mean
            x_data /= self.input_std

        _model = None
        try:
            input_tens = self.keras_model_predict.get_layer("input").input
            output_tens = self.keras_model_predict.get_layer("output").output
            input_shape_expectation = self.keras_model_predict.get_layer("input").input_shape
            output_shape_expectation = self.keras_model_predict.get_layer("output").output_shape
            _model = self.keras_model_predict
        except AttributeError:
            input_tens = self.keras_model.get_layer("input").input
            output_tens = self.keras_model.get_layer("output").output
            input_shape_expectation = self.keras_model.get_layer("input").input_shape
            output_shape_expectation = self.keras_model.get_layer("output").output_shape
            _model = self.keras_model
        except ValueError:
            raise ValueError("astroNN expects input layer is named as 'input' and output layer is named as 'output', "
                             "but None is found.")

        if len(input_shape_expectation) == 1:
            input_shape_expectation = input_shape_expectation[0]

        # just in case only 1 data point is provided and mess up the shape issue
        if len(input_shape_expectation) == 3:
            x_data = np.atleast_3d(x_data)
        elif len(input_shape_expectation) == 4:
            if len(x_data.shape) < 4:
                x_data = x_data[:, :, :, np.newaxis]
        else:
            raise ValueError('Input data shape do not match neural network expectation')

        total_num = x_data.shape[0]

        #TODO: move this to master??
        input_dim = len(np.squeeze(np.ones(input_shape_expectation[1:])).shape)
        output_dim = len(np.squeeze(np.ones(output_shape_expectation[1:])).shape)
        if input_dim > 3 or output_dim > 3:
            raise ValueError("Unsupported data dimension")

        xtensor = tf.Variable(x_data)

        with tf.GradientTape(watch_accessed_variables=False) as tape:
            tape.watch(xtensor)
            temp = _model(xtensor)

        start_time = time.time()

        jacobian = tf.squeeze(tape.batch_jacobian(temp, xtensor))

        if mean_output is True:
            jacobian_master = tf.reduce_mean(jacobian, axis=0).numpy()
        else:
            jacobian_master = jacobian.numpy()

        if denormalize:
            if self.input_std is not None:
                jacobian_master = jacobian_master / np.squeeze(self.input_std)

            if self.labels_std is not None:
                try:
                    jacobian_master = jacobian_master * self.labels_std
                except ValueError:
                    jacobian_master = jacobian_master * self.labels_std.reshape(-1, 1)

        print(f'Finished all gradient calculation, {(time.time() - start_time):.{2}f} seconds elapsed')

        return jacobian_master

    def plot_dense_stats(self):
        """
        Plot dense layers weight statistics

        :return: A plot
        :History: 2018-May-12 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        dense_list = []
        for counter, layer in enumerate(self.keras_model.layers):
            if isinstance(layer, tfk.layers.Dense):
                dense_list.append(counter)

        denses = np.array(self.keras_model.layers)[dense_list]
        fig, ax = plt.subplots(1, figsize=(15, 10), dpi=100)
        for counter, dense in enumerate(denses):
            weight_temp = np.array(dense.get_weights())[0].flatten()
            ax.hist(weight_temp, 200, density=True, range=(-2., 2.), alpha=0.7,
                    label=f'Dense Layer {counter}, max: {weight_temp.max():.{2}f}, min: {weight_temp.min():.{2}f}, '
                          f'mean: {weight_temp.mean():.{2}f}, std: {weight_temp.std():.{2}f}')
        fig.suptitle(f'Dense Layers Weight Statistics of {self.folder_name}', fontsize=17)
        ax.set_xlabel('Weights', fontsize=17)
        ax.set_ylabel('Normalized Distribution', fontsize=17)
        ax.minorticks_on()
        ax.tick_params(labelsize=15, width=3, length=10, which='major')
        ax.tick_params(width=1.5, length=5, which='minor')
        ax.legend(loc='best', fontsize=15)
        fig.tight_layout(rect=[0, 0.00, 1, 0.96])
        fig.show()

        return fig

    @property
    def output_shape(self):
        """
        Get output shape of the prediction model

        :return: output shape expectation
        :rtype: tuple
        :History: 2018-May-19 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        try:
            return self.keras_model_predict.output_shape
        except AttributeError:
            return self.keras_model.output_shape

    @property
    def input_shape(self):
        """
        Get input shape of the prediction model

        :return: input shape expectation
        :rtype: tuple
        :History: 2018-May-21 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        try:
            return self.keras_model_predict.input_shape
        except AttributeError:
            return self.keras_model.input_shape

    def get_weights(self):
        """
        Get all model weights

        :return: weights arrays
        :rtype: ndarray
        :History: 2018-May-23 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        return self.keras_model.get_weights()

    def summary(self):
        """
        Get model summary

        :return: None, just print
        :History: 2018-May-23 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        return self.keras_model.summary()

    def get_config(self):
        """
        Get model configuration as a dictionary

        :return: dict
        :History: 2018-May-23 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        return self.keras_model.get_config()

    def save_weights(self, filename=_astroNN_MODEL_NAME, overwrite=True):
        """
        Save model weights as .h5

        :param filename: Filename of .h5 to be saved
        :type filename: str
        :param overwrite: whether to overwrite
        :type overwrite: bool
        :return: None, a .h5 file will be saved
        :History: 2018-May-23 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        print('==========================')
        print('This is a remainder that saving weights to h5, you might have difficult to '
              'load it back and cannot be used with astroNN probably')
        print('==========================')
        if self.fullfilepath is not None:
            return self.keras_model.save_weights(str(os.path.join(self.fullfilepath, filename)), overwrite=overwrite)
        else:
            return self.keras_model.save_weights(filename, overwrite=overwrite)

    @property
    def uses_learning_phase(self):
        """
        To determine whether the model depends on keras learning flag. If False, then setting learning phase will not
        affect the model

        :return: the boolean to indicate keras learning flag dependence of the model
        :rtype: bool
        :History: 2018-Jun-03 - Written - Henry Leung (University of Toronto)
        """
        self.has_model_check()
        return any([getattr(x, '_uses_learning_phase', False) for x in self.keras_model.outputs])

    def get_layer(self, *args, **kwargs):
        """
        get_layer() method of tensorflow
        """
        return self.keras_model.get_layer(*args, **kwargs)

    def flush(self):
        """
        | Experimental, I don't think it works
        | Flush GPU memory from tensorflow

        :History: 2018-Jun-19 - Written - Henry Leung (University of Toronto)
        """
        tfk.backend.clear_session()

    def transfer_weights(self, model):
        """
        Transfer weight of a model to current model if possible
        # TODO: remove layers after successful transfer so wont mix up?
        # TODO: seems like the weights still get trained?

        :param model: astroNN model
        :type model: astroNN.model.NeuralNetMaster
        :return: bool
        :History: 2022-Mar-06 - Written - Henry Leung (University of Toronto)
        """
        counter = 0
        transferred = []
        for new_l in self.keras_model.layers:
            for l in model.keras_model.layers:
                try:
                    new_l.set_weights(l.get_weights())
                    new_l.trainable = False
                    counter += 1
                    transferred.append(l.name)
                    break
                except ValueError:
                    pass
        
        if counter == 0:
            warnings.warn("None of the layers' weights are successfully transfered due to shape incompatibility.")
        else:
            # self.compile()
            print(f"Successfully transferred: {transferred}") class VirutalCSVLogger(Callback):
    """
    A modification of keras' CSVLogger, but not actually write a file until you call method to save

    :param filename: filename of the log to be saved on disk
    :type filename: str
    :param separator: separator of fields
    :type separator: str
    :param append: whether allow append or not
    :type append: bool
    :return: callback instance
    :rtype: object
    :History:
        | 2018-Feb-22 - Written - Henry Leung (University of Toronto)
        | 2018-Mar-12 - Update - Henry Leung (University of Toronto)
    """

    def __init__(self, filename='training_history.csv', separator=',', append=False):
        self.sep = separator
        self.filename = filename
        self.append = append
        self.writer = None
        self.keys = None
        self.append_header = True
        self.csv_file = None
        self.epoch = []
        self.history = {}
        super().__init__()

    def on_epoch_end(self, epoch, logs=None):
        logs = logs or {}
        self.epoch.append(epoch)
        for k, v in logs.items():
            self.history.setdefault(k, []).append(v)

    def savefile(self, folder_name=None):
        """
        the method to actually save the file to disk

        :param folder_name: foldername, can be None to save to current directory
        :type folder_name: Union[NoneType, str]
        """
        if folder_name is not None:
            full_path = os.path.normpath(os.path.join(os.getcwd(), folder_name))
            if not os.path.exists(full_path):
                os.makedirs(full_path)
            self.filename = os.path.join(full_path, self.filename)

        if self.append:
            if os.path.exists(self.filename):
                with open(self.filename, 'r') as f:
                    self.append_header = not bool(len(f.readline()))
            self.csv_file = open(self.filename, 'a')
        else:
            self.csv_file = open(self.filename, 'w')

        class CustomDialect(csv.excel):
            delimiter = self.sep

        self.keys = sorted(self.history.keys())

        self.writer = csv.DictWriter(self.csv_file, fieldnames=['epoch'] + self.keys, dialect=CustomDialect)
        if self.append_header:
            self.writer.writeheader()

        for i in self.epoch:
            self.writer.writerow({**{'epoch': self.epoch[i]}, **dict([(k, self.history[k][i]) for k in self.keys])})
        self.csv_file.close() def categorical_crossentropy(y_true, y_pred, sample_weight=None, from_logits=False):
    """
    Categorical cross-entropy between an output tensor and a target tensor, ignoring the magic number

    :param y_true: Ground Truth
    :type y_true: Union(tf.Tensor, tf.Variable)
    :param y_pred: Prediction
    :type y_pred: Union(tf.Tensor, tf.Variable)
    :param sample_weight: Sample weights
    :type sample_weight: Union(tf.Tensor, tf.Variable, list)
    :param from_logits: From logits space or not. If you want to use logits, please use from_logits=True
    :type from_logits: boolean
    :return: Categorical Cross-Entropy
    :rtype: tf.Tensor
    :History: 2018-Jan-14 - Written - Henry Leung (University of Toronto)
    """
    # calculate correction term first
    correction = magic_correction_term(y_true)

    # Deal with magic number
    y_true = tf.where(magic_num_check(y_true), tf.zeros_like(y_true), y_true)

    # Note: tf.nn.softmax_cross_entropy_with_logits expects logits, we expects probabilities by default.
    if not from_logits:
        epsilon_tensor = tf.cast(tf.constant(tfk.backend.epsilon()), tf.float32)
        # scale preds so that the class probas of each sample sum to 1
        y_pred /= tf.reduce_sum(y_pred, len(y_pred.get_shape()) - 1, True)
        # manual computation of crossentropy
        y_pred = tf.clip_by_value(y_pred, epsilon_tensor, 1. - epsilon_tensor)
        losses = - tf.reduce_sum(y_true * tf.math.log(y_pred), len(y_pred.get_shape()) - 1) * correction
        return weighted_loss(losses, sample_weight)
    else:
        losses = tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=y_pred) * correction
        return weighted_loss(losses, sample_weight) def binary_crossentropy(y_true, y_pred, sample_weight=None, from_logits=False):
    """
    Binary cross-entropy between an output tensor and a target tensor, ignoring the magic number

    :param y_true: Ground Truth
    :type y_true: Union(tf.Tensor, tf.Variable)
    :param y_pred: Prediction
    :type y_pred: Union(tf.Tensor, tf.Variable)
    :param from_logits: From logits space or not. If you want to use logits, please use from_logits=True
    :type from_logits: boolean
    :param sample_weight: Sample weights
    :type sample_weight: Union(tf.Tensor, tf.Variable, list)
    :return: Binary Cross-Entropy
    :rtype: tf.Tensor
    :History: 2018-Jan-14 - Written - Henry Leung (University of Toronto)
    """
    # Note: tf.nn.sigmoid_cross_entropy_with_logits expects logits, we expects probabilities by default.
    if not from_logits:
        epsilon_tensor = tf.cast(tf.constant(tfk.backend.epsilon()), tf.float32)
        # transform back to logits
        y_pred = tf.clip_by_value(y_pred, epsilon_tensor, 1. - epsilon_tensor)
        y_pred = tf.math.log(y_pred / (1. - y_pred))

    cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true, logits=y_pred)
    corrected_cross_entropy = tf.where(magic_num_check(y_true), tf.zeros_like(cross_entropy), cross_entropy)

    losses = tf.reduce_mean(corrected_cross_entropy, axis=-1) * magic_correction_term(y_true)
    return weighted_loss(losses, sample_weight) def mean_squared_error(y_true, y_pred, sample_weight=None):
    """
    Calculate mean square error losses

    :param y_true: Ground Truth
    :type y_true: Union(tf.Tensor, tf.Variable)
    :param y_pred: Prediction
    :type y_pred: Union(tf.Tensor, tf.Variable)
    :param sample_weight: Sample weights
    :type sample_weight: Union(tf.Tensor, tf.Variable, list)
    :return: Mean Squared Error
    :rtype: tf.Tensor
    :History: 2017-Nov-16 - Written - Henry Leung (University of Toronto)
    """
    losses = tf.reduce_mean(tf.where(magic_num_check(y_true), tf.zeros_like(y_true),
                                     tf.square(y_true - y_pred)), axis=-1) * magic_correction_term(y_true)
    return weighted_loss(losses, sample_weight) def mean_absolute_error(y_true, y_pred, sample_weight=None):
    """
    Calculate mean absolute error, ignoring the magic number

    :param y_true: Ground Truth
    :type y_true: Union(tf.Tensor, tf.Variable)
    :param y_pred: Prediction
    :type y_pred: Union(tf.Tensor, tf.Variable)
    :param sample_weight: Sample weights
    :type sample_weight: Union(tf.Tensor, tf.Variable, list)
    :return: Mean Absolute Error
    :rtype: tf.Tensor
    :History: 2018-Jan-14 - Written - Henry Leung (University of Toronto)
    """
    losses = tf.reduce_mean(tf.where(magic_num_check(y_true), tf.zeros_like(y_true),
                                     tf.abs(y_true - y_pred)), axis=-1) * magic_correction_term(y_true)
    return weighted_loss(losses, sample_weight) def mean_error(y_true, y_pred, sample_weight=None):
    """
    Calculate mean error as a way to get the bias in prediction, ignoring the magic number

    :param y_true: Ground Truth
    :type y_true: Union(tf.Tensor, tf.Variable)
    :param y_pred: Prediction
    :type y_pred: Union(tf.Tensor, tf.Variable)
    :param sample_weight: Sample weights
    :type sample_weight: Union(tf.Tensor, tf.Variable, list)
    :return: Mean Error
    :rtype: tf.Tensor
    :History: 2018-May-22 - Written - Henry Leung (University of Toronto)
    """
    losses = tf.reduce_mean(tf.where(magic_num_check(y_true), tf.zeros_like(y_true), y_true - y_pred),
                            axis=-1) * magic_correction_term(y_true)
    return weighted_loss(losses, sample_weight) class Normalizer(object):
    """Top-level class for a normalizer"""

    def __init__(self, mode=None):
        """
        NAME:
            __init__
        PURPOSE:
            To define a normalizer
        HISTORY:
            2018-Jan-06 - Written - Henry Leung (University of Toronto)
        """

        self.normalization_mode = mode

        self.featurewise_center = {}
        self.datasetwise_center = {}

        self.featurewise_stdalization = {}
        self.datasetwise_stdalization = {}

        self.mean_labels = {}
        self.std_labels = {}

        self._custom_norm_func = None
        self._custom_denorm_func = None

    def mode_checker(self, data):
        if type(data) is not dict:
            dict_flag = False
            data = {"Temp": data}
            self.mean_labels = {"Temp": self.mean_labels}
            self.std_labels = {"Temp": self.std_labels}
        else:
            dict_flag = True

        master_data = {}
        if type(self.normalization_mode) is not dict:
            self.normalization_mode = list_to_dict(data.keys(), to_iterable(self.normalization_mode))
        for name in data.keys():  # normalize data for each named inputs
            if data[name].ndim == 1:
                data_array = np.expand_dims(data[name], 1)
            else:
                data_array = np.array(data[name])

            self.normalization_mode.update({name: str(self.normalization_mode[name])})  # just to prevent unnecessary type issue

            if data_array.dtype == bool:
                if self.normalization_mode[name] != '0':  # binary classification case
                    warnings.warn("Data type is detected as bool, setting normalization_mode to 0 which is "
                                  "doing nothing because no normalization can be done on bool")
                    self.normalization_mode[name] = '0'
            data_array = data_array.astype(np.float32, copy=False)  # need to convert data to float in every case

            if self.normalization_mode[name] == '0':
                self.featurewise_center.update({name: False})
                self.datasetwise_center.update({name: False})
                self.featurewise_stdalization.update({name: False})
                self.datasetwise_stdalization.update({name: False})
                self.mean_labels.update({name: np.array([0.])})
                self.std_labels.update({name: np.array([1.])})
            elif self.normalization_mode[name] == '1':
                self.featurewise_center.update({name: False})
                self.datasetwise_center.update({name: True})
                self.featurewise_stdalization.update({name: False})
                self.datasetwise_stdalization.update({name: True})
            elif self.normalization_mode[name] == '2':
                self.featurewise_center.update({name: True})
                self.datasetwise_center.update({name: False})
                self.featurewise_stdalization.update({name: True})
                self.datasetwise_stdalization.update({name: False})
            elif self.normalization_mode[name] == '3':
                self.featurewise_center.update({name: True})
                self.datasetwise_center.update({name: False})
                self.featurewise_stdalization.update({name: False})
                self.datasetwise_stdalization.update({name: False})
            elif self.normalization_mode[name] == '3s':  # allow custom function, default to use sigmoid to normalize
                self.featurewise_center.update({name: False})
                self.datasetwise_center.update({name: False})
                self.featurewise_stdalization.update({name: False})
                self.datasetwise_stdalization.update({name: False})
                if self._custom_norm_func is None:
                    self._custom_norm_func = sigmoid
                if self._custom_denorm_func is None:
                    self._custom_denorm_func = sigmoid_inv
                self.mean_labels.update({name: np.array([0.])})
                self.std_labels.update({name: np.array([1.])})
            elif self.normalization_mode[name] == '4':
                self.featurewise_center.update({name: False})
                self.datasetwise_center.update({name: False})
                self.featurewise_stdalization.update({name: True})
                self.datasetwise_stdalization.update({name: False})
            elif self.normalization_mode[name] == '255':
                # Used to normalize 8bit images
                self.featurewise_center.update({name: False})
                self.datasetwise_center.update({name: False})
                self.featurewise_stdalization.update({name: False})
                self.datasetwise_stdalization.update({name: False})
                self.mean_labels.update({name: np.array([0.])})
                self.std_labels.update({name: np.array([255.])})
            else:
                raise ValueError(f"Unknown Mode -> {self.normalization_mode[name]}")
            master_data.update({name: data_array})

        return master_data, dict_flag

    def normalize(self, data, calc=True):
        data_array, dict_flag = self.mode_checker(data)

        for name in data_array.keys():  # normalize data for each named inputs
            magic_mask = [(data_array[name] == MAGIC_NUMBER) | (np.isnan(data_array[name]))]

            try:
                self.mean_labels[name]
            except KeyError:
                self.mean_labels.update({name: np.array([0.])})
            try:
                self.std_labels[name]
            except KeyError:
                self.std_labels.update({name: np.array([1.])})

            if calc is True:  # check if normalizing with predefine values or get a new one
                print(
                    f"""====Message from {self.__class__.__name__}==== \n You selected mode: {self.normalization_mode[name]} \n Featurewise Center: {self.featurewise_center} \n Datawise Center: {self.datasetwise_center} \n Featurewise std Center: {self.featurewise_stdalization} \n Datawise std Center: {self.datasetwise_stdalization} \n ====Message ends====""")

                if self.featurewise_center[name] is True:
                    self.mean_labels.update({name: np.ma.array(data_array[name], mask=magic_mask).mean(axis=0)})
                    data_array[name] -= self.mean_labels[name]
                elif self.datasetwise_center[name] is True:
                    self.mean_labels.update({name: np.ma.array(data_array[name], mask=magic_mask).mean()})
                    data_array[name] -= self.mean_labels[name]

                if self.featurewise_stdalization[name] is True:
                    self.std_labels.update({name: np.ma.array(data_array[name], mask=magic_mask).std(axis=0)})
                    data_array[name] /= self.std_labels[name]
                elif self.datasetwise_stdalization[name] is True:
                    self.std_labels.update({name: np.ma.array(data_array[name], mask=magic_mask).std()})
                    data_array[name] /= self.std_labels[name]
                if self.normalization_mode[name] == '255':
                    data_array[name] -= self.mean_labels[name]
                    data_array[name] /= self.std_labels[name]
            else:
                data_array[name] -= self.mean_labels[name]
                data_array[name] /= self.std_labels[name]

            if self._custom_norm_func is not None:
                data_array.update({name: self._custom_norm_func(data_array[name])})

            np.place(data_array[name], magic_mask, MAGIC_NUMBER)

        if not dict_flag:
            data_array = data_array['Temp']
            self.mean_labels = self.mean_labels['Temp']
            self.std_labels = self.std_labels['Temp']

        return data_array

    def denormalize(self, data):
        data_array, dict_flag = self.mode_checker(data)
        for name in data_array.keys():  # normalize data for each named inputs
            magic_mask = [(data_array[name] == MAGIC_NUMBER) | (np.isnan(data_array[name]))]

            if self._custom_denorm_func is not None:
                data_array[name] = self._custom_denorm_func(data_array[name])
            data_array[name] *= self.std_labels[name]
            data_array[name] += self.mean_labels[name]

            np.place(data_array[name], magic_mask, MAGIC_NUMBER)

        if not dict_flag:
            data_array = data_array["Temp"]
            self.mean_labels = self.mean_labels['Temp']
            self.std_labels = self.std_labels['Temp']

        return data_array class GeneratorMaster(Sequence):
    """
    | Top-level class of astroNN data pipeline to generate data for NNs.
    | It is implemented based on Tensorflow data ``Sequence`` class.

    You need to implement the ``__getitem__`` in the generator sub-class

    :History: 2019-Feb-17 - Updated - Henry Leung (University of Toronto)
    """

    def __init__(self, batch_size, shuffle, steps_per_epoch, data, manual_reset):
        self.batch_size = batch_size
        self.data = data
        self.shuffle = shuffle
        # see if it needs to be reset idx manually if on_epoch_end() cannot be reached like val_generator
        self.manual_reset = manual_reset

        self.steps_per_epoch = steps_per_epoch

    def __len__(self):
        return self.steps_per_epoch

    def _get_exploration_order(self, idx_list):
        """
        :param idx_list:
        :return:
        """
        # shuffle (if applicable) and find exploration order
        if self.shuffle is True:
            idx_list = np.copy(idx_list)
            np.random.shuffle(idx_list)

        return idx_list

    def sparsify(self, y):
        """Returns labels in binary NumPy array"""
        # n_classes =  # Enter number of classes
        # return np.array([[1 if y[i] == j else 0 for j in range(n_classes)]
        #                  for i in range(y.shape[0])])
        pass

    def input_d_checking(self, inputs, idx_list_temp):
        x_dict = {}
        for name in inputs.keys():
            if inputs[name].ndim == 2:
                x = np.empty((len(idx_list_temp), inputs[name].shape[1], 1))
                # Generate data
                x[:, :, 0] = inputs[name][idx_list_temp]

            elif inputs[name].ndim == 3:
                x = np.empty((len(idx_list_temp), inputs[name].shape[1], inputs[name].shape[2], 1))
                # Generate data
                x[:, :, :, 0] = inputs[name][idx_list_temp]

            elif inputs[name].ndim == 4:
                x = np.empty((len(idx_list_temp), inputs[name].shape[1], inputs[name].shape[2], inputs[name].shape[3]))
                # Generate data
                x[:, :, :, :] = inputs[name][idx_list_temp]
            else:
                raise ValueError(f"Unsupported data dimension, your data has {inputs[name].ndim} dimension")

            x_dict.update({name: x})

        return x_dict def dict_np_to_dict_list(input_dict):
    """
    Convert a dict of numpy array to a dict of list
    """
    input_dict = copy.copy(input_dict)
    if type(input_dict) is dict:
        for name in list(input_dict.keys()):
            input_dict.update({name: input_dict[name].tolist()})
        return input_dict
    else:
        return input_dict.tolist() def list_to_dict(names, arrs):
    """
    Matching a list of array with names
    """
    # TODO: need detailed test
    if type(arrs) is list:
        final_dict = {}
        if len(names) == len(arrs):
            for name, arr in zip(names, arrs):
                final_dict.update({name: arr})
        elif len(arrs):
            for name in names:
                final_dict.update({name: arrs[0]})
        else:
            raise IndexError(f"names has a length of {len(names)} but arrs has a  length of {len(arrs)}")
        return final_dict
    elif type(arrs) is np.ndarray and len(names) == 1:
        return {names[0]: arrs}
    elif type(arrs) is np.ndarray and len(names) > 1:
        final_dict = {}
        for name in names:
            final_dict.update({name: arrs})
        return final_dict
    else:
        return arrs def deprecated(func):
    """
    This is a decorator which can be used to mark functions
    as deprecated. It will result in a warning being emitted
    when the function is used.
    """

    @functools.wraps(func)
    def new_func(*args, **kwargs):
        if inspect.isclass(func):
            warnings.warn(f"The use of class {func.__name__}() is deprecated. It will be removed in future",
                          category=DeprecationWarning, stacklevel=2)
        else:
            warnings.warn(f"Call to function {func.__name__}() is deprecated. It will be removed in future",
                          category=DeprecationWarning, stacklevel=2)
        return func(*args, **kwargs)

    return new_func def deprecated_copy_signature(signature_source):
    """
    This is a decorator which can be used to mark old functions
    as deprecated, copy signature from new functions (signature_source). 
    It will result in a warning being emitted when the function is used.
    """
    def deco(target):
        @functools.wraps(target)
        def tgt(*args, **kwargs):
            warnings.warn(f"Call to function {target.__name__}() is deprecated and will be removed in " + 
                          f"future. Use {signature_source.__name__}() instead.", stacklevel=2)
            inspect.signature(signature_source).bind(*args, **kwargs)
            return target(*args, **kwargs)
        tgt.__signature__ = inspect.signature(signature_source)
        return tgt
    return deco
import json
import os
import time
import numpy as np
from abc import ABC
from tqdm import tqdm
from tensorflow import keras as tfk
from astroNN.config import MULTIPROCESS_FLAG
from astroNN.config import _astroNN_MODEL_NAME
from astroNN.models.base_master_nn import NeuralNetMaster
from astroNN.nn.callbacks import VirutalCSVLogger
from astroNN.nn.losses import categorical_crossentropy, binary_crossentropy
from astroNN.nn.losses import mean_squared_error, mean_absolute_error, mean_error
from astroNN.nn.metrics import categorical_accuracy, binary_accuracy
from astroNN.nn.utilities import Normalizer
from astroNN.nn.utilities.generator import GeneratorMaster
from astroNN.shared.dict_tools import dict_np_to_dict_list, list_to_dict
from astroNN.shared.warnings import deprecated, deprecated_copy_signature
from sklearn.model_selection import train_test_split
    def fit(self, input_data, labels, sample_weights=None):
        """
        Train a Convolutional neural network

        :param input_data: Data to be trained with neural network
        :type input_data: ndarray
        :param labels: Labels to be trained with neural network
        :type labels: ndarray
        :param sample_weights: Sample weights (if any)
        :type sample_weights: Union([NoneType, ndarray])
        :return: None
        :rtype: NoneType
        :History: 2017-Dec-06 - Written - Henry Leung (University of Toronto)
        """
        # Call the checklist to create astroNN folder and save parameters
        self.pre_training_checklist_child(input_data, labels, sample_weights)

        reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                                      min_delta=self.reduce_lr_epsilon,
                                      patience=self.reduce_lr_patience, min_lr=self.reduce_lr_min, mode='min',
                                      verbose=2)

        early_stopping = EarlyStopping(monitor='val_loss', min_delta=self.early_stopping_min_delta,
                                       patience=self.early_stopping_patience, verbose=2, mode='min')

        self.virtual_cvslogger = VirutalCSVLogger()

        self.__callbacks = [reduce_lr, self.virtual_cvslogger]  # default must have unchangeable callbacks

        if self.callbacks is not None:
            if isinstance(self.callbacks, list):
                self.__callbacks.extend(self.callbacks)
            else:
                self.__callbacks.append(self.callbacks)

        start_time = time.time()

        self.history = self.keras_model.fit(x=self.training_generator,
                                            validation_data=self.validation_generator,
                                            epochs=self.max_epochs, verbose=self.verbose,
                                            workers=os.cpu_count(),
                                            callbacks=self.__callbacks,
                                            use_multiprocessing=MULTIPROCESS_FLAG)

        print(f'Completed Training, {(time.time() - start_time):.{2}f}s in total')

        if self.autosave is True:
            # Call the post training checklist to save parameters
            self.save()

        return None

    def fit_on_batch(self, input_data, labels, sample_weights=None):
        """
        Train a neural network by running a single gradient update on all of your data, suitable for fine-tuning

        :param input_data: Data to be trained with neural network
        :type input_data: ndarray
        :param labels: Labels to be trained with neural network
        :type labels: ndarray
        :param sample_weights: Sample weights (if any)
        :type sample_weights: Union([NoneType, ndarray])
        :return: None
        :rtype: NoneType
        :History: 2018-Aug-22 - Written - Henry Leung (University of Toronto)
        """

        input_data, labels = self.pre_training_checklist_master(input_data, labels)

        # check if exists (existing means the model has already been trained (e.g. fine-tuning),
        # so we do not need calculate mean/std again)
        if self.input_normalizer is None:
            self.input_normalizer = Normalizer(mode=self.input_norm_mode)
            self.labels_normalizer = Normalizer(mode=self.labels_norm_mode)

            norm_data = self.input_normalizer.normalize(input_data)
            self.input_mean, self.input_std = self.input_normalizer.mean_labels, self.input_normalizer.std_labels
            norm_labels = self.labels_normalizer.normalize(labels)
            self.labels_mean, self.labels_std = self.labels_normalizer.mean_labels, self.labels_normalizer.std_labels
        else:
            norm_data = self.input_normalizer.normalize(input_data, calc=False)
            norm_labels = self.labels_normalizer.normalize(labels, calc=False)

        start_time = time.time()

        fit_generator = CNNDataGenerator(batch_size=input_data['input'].shape[0],
                                         shuffle=False,
                                         steps_per_epoch=1,
                                         data=[norm_data, norm_labels], 
                                         sample_weights=sample_weights)

        scores = self.keras_model.fit(x=fit_generator,
                                      epochs=1,
                                      verbose=self.verbose,
                                      workers=os.cpu_count(),
                                      use_multiprocessing=MULTIPROCESS_FLAG)

        print(f'Completed Training on Batch, {(time.time() - start_time):.{2}f}s in total')

        return None

    def post_training_checklist_child(self):
        self.keras_model.save(self.fullfilepath + _astroNN_MODEL_NAME)
        print(_astroNN_MODEL_NAME + f' saved to {(self.fullfilepath + _astroNN_MODEL_NAME)}')

        self.hyper_txt.write(f"Dropout Rate: {self.dropout_rate} \n")
        self.hyper_txt.flush()
        self.hyper_txt.close()

        data = {'id': self.__class__.__name__ if self._model_identifier is None else self._model_identifier,
                'pool_length': self.pool_length,
                'filterlen': self.filter_len,
                'filternum': self.num_filters,
                'hidden': self.num_hidden,
                'input': self._input_shape,
                'labels': self._labels_shape,
                'task': self.task,
                'last_layer_activation': self._last_layer_activation,
                'activation': self.activation,
                'input_mean': dict_np_to_dict_list(self.input_mean),

                'labels_mean': dict_np_to_dict_list(self.labels_mean),